6 min to read
[스프링부트 시리즈10] 질문 목록 생성하기
SBB의 핵심기능인 질문 목록이 담긴 페이지를 만들어보자.
질문 목록 생성하기
이 질문 목록은 다음 주소에 연결되어야 한다.
http://localhost:8080/question/list
그레이들 프로젝트를 리로드 한 뒤 로컬 서버를 실행한 후 브라우저에서 해당 주소를 입력하여 접속하자. 그러면 화이트레이블 에러 페이지와 404 오류가 발생한다.
질문 목록 URL 매핑하기
1) 404 오류를 해결하려면 /question/list
URL을 매핑하기 위한 컨트롤러가 필요하다. QuestionController.java
파일을 question 패키지 안에 생성해 다음과 같이 작성한다.
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";
}
}
2) 로컬 서버를 재실행하고 다시 http://localhost:8080/question/list
에 접속한다. 그러면 question list 라는 문자열이 출력된다.
문자열이 출력되었으면 매핑에 성공한 것이다.
템플릿 설정하기
앞서 우리는 문자열 ‘question list’를 직접 자바 코드로 작성하여 브라우저에 리턴했다. 하지만 보통 브라우저에 응답하는 문자열은 이 예처럼 자바 코드에서 직접 만들지 않는다. 일반적으로 많이 사용하는 방식은 템플릿(template) 방식이다. 템플릿은 자바 코드를 삽입할 수 있는 HTML 형식의 파일을 말한다.
템플릿을 사용하기 위해 스프링 부트에서는 템플릿 엔진을 지원한다. 템플릿 엔진에는 Thymeleaf, Mustache, Groovy, Freemarker, Velocity 등이 있는데, 이 책에서는 스프링 진영에서 추천하는 타임리프(Thymleaf) 템플릿 엔진을 사용할 것이다.
타임리프를 사용하려면 먼저 설치가 필요하다. 다음과 같이 build.gradle.kts
파일을 수정하여 타임리프를 설치해 보자.
(... 생략 ...)
dependencies {
(... 생략 ...)
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect'
}
(... 생략 ...)
우측 그레이들 패널에서 리로드를 한번 해줘야 설치된다.
타임리프를 적용하려면 로컬 서버 재시작이 필요하다.
템플릿 사용하기
이번에는 템플릿을 생성해 질문 목록 페이지를 만들어 보자.
1) src/main/resources
디렉터리에서 templates
를 선택한 후,
마우스 오른쪽 버튼을 누르고 [New → File]을 클릭한다. 다음과 같이 파일 이름으로 question_list.html를 입력하여 템플릿을 생성한다.
2) question_list.html
파일의 내용은 다음과 같이 작성해 보자.
[파일명: /templates/question_list.html]
<h2>Hello Template</h2>
3) 다시 QuestionController.java
파일로 돌아가 다음과 같이 수정해 보자.
[파일명: /question/questionController.java]
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’를 리턴한다.
4) 그리고 다시 로컬 서버를 실행한 뒤, http://localhost:8080/question/list
에 접속해 보자. 우리가 question_list.html
파일에 작성한 <h2>Hello Template</h2>
내용이 브라우저에 출력되는 것을 확인할 수 있다.
데이터를 템플릿에 전달하기
앞선 실습을 통해 템플릿에 저장된 내용을 화면에 전달하는 것은 성공했다.
이제 질문 목록이 담긴 데이터를 조회하여 이를 템플릿을 통해 화면에 전달해 보려고 한다.
질문 목록과 관련된 데이터를 조회하려면 QuestionRepository
를 사용해야 한다. QuestionRepository
로 조회한 질문 목록 데이터는 Model
클래스를 사용하여 템플릿에 전달할 수 있다. 코드를 작성하면서 더 자세히 알아보자.
먼저, 다음과 같이 QuestionController.java
를 수정해 보자.
[파일명:/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";
}
}
매개변수로
Model
을 지정하면 객체가 자동으로 생성된다.
@RequiredArgsConstructor
애너테이션의 생성자 방식으로 questionRepository
객체를 주입했다. @RequiredArgsConstructor
는 롬복(Lombok)이 제공하는 애너테이션으로, final이 붙은 속성을 포함하는 생성자를 자동으로 만들어 주는 역할을 한다. 따라서 스프링 부트(Spring Boot)가 내부적으로 QuestionController
를 생성할 때 롬복으로 만들어진 생성자에 의해 questionRepository
객체가 자동으로 주입된다.
그리고 QuestionRepository
의 findAll
메서드를 사용하여 질문 목록 데이터인 questionList
를 생성하고 Model
객체에 ‘questionList’라는 이름으로 저장했다. 여기서 Model
객체는 자바 클래스(Java class)와 템플릿(template) 간의 연결 고리 역할을 한다.
Model 객체에 값을 담아 두면 템플릿에서 그 값을 사용할 수 있다. Model
객체는 따로 생성할 필요 없이 컨트롤러의 메서드에 매개변수로 지정하기만 하면 스프링 부트가 자동으로 Model
객체를 생성한다.
데이터를 화면에 출력하기
DB로부터 데이터를 조회하여 Model
객체에 저장하였고, 이제 Model
객체를 통해 전달받은 데이터들을 템플릿에서 활용할 준비를 마쳤다. 그렇다면 이제 질문 목록 데이터를 화면에 노출해 보자.
1) 이전에 입력한 내용을 지우고 다음과 같이 question_list.html
템플릿을 수정해 보자.
[파일명: /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}">
다음 코드도 question 객체의 createDate 를 출력한다.
<td th:text="${question.createDate}"></td>
2) 브라우저에서 다시 http://localhost:8080/question/list
에 접속해 보자.
위와 같이 뜨게 된다.
만약 질문 엔티티에 데이터를 더 추가했다면 더 많은 데이터가 화면에 노출될 것이다.
URL을 매핑하는 것부터 HTML 템플릿을 활용해 웹 페이지에 DB를 읽어 출력하는 것까지 모두 익혔고, SBB 게시판의 웹 페이지 하나를 완성한 것이다.
자주 사용되는 타임리프 3가지 속성
- 분기문 속성
if 문 , else if 문과 같은 분기문은 다음과 같이 사용한다.th:if= "${question != null}"
이 경우 question 객체가 null 이 아닌 경우에만 이 속성을 포함한 요소가 표시된다.
- 반복문 속성
th:each
반복문 속성은 자바의 for each 문과 유사하다.th:each="question : ${questionList}"
반복문 속성은 다음과도 같이 사용할 수 있다.
th:each="question, loop: ${questionList}"
여기서 추가한 loop 객체를 이용해 루프 내에서 다음과 같이 상요할 수 있다.
- loop.index: 루프의 순서(루프 반복순서, 0부터 1씩 증가)
- loop.count: 루프의 순서(루프 반복순서, 1부터 1씩 증가)
- loop.size: 반복 객체의 요소갯수(ex.questionList의 요소 갯수)
- loop.first: 루프의 첫 번째 순서인 경우 true
- loop.last: 루프의 마지막 순서인 경우 true
- loop.odd: 루프의 홀수번째 순서인 경우 true
- loop.even: 루프의 짝수번째 순서인 경우 true
- loop.current: 현재 대입된 객체(여기서는 question과 동일)
- 텍스트 속성
th:text=(속성)
은 해당 요소의 텍스트값을 출력한다.th:text = "${question.subject}"
텍스트는
th:text
속성 대신 아래처럼 대괄호를 써서 값을 직접 출력할 수 있다.<tr th:each="question : ${questionList}"> <td>[[${question.subject}]]</td> <td>[[${question.createDate}]]</td> </tr>