Spring Boot: 스프링 부트 기초 [4]

hyeppy·2025년 9월 7일

Spring Boot

목록 보기
5/6
post-thumbnail

2-07 질문 목록 만들기

2-08 루트 URL 사용하기


스프링 부트는 백엔드 프레임워크인 만큼 일반적인 상황의 경우 백엔드 개발을 위해 사용된다. 하지만 그렇다고 해서 프론트 화면을 만들 수 없는 것은 아닌데, 스프링 부트에서는 서버 사이드 렌더링(SSR)도 지원하기 때문이다. SSR 방식에서는 컨트롤러에서 데이터를 준비하고, 템플릿 엔진을 통해 HTML을 동적으로 만들어서, 브라우저에 완성된 화면을 보내 준다.

  1. 컨트롤러에서 데이터 준비: 비즈니스 로직을 통해 필요한 데이터를 조회
  2. 템플릿 엔진으로 HTML 동적 생성: Thymeleaf, Mustache 등을 활용
  3. 완성된 화면을 브라우저에 전송: 서버에서 렌더링된 HTML 제공

서버 사이드 렌더링 방식은 프론트와 백이 강하게 결합되어 하나의 프로젝트로 끝낼 수 있다는 장점이 있으나, 프론트와 백이 강하게 결합되어 유지보수 시에 불필요한 작업이 늘어날 수 있다는 단점도 있다. SSR의 반대는 CSR로, 프론트와 백을 분리해 개발하는 형식이다.


질문 목록 URL 매핑하기

앞서 스프링 부트 기초 [1] 게시글에서 Controller를 만들어 URL을 매핑하는 과정을 작성한 적이 있다. @ResponseBody 어노테이션을 이용하지 않는다면 반환되는 문자열을 이름으로 가진 템플릿 파일을 찾아 출력한다고 했는데, 이처럼 스프링 부트에서 SSR 기능을 이용하기 위해서는 이 기능을 이용하면 된다.

@Controller
public class QuestionController {

    @GetMapping("/question/list")
    public String list() {
        return "question_list"; // 이름이 question_list인 템플릿 파일을 리턴
    }
}

여기서 핵심은 @ResponseBody 어노테이션을 사용하지 않으면, 반환되는 문자열을 템플릿 파일명으로 인식하여 templates 디렉토리에서 해당 템플릿을 찾아 렌더링한다는 점이다.


데이터를 템플릿에 전달

앞에서 계속 사용하던 실습 코드를 기반으로 설명을 하자면, 질문 목록과 관련된 데이터를 조회하려면 QuestionRepository를 사용해야 한다. 이 레포지토리에서 받은 질문 목록 데이터는 Model 클래스를 사용하여 템플릿에 전달할 수 있다.

import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@Controller
public class QuestionController {

    private final QuestionRepository questionRepository;

    @GetMapping("/question/list")
    public String list(Model model) { // 매개변수로 Model를 지정하면 객체가 자동으로 생성
        List<Question> questionList = this.questionRepository.findAll();
        model.addAttribute("questionList", questionList);
        return "question_list";
    }
}

@RequiredArgsConstructor

  • 롬복(Lombok)이 제공하는 어노테이션
  • final이 붙은 속성을 포함하는 생성자를 자동으로 만들어 주는 역할
  • 의존성 주입을 위한 생성자 코드를 줄여주는 핵심 어노테이

Model

  • 자바 클래스와 템플릿 간의 연결 고리 역할
  • Model 객체에 값을 담아 두면 템플릿에서 그 값을 이용할 수 있음
  • 따로 생성할 필요 없이 컨트롤러의 메서드에 매개변수로 지정하기만 하면 자동 생성

데이터를 화면에 출력

앞선 과정들에선 DB로부터 데이터를 조회하여 Model 객체에 저장하고, Model 객체를 통해 전달받은 데이터를 템플릿에서 사용할 수 있도록 준비를 마쳤다. 이후 프로젝트에 생성되어 있을 templates 디렉토리 아래에 준비를 마친 데이터를 출력할 수 있도록 html 템플릿을 작성해야 한다.

타임리프 (Thymeleaf)

질문 목록 데이터를 화면에 노출하기 전, 타임리프에 대해 먼저 알아보자. 타임리프는 WAS에서 사용 가능한 자바 기반 서버 템플릿 엔진으로, HTML/XML와 같은 마크업 파일에 자바 데이터를 쉽게 바인딩해 동적인 웹 페이지를 만들 수 있다. 타임리프의 특징은 HTML과 자연스럽게 통합되며, 서버 없이도 정적 파일로 브라우저에서 확인이 가능하고, Spring Boot에서 기본 지원한다는 점이다.

th:는 타임리프에서 사용하는 속성임을 나타내며, 이 부분이 자바 코드와 연결되어 마크업 파일에 바인딩될 수 있게 도와준다. 또한 Model 객체에 저장한 데이터는 ${변수} 형태로 읽을 수 있다.

  • 데이터 출력
    <!-- 속성 방식 -->
    <p th:text="${username}">기본값</p>
    
    <!-- 인라인 방식 -->
    <div>[[${value}]]</div>
    타임리프에서 데이터를 출력하는 방법에는 여러 가지가 있다. 속성 방식으로는 <p th:text="${username}">기본값</p>처럼 사용할 수 있고, 인라인 방식으로는 <div>[[${value}]]</div>처럼 사용할 수 있다.
  • 반복문 (자바의 for each와 유사)
    <!-- for each -->
    <ul>
      <li th:each="item : ${items}" th:text="${item}"></li>
    </ul>
    
    <!-- loop 객체 -->
    <ul>
      <li th:each="question, loop : ${items}" th:text="${item}"></li>
    </ul>
    타임리프의 반복문은 자바의 향상된 for문과 유사하다. 기본적인 반복문은 <li th:each="question : ${questionList}" th:text="${question.content}"></li>처럼 사용하며, 루프 상태 정보가 필요한 경우에는 <tr th:each="question, loop : ${questionList}">와 같이 두 번째 매개변수를 추가할 수 있다.
    • 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과 동일)
  • 조건문
    <p th:if="${isAdmin}">관리자 전용 메뉴</p>
  • 링크 처리
    <a th:href="@{/posts/{id}(id=${post.id})}">글 보기</a>
    타임리프에서 링크를 처리할 때는 @{} 문법을 사용한다. 기본 링크는 <a th:href="@{/question/detail}">질문 상세</a>처럼 사용하고, 파라미터가 있는 링크는 <a th:href="@{/posts/{id}(id=${post.id})}">글 보기</a>와 같이 경로 변수와 쿼리 파라미터를 함께 활용할 수 있다.

실제 템플릿 구현

다시 실습으로 돌아와, 앞서 Model 객체에 저장해 둔 데이터를 템플릿으로 전달하게 되면 템플릿에서는 타임리프와 같은 코드로 데이터를 html 형태로 구조화할 수 있게 된다.

<!-- 리스트로 출력 -->
<ul>
    <li th:each="question : ${questionList}" th:text="${question.content}"></li>
</ul>

<!-- 테이블로 출력 -->
<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>

<!-- 조건부 렌더링과 함께 사용하는 실무적인 예시 -->
<div th:if="${questionList != null and !questionList.isEmpty()}">
    <table>
        <tbody>
            <tr th:each="question, iterStat : ${questionList}">
                <td th:text="${iterStat.count}">번호</td>
                <td>
                    <a th:href="@{/question/detail/{id}(id=${question.id})}" 
                       th:text="${question.subject}">제목</a>
                </td>
                <td th:text="${#temporals.format(question.createDate, 'yyyy-MM-dd HH:mm')}">작성일시</td>
            </tr>
        </tbody>
    </table>
</div>

<div th:unless="${questionList != null and !questionList.isEmpty()}">
    <p>등록된 질문이 없습니다.</p>
</div>

루트 URL 사용하기

    @GetMapping("/")
    public String root() {
        return "redirect:/question/list";
    }

웹 애플리케이션에서 사용자가 도메인 루트(”/”)에 접근했을 때 자동으로 출력을 원하는 화면으로 이동하도록 하려면 위와 같은 코드가 필요하다. 이는 마치 실제 건물에서 1층 로비에 들어온 방문객을 안내데스크에서 안내하는 것과 같은 개념이다.

구현 방식을 설명하자면 이전에 학습한 다른 기능들과 동일한 방식으로 @GetMapping("/")을 이용해 루트 URL을 매핑하고, 반환값으로 "redirect:/"를 이용한다. 여기서 redirect:라는 접두사는 Spring에게 단순히 뷰를 렌더링하는 것이 아니라, 클라이언트를 다른 URL로 재전송하라고 지시한다.


profile
Backend

0개의 댓글