백엔드/SpringBoot

Thymeleaf

mino28 2025. 7. 7. 22:27

타임리프(Thymeleaf)는 자바 기반 웹 애플리케이션에서 사용되는 현대적인 서버 사이드 템플릿 엔진으로, HTML, XML, JavaScript, CSS 등 다양한 마크업 언어와 자연스럽게 통합됩니다. 특히 HTML 파일을 그대로 브라우저에서 열어도 구조를 유지할 수 있어 디자이너와 개발자가 협업하기에 유리하며, ${변수}나 th:text, th:if 같은 속성 기반 문법을 통해 동적인 화면 구성이 가능하고, Spring MVC와도 강력하게 통합되어 컨트롤러에서 넘겨준 데이터를 직관적으로 표현할 수 있습니다.

 

1. 문법 구조

<!-- 반복문과 조건문 -->
<ul>
  <li th:each="item : ${items}" th:text="${item}" th:if="${item != '삭제'}"></li>
</ul>

<!-- 폼 제출 -->
<form th:action="@{/register}" method="post">
  <input type="text" th:field="*{username}" />
  <input type="password" th:field="*{password}" />
  <button type="submit">가입</button>
</form>

 

 

2. 회원 가입 예제

테이블

CREATE TABLE member (
    id INT AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(50) NOT NULL UNIQUE,
    password VARCHAR(100) NOT NULL,
    name VARCHAR(50) NOT NULL
);

 

build.gradle

...
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.3'
    compileOnly 'org.projectlombok:lombok'
    developmentOnly 'org.springframework.boot:spring-boot-devtools'
    runtimeOnly 'com.mysql:mysql-connector-j'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
...

 

application.properties

application.properties는 Spring Framework에서 애플리케이션의 설정 정보를 정의하는 파일로, 서버 포트, 데이터베이스 연결 정보, 로깅 수준, 경로 설정 등 다양한 환경설정을 key-value 형태로 작성할 수 있습니다. 이 파일은 src/main/resources 디렉토리에 위치하며, Spring Boot는 이 파일의 설정값을 자동으로 읽어와 애플리케이션 구동 시 적용합니다. 이를 통해 코드 변경 없이 운영환경에 맞는 설정을 쉽게 관리할 수 있습니다.

server.port=8080
spring.datasource.url=jdbc:mysql://localhost:3306/mydb
spring.datasource.username=root

 

application.yaml은 들여쓰기를 기반으로 한 계층적 구조를 사용하여 설정을 트리 형태로 표현할 수 있어 가독성이 높고 복잡한 설정을 더 명확하게 나타낼 수 있습니다.

server:
  port: 8080
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/mydb
    username: root
spring.application.name=MemberTest

spring.datasource.url=jdbc:mysql://localhost:3306/test?serverTimezone=Asia/Seoul&characterEncoding=UTF-8
spring.datasource.username=root
spring.datasource.password=1234
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

mybatis.mapper-locations=classpath:mapper/*.xml
mybatis.type-aliases-package=com.example.member.dto

spring.datasource.hikari.connection-test-query=SELECT 1
spring.datasource.hikari.maximum-pool-size=10

logging.level.com.example=DEBUG
logging.level.org.mybatis=DEBUG

 

member-mapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.member.mapper.MemberMapper">

    <insert id="save" parameterType="MemberDTO">
        INSERT INTO member (username, password, name)
        VALUES (#{username}, #{password}, #{name})
    </insert>

    <select id="findByUsername" resultType="MemberDTO">
        SELECT * FROM member WHERE username = #{username}
    </select>

    <update id="update" parameterType="MemberDTO">
        UPDATE member SET password = #{password}, name = #{name}
        WHERE id = #{id}
    </update>

</mapper>

 

MemberDTO.java

@Data는 Lombok 라이브러리에서 제공하는 애노테이션으로, 자바 클래스에 자주 사용하는 보일러플레이트 코드(boilerplate code)를 자동으로 생성해주는 매우 유용한 기능입니다. @Data가 자동 생성하는 요소는 아래와 같습니다.

  1. Getter 메서드 (모든 필드에 대해)
  2. Setter 메서드 (모든 필드에 대해 final이 아닌 경우)
  3. toString() 메서드
  4. equals() 및 hashCode() 메서드
  5. @RequiredArgsConstructor (final 필드나 @NonNull 필드를 매개변수로 받는 생성자)
package com.example.member.dto;

import lombok.Data;

@Data
public class MemberDTO {
    private int id;
    private String username;
    private String password;
    private String name;
}

 

MemberMapper.java

@Mapper는 MyBatis 프레임워크에서 사용되는 애노테이션으로, 인터페이스가 SQL 매퍼임을 명시하는 데 사용됩니다. 이 애노테이션을 통해 MyBatis는 해당 인터페이스를 구현체로 인식하고, XML 매퍼 파일이나 어노테이션 기반 SQL과 연결하여 자동으로 매핑을 처리할 수 있습니다.

  • SQL Mapper 인터페이스 지정
    @Mapper가 붙은 인터페이스는 MyBatis가 자동으로 프록시 객체를 생성하여, 실제 SQL 실행 코드를 자동으로 주입합니다.
  • Spring과의 통합
    Spring Boot와 함께 사용할 경우, @Mapper를 사용하면 Mapper 인터페이스가 자동으로 빈(bean)으로 등록되어 DI(의존성 주입)에 사용할 수 있습니다.
package com.example.member.mapper;

import com.example.member.dto.MemberDTO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

@Mapper
public interface MemberMapper {
    MemberDTO findByUsername(@Param("username") String username);
    void save(MemberDTO member);
    void update(MemberDTO member);
}

 

빈(Bean)

  • 자바에서 클래스를 만들면 new로 객체를 직접 생성하지만 Spring에서는 필요한 객체를 직접 만들지 않고, Spring이 대신 만들어서 관리해줍니다. 이처럼 Spring 컨테이너가 만들어서 관리하는 객체를 "빈(Bean)"이라고 부릅니다.
@Component
public class MemberService {
    public void print() {
        System.out.println("회원 서비스 실행");
    }
}

 

의존성 주입(DI: Dependency Injection)

  • 어떤 클래스가 다른 클래스를 사용할 때, 그 객체를 직접 만들지 않고 Spring에게 주세요! 라고 요청하는 방식입니다. 이렇게 다른 객체를 필요한 시점에 자동으로 넣어주는 것을 의존성 주입(DI)이라고 합니다.
@Repository
public class MemberRepository {
    public void save() {
        System.out.println("회원 저장됨!");
    }
}
@Service
public class MemberService {

    // Spring이 이 부분을 자동으로 채워줌 (의존성 주입)
    @Autowired
    private MemberRepository memberRepository;

    public void join() {
        memberRepository.save();
    }
}

 

MemberController.java

package com.example.member.controller;

import com.example.member.dto.MemberDTO;
import com.example.member.service.MemberService;
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;

@Controller
@RequiredArgsConstructor
public class MemberController {

    private final MemberService service;

    @GetMapping("/login")
    public String loginPage() {
        return "login";
    }

    @PostMapping("/login")
    public String login(@RequestParam String username,
                        @RequestParam String password,
                        HttpSession session,
                        Model model) {
        if (service.login(username, password, session)) {
            return "redirect:/home";
        } else {
            model.addAttribute("error", "로그인 실패");
            return "login";
        }
    }

    @GetMapping("/register")
    public String registerPage() {
        return "register";
    }

    @PostMapping("/register")
    public String register(MemberDTO member) {
        service.register(member);
        return "redirect:/login";
    }

    @GetMapping("/home")
    public String home(HttpSession session, Model model) {
        MemberDTO user = (MemberDTO) session.getAttribute("loginUser");
        if (user == null) return "redirect:/login";
        model.addAttribute("user", user);
        return "home";
    }

    @GetMapping("/update")
    public String updatePage(HttpSession session, Model model) {
        MemberDTO user = (MemberDTO) session.getAttribute("loginUser");
        if (user == null) return "redirect:/login";
        model.addAttribute("member", user);
        return "update";
    }

    @PostMapping("/update")
    public String update(MemberDTO member, HttpSession session) {
        service.update(member);
        session.setAttribute("loginUser", member);
        return "redirect:/home";
    }

    @GetMapping("/logout")
    public String logout(HttpSession session) {
        service.logout(session);
        return "redirect:/login";
    }
}

 

MemberService.java

package com.example.member.service;

import com.example.member.dto.MemberDTO;
import jakarta.servlet.http.HttpSession;

public interface MemberService {
    boolean login(String username, String password, HttpSession session);
    void register(MemberDTO member);
    void update(MemberDTO member);
    void logout(HttpSession session);
}

 

MemberServiceImpl.java

@RequiredArgsConstructor

  • 클래스에 있는 final 필드나 @NonNull이 붙은 필드에 대해 생성자(constructor)를 자동으로 생성해주는 Lombok 애노테이션입니다. 즉, 필수 필드만을 매개변수로 받는 생성자를 만들어줍니다.
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@RequiredArgsConstructor
@Service
public class MemberService {
    private final MemberRepository memberRepository;
}

위 코드는 Lombok이 아래 생성자를 자동으로 만들어줍니다

 

public MemberService(MemberRepository memberRepository) {
    this.memberRepository = memberRepository;
}
package com.example.member.service;

import com.example.member.dto.MemberDTO;
import com.example.member.mapper.MemberMapper;
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

@Service
@RequiredArgsConstructor
public class MemberServiceImpl implements MemberService {

    private final MemberMapper mapper;

    @Override
    public boolean login(String username, String password, HttpSession session) {
        MemberDTO member = mapper.findByUsername(username);
        if (member != null && StringUtils.hasText(password) && password.equals(member.getPassword())) {
            session.setAttribute("loginUser", member);
            return true;
        }
        return false;
    }

    @Override
    public void register(MemberDTO member) {
        mapper.save(member);
    }

    @Override
    public void update(MemberDTO member) {
        mapper.update(member);
    }

    @Override
    public void logout(HttpSession session) {
        session.invalidate();
    }
}

 

login.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
  <meta charset="UTF-8">
  <title>로그인</title>
</head>
<body>
<h2>로그인</h2>
<form th:action="@{/login}" method="post">
  <p>아이디: <input type="text" name="username" /></p>
  <p>비밀번호: <input type="password" name="password" /></p>
  <p><button type="submit">로그인</button></p>
</form>
<p style="color:red;" th:if="${error}" th:text="${error}"></p>
<p><a th:href="@{/register}">회원가입</a></p>
</body>
</html>

 

register.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
  <meta charset="UTF-8">
  <title>회원가입</title>
</head>
<body>
<h2>회원가입</h2>
<form th:action="@{/register}" method="post">
  <p>아이디: <input type="text" name="username" /></p>
  <p>비밀번호: <input type="password" name="password" /></p>
  <p>이름: <input type="text" name="name" /></p>
  <p><button type="submit">가입</button></p>
</form>
<p><a th:href="@{/login}">로그인으로</a></p>
</body>
</html>

 

home.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
  <meta charset="UTF-8">
  <title>홈</title>
</head>
<body>
<h2 th:text="'환영합니다, ' + ${user.name} + '님!'"></h2>
<ul>
  <li>아이디: <span th:text="${user.username}"></span></li>
  <li>이름: <span th:text="${user.name}"></span></li>
</ul>
<p><a th:href="@{/update}">회원정보 수정</a></p>
<p><a th:href="@{/logout}">로그아웃</a></p>
</body>
</html>

 

update.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
  <meta charset="UTF-8">
  <title>회원정보 수정</title>
</head>
<body>
<h2>회원정보 수정</h2>
<form th:action="@{/update}" method="post">
  <input type="hidden" name="id" th:value="${member.id}" />
  <p>아이디: <input type="text" name="username" th:value="${member.username}" readonly /></p>
  <p>비밀번호: <input type="password" name="password" /></p>
  <p>이름: <input type="text" name="name" th:value="${member.name}" /></p>
  <p><button type="submit">수정</button></p>
</form>
<p><a th:href="@{/home}">홈으로</a></p>
</body>
</html>

'백엔드 > SpringBoot' 카테고리의 다른 글

REST API 구현  (0) 2025.07.07
스프링 VS 스프링부트  (0) 2025.07.07
웹서버와 WAS  (0) 2025.07.07