타임리프(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가 자동 생성하는 요소는 아래와 같습니다.
- Getter 메서드 (모든 필드에 대해)
- Setter 메서드 (모든 필드에 대해 final이 아닌 경우)
- toString() 메서드
- equals() 및 hashCode() 메서드
- @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 |