02G. 질문 목록과 템플릿
02G. 질문 목록과 템플릿 관련
이번 장에서는 SBB의 핵심 기능인 질문 목록을 구현할 것이다. 그럼 시작해 보자.
우리가 원하는 질문 목록은 다음 주소에 접속할 때 동작해야 한다. 로컬 서버를 실행하고 웹 브라우저에서 http://localhost:8080/question/list
에 접속해 보자.
404 오류 해결하기
404 오류를 해결하려면 /question/list
URL에 대한 매핑이 있는 컨트롤러가 필요하다. QuestionController.java
파일을 다음과 같이 신규 작성하자.
파일명:
/sbb/src/main/java/com/mysite/sbb/question
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) 템플릿 엔진을 사용할 것이다.
- 타임리프 - https://www.thymeleaf.org
타임리프를 사용하려면 설치가 필요하다. 다음과 같이 build.gradle
파일을 수정하자.
파일명:
/sbb/
build.gradle
// (... 생략 ...)
dependencies {
// (... 생략 ...)
implementation "org.springframework.boot:spring-boot-starter-thymeleaf"
implementation "nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect"
}
// (... 생략 ...)
위와 같이 수정하고 ["Refresh Gradle Project"]
로 필요한 라이브러리를 설치하자.
타임리프 템플릿 엔진을 적용하기 위해서는 로컬서버 재시작이 필요하다. 로컬서버를 반드시 재시작하고 이후 과정을 진행하자.
템플릿 사용하기
.question_list.html
파일의 내용은 다음과 같이 작성하자.
파일명:
/sbb/src/main/resources/templates/
question_list.html
<h2>Hello Template</h2>
question_list.html
파일이 바로 템플릿 파일이다.
그리고 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")
// @ResponseBody
public String list() {
return "question_list";
}
}
리턴 문자열은 "question list"가 아닌 "question_list" 임에 주의하자.
템플릿을 사용하기 때문에 기존에 사용했던 @ResponseBody
애너테이션은 필요없으므로 삭제한다. 그리고 list 메서드에서 question_list.html
템플릿 파일의 이름인 "question_list
"를 리턴한다.
그리고 다시 http://localhost:8080/question/list
에 접속해 보자.
데이터 조회하여 템플릿에 전달하기
템플릿의 내용을 화면에 전달하는 것은 성공했다. 이제 템플릿에 질문 목록을 조회하여 출력해 보자. 질문 목록을 조회하기 위해서는 Question 리포지터리를 사용해야 한다. Question 리포지터리로 조회한 질문 목록은 Model 클래스를 사용하여 템플릿에 전달할수 있다. 예제를 통해 알아보자.
다음과 같이 QuestionController
를 수정하자.
파일명:
/sbb/src/main/java/com/mysite/sbb/question/
QuestionController.java
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<Question> `questionList` = this.questionRepository.findAll();
model.addAttribute("`questionList`", `questionList`);
return "question_list";
}
}
우선 @RequiredArgsConstructor
애너테이션으로 questionRepository
속성을 포함하는 생성자를 생성하였다. @RequiredArgsConstructor
는 롬복이 제공하는 애너테이션으로 final이 붙은 속성을 포함하는 생성자를 자동으로 생성하는 역할을 한다. 롬복의 @Getter
, @Setter
가 자동으로 Getter, Setter 메서드를 생성하는 것과 마찬가지로 @RequiredArgsConstructor
는 자동으로 생성자를 생성한다. 따라서 스프링 의존성 주입 규칙에 의해 questionRepository
객체가 자동으로 주입된다.
스프링의 의존성 주입(Dependency Injection) 방식 3가지
@Autowired
속성: 속성에@Autowired
애너테이션을 적용하여 객체를 주입하는 방식- 생성자: 생성자를 작성하여 객체를 주입하는 방식 (권장하는 방식)
- Setter: Setter 메서드를 작성하여 객체를 주입하는 방식 (메서드에
@Autowired
애너테이션 적용이 필요하다.)
테스트코드(
SbbApplicationTests.java
)에서는 속성에@Autowired
애너테이션을 사용하여 객체를 주입했다.
그리고 Question 리포지터의 findAll 메서드를 사용하여 질문 목록 데이터인 questionList
를 생성하고 Model 객체에 "questionList
" 라는 이름으로 값을 저장했다. Model 객체는 자바 클래스와 템플릿 간의 연결고리 역할을 한다. Model 객체에 값을 담아두면 템플릿에서 그 값을 사용할 수 있다.
Model 객체는 따로 생성할 필요없이 컨트롤러 메서드의 매개변수로 지정하기만 하면 스프링부트가 자동으로 Model 객체를 생성한다.
템플릿에서 전달받은 데이터 사용하기
Model 객체에 저장한 값을 템플릿에서 사용할 수 있다고 했다. 어떻게 사용할수 있을까? 다음과 같이 question_list.html 템플릿을 수정해 보자.
파일명:
/sbb/src/main/resources/templates/
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>
질문 목록을 HTML의 테이블 구조로 표시되게 하였다.
템플릿 파일에 입력된 th:each="question : ${questionList}"
와 같은 특이한 표현이 눈에띌 것이다. th:
로 시작하는 속성은 타임리프 템플릿 엔진이 사용하는 속성이다. 바로 이 부분이 자바 코드와 연결된다. question_list.html
파일에 사용한 타임리프 속성들을 잠시 살펴보자.
<tr th:each="question : ${`questionList`}">
QuestionController
의 list 메서드에서 조회한 질문 목록 데이터를 "questionList
"라는 이름으로 Model 객체에 저장했다. 타임리프는 Model 객체에 저장된 값을 읽을 수 있으므로 템플릿에서 questionList
를 사용할수 있게 되는 것이다. 위의 코드는 <tr> ... </tr>
엘리먼트를 questionList
의 갯수만큼 반복하여 출력하는 역할을 한다. 그리고 questionList
에 저장된 데이터를 하나씩 꺼내 question 객체에 대입하여 반복구간 내에서 사용할수 있게 한다. 자바의 for each 문을 떠올리면 쉽게 이해할 수 있을 것이다.
다음 코드는 바로 앞의 for 문에서 얻은 question
객체의 제목을 <td>
엘리먼트의 텍스트로 출력한다.
<td th:text="${question.subject}"></td>
다음 코드도 같은 맥락으로 이해할 수 있다.
<td th:text="${question.createDate}"></td>
이제 브라우저에서 다시 http://localhost:8080/question/list
에 접속해 보자.
이전에 테스트로 등록한 질문 1건이 조회된 모습이다. 만약 테스트시 Question 데이터를 더 추가했다면 더 많은 질문이 표시될 것이다.
자주 사용하는 타임리프의 속성
타임리프의 자주 사용하는 속성에는 다음 3가지 유형이 있다. 이 3가지 유형만 알아도 여러 기능을 충분히 만들 수 있다.
1. 분기문 속성
분기문 속성은 다음과 같이 사용한다.
th:if="${question != null}"
위의 경우 question 객체가 null 이 아닌 경우에 해당 엘리먼트가 표시된다.
2. 반복문 속성
반복문은 반복횟수만큼 해당 엘리먼트를 반복하여 표시한다. 반복문 속성은 자바의 for each 문과 유사하다.
th:each="question : ${`questionList`}"
반복문은 다음과 같이 사용할 수도 있다.
th:each="question, loop : ${`questionList`}"
추가한 loop 객체를 이용하여 루프 내에서 다음과 같은 속성을 사용할수 있다.
loop.index
: 반복 순서, 0부터 1씩 증가loop.count
: 반복 순서, 1부터 1씩 증가loop.size
: 반복 객체의 요소 갯수 (예:questionList
의 요소 갯수)loop.first
: 루프의 첫번째 순서인 경우true
loop.last
: 루프의 마지막 순서인 경우true
loop.odd
: 루프의 홀수번째 순서인 경우true
loop.even
: 루프의 짝수번째 순서인 경우true
loop.current
: 현재 대입된 객체 (예: 위의 경우question
과 동일)
3. 텍스트 속성
th:text=값
속성은 해당 엘리먼트의 텍스트로 "값"을 출력한다.
th:text="${question.subject}"
텍스트는 th:text 속성 대신에 다음처럼 대괄호를 사용하여 값을 직접 출력할수 있다.
<tr th:each="question : ${`questionList`}">
<td>[[${question.subject}]]</td>
<td>[[${question.createDate}]]</td>
</tr>
이 책에서는 새로운 타임리프 문법이 나올 때마다 자세히 설명할 것이므로 지금 당장 모든 타임리프의 속성에 대해 알아 둘 필요는 없다.
이상과 같이 질문 목록을 만들었다.
축하한다!!