우리가 원하는 질문 목록은 다음 주소에 접속할 때 동작해야 한다. 로컬 서버를 실행하고 웹 브라우저에서 http://localhost:8080/question/list 에 접속해 보자. 아마 다음과 같은 404 오류페이지가 나타날 것이다.
404 오류를 해결하려면 /question/list URL에 대한 매핑이 있는 컨트롤러가 필요하다. QuestionController.java 파일을 다음과 같이 신규 작성하자.
package com.mysite.sbb.question;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class QuestionController {
@GetMapping("/question/list")
@ResponseBody
public String list() {
return "question list";
}
}
이렇게 수정하고 다시 http://localhost:8080/question/list에 접속하면 화면에 "question list" 문자열이 출력될 것이다.
하지만 보통 브라우저에서 응답하는 문자열은 위의 예처럼 자바 코드에서 직접 만들지는 않는다.
위에서는 "question list"라는 문자열을 직접 자바 코드로 작성하여 브라우저에 리턴했다.
일반적으로 많이 사용하는 방식은 템플릿 방식이다. 템플릿은 자바 코드를 삽입할 수 있는 HTML 형식의 파일이다.
템플릿을 어떻게 사용할수 있는지 알아보자. 스프링부트에서 사용할수 있는 템플릿 엔진에는 Thymeleaf, Mustache, Groovy, Freemarker, Velocity 등이 있다. 이 책에서는 스프링 진영에서 추천하는 타임리프(Thymleaf) 템플릿 엔진을 사용할 것이다.
QuestionController를 다음과 같이 수정하자.
package com.mysite.sbb.question;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class QuestionController {
@GetMapping("/question/list")
public String list() {
return "question_list";
}
}
템플릿을 사용하기 때문에 기존에 사용했던 @ResponseBody 어노테이션은 필요없으므로 삭제한다. 그리고 list 메서드에서 question_list.html 템플릿 파일의 이름인 "question_list"를 리턴한다.
그리고 다시 http://localhost:8080/question/list에 접속해 보자. 우리가 question_list.html 파일에 작성한
템플릿의 내용을 화면에 전달하는 것은 성공했다. 이제 템플릿에 질문 목록을 조회하여 출력해 보자. 질문 목록을 조회하기 위해서는 Question 리포지토리를 사용해야 한다. Question 리포지토리로 조회한 질문 목록은 Model 클래스를 사용하여 템플릿에 전달할 수 있다.
package com.mysite.sbb.question;
import java.util.List;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Controller
public class QuestionController {
private final QuestionRepository questionRepository;
@GetMapping("/question/list")
public String list(Model model) {
List questionList = this.questionRepository.findAll();
model.addAttribute("questionList", questionList);
return "question_list";
}
}
우선 @RequiredArgsConstructor 어노테이션으로 questionRepository 속성을 포함하는 생성자를 생성하였다.
@RequiredArgsConstructor는 롬북이 제공하는 어노테이션으로 final이 붙은 속성을 포함하는 생성자를 자동으로 생성하는 역할을 한다. 롬북의 @Getter, @Setter가 자동으로 Getter, Setter 메서드를 생성하는 것과 마찬가지로 @RequiredArgsConstructor는 자동으로 생성자를 생성한다. 따라서 스프링 의존성 주입 규칙에 의해 questionRepository 객체가 자동으로 주입된다.
그리고 Question 리포지터의 findAll 메서드를 사용하여 질문 목록 데이터인 questionList를 생성하고 Model 객체에 "questionList" 라는 이름으로 값을 저장했다. Model 객체는 자바 클래스와 템플릿 간의 연결고리 역할을 한다. Model 객체에 값을 담아두면 템플릿에서 그 값을 사용할 수 있다.
Model 객체는 따로 생성할 필요없이 컨트롤러 메서드의 매개변수로 지정하기만 하면 스프링부트가 자동으로 Model 객체를 생성한다.
Model 객체에 저장한 값을 템플릿에서 사용할 수 있다고 했다. 어떻게 사용할수 있을까? 다음과 같이 question_list.html 템플릿을 수정해 보자.
<table>
<thead>
<tr>
<th>제목</th>
<th>작성일시</th>
</tr>
</thead>
<tbody>
<tr th:each="question : ${questionList}">
<td th:text="${question.subject}"></td>
<td th:text="${question.createDate}"></td>
</tr>
</tbody>
</table>
템플릿 파일에 입력된 th:each="question : ${questionList}"와 같은 특이한 표현이 눈에띌 것이다. th:로 시작하는 속성은 타임리프 템플릿 엔진이 사용하는 속성이다. 바로 이 부분이 자바 코드와 연결된다.
QuestionController의 list 메서드에서 조회한 질문 목록 데이터를 "questionList"라는 이름으로 Model 객체에 저장했다. 타임리프는 Model 객체에 저장된 값을 읽을 수 있으므로 템플릿에서 questionList를 사용할수 있게 되는 것이다. 위의 코드는 ... 엘리먼트를 questionList의 갯수만큼 반복하여 출력하는 역할을 한다. 그리고 questionList에 저장된 데이터를 하나씩 꺼내 question 객체에 대입하여 반복구간 내에서 사용할 수 있게 한다. 자바의 for each 문을 떠올리면 쉽게 이해할 수 있을 것이다.
다음 코드는 바로 앞의 for 문에서 얻은 question 객체의 제목을 엘리먼트의 텍스트로 출력한다.

이전에 테스트로 등록한 질문 1건이 조회된 모습이다. 만약 테스트시 Question 데이터를 더 추가했다면 더 많은 질문이 표시될 것이다.