사랑하애오
article thumbnail

우리가 원하는 질문 목록은 다음 주소에 접속할 때 동작해야 한다.
로컬 서버를 실행하고 웹 브라우저에서 http://localhost:8080/question/list에 접속해 보자.
아마 다음과 같은 404 오류페이지가 나타날 것이다.

 

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.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class QuestionController {

    @RequestMapping("/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) 템플릿 엔진을 사용할 것이다.

 

Thymeleaf

Integrations galore Eclipse, IntelliJ IDEA, Spring, Play, even the up-and-coming Model-View-Controller API for Java EE 8. Write Thymeleaf in your favourite tools, using your favourite web-development framework. Check out our Ecosystem to see more integrati

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 템플릿 파일을 신규로 작성하자.

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.RequestMapping;

@Controller
public class QuestionController {

    @RequestMapping("/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_list.html 파일에 작성한 <h2>Hello Template</h2> 내용이 브라우저에 출력되는 것을 확인할 수 있다.

 

데이터 조회하여 템플릿에 전달하기

템플릿의 내용을 화면에 전달하는 것은 성공했다.
이제 템플릿에 질문 목록을 조회하여 출력해 보자.
질문 목록을 조회하기 위해서는 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.RequestMapping;

import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@Controller
public class QuestionController {

    private final QuestionRepository questionRepository;

    @RequestMapping("/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>

새로운 타임리프 문법이 나올 때마다 자세히 설명할 것이므로
지금 당장 모든 타임리프의 속성에 대해 알아 둘 필요는 없다.

이상과 같이 질문 목록을 만들었다.
축하한다!!

profile

사랑하애오

@사랑하애

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!