๐Ÿฏ[TIL] 250709-028

byoยท2025๋…„ 7์›” 9์ผ

๐Ÿ’ซ JPA

  • JPA: ์ž๋ฐ” ๊ฐ์ฒด์™€ ๊ด€๊ณ„ํ˜• ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ๋งคํ•‘ํ•˜๋Š” ํ‘œ์ค€ API
  • ORM(Objectโ€‘Relational Mapping): ๊ฐ์ฒด ์ง€ํ–ฅ ๋ชจ๋ธ๊ณผ RDB ํ…Œ์ด๋ธ” ๊ฐ„ ๋ณ€ํ™˜ ์ฑ…์ž„
  • Persistence Context: 1์ฐจ ์บ์‹œ๋กœ ์—”ํ‹ฐํ‹ฐ ์ƒํƒœ ๊ด€๋ฆฌ
  • EntityManager: ์˜์†์„ฑ ์ž‘์—…์˜ ์ง„์ž…์ 
  • ์ฃผ์š” ์–ด๋…ธํ…Œ์ด์…˜: @Entity, @Id, @Column, @OneToMany ๋“ฑ
  • JPQL: SQL์ฒ˜๋Ÿผ ๋ณด์ด์ง€๋งŒ ์—”ํ‹ฐํ‹ฐ๋ฅผ ๋Œ€์ƒ์œผ๋กœ ์ฟผ๋ฆฌ ์ž‘์„ฑ ๊ฐ€๋Šฅ
์–ด๋…ธํ…Œ์ด์…˜์„ค๋ช…
@Entity์—”ํ‹ฐํ‹ฐ ํด๋ž˜์Šค๋กœ ํ‘œ์‹œ
@Table(name=)ํ…Œ์ด๋ธ” ๋ช… ์ง€์ •
@Id๊ธฐ๋ณธ ํ‚ค ํ•„๋“œ ์ง€์ •
@GeneratedValueํ‚ค ์ž๋™ ์ƒ์„ฑ ์ „๋žต ์ง€์ •
@Column์ปฌ๋Ÿผ ๋งคํ•‘ ์†์„ฑ(์ด๋ฆ„, ๋„ ํ—ˆ์šฉ ๋“ฑ)
@ManyToOneN:1 ์—ฐ๊ด€ ๋งคํ•‘ (์™ธ๋ž˜ํ‚ค)
@OneToMany1:N ์—ฐ๊ด€ ๋งคํ•‘ (mappedBy ์‚ฌ์šฉ)
@JoinColumnFK ์ปฌ๋Ÿผ ๋ช… ์ง€์ •
@Embedded๊ฐ’ ํƒ€์ž…(๋‚ด์žฅ ๊ฐ์ฒด) ๋งคํ•‘

๐Ÿ Simple_board with JPA

๐ŸŒฟsimple_board.git

โœ… ์‹ค์Šต ์˜์กด์„ฑ ๋ชฉ๋ก

Spring Boot DevTools Developer Tools
Provides fast application restarts, LiveReload, and configurations for enhanced development experience.

Lombok Developer Tools
Java annotation library which helps to reduce boilerplate code.

Spring Web Web
Build web, including RESTful, applications using Spring MVC. Uses Apache Tomcat as the default embedded container.

Thymeleaf Template Engines
A modern server-side Java template engine for both web and standalone environments. Allows HTML to be correctly displayed in browsers and as static prototypes.

Spring Data JPA SQL
Persist data in SQL stores with Java Persistence API using Spring Data and Hibernate.

PostgreSQL Driver SQL
A JDBC and R2DBC driver that allows Java programs to connect to a PostgreSQL database using standard, database independent Java code.

Validation I/O
Bean Validation with Hibernate validator.

โœ… PostgreSql ๊ณต๊ฐ„ ๋งŒ๋“ค์–ด์ฃผ๊ธฐ
JPA๊ฐ€ ์ž๋™์œผ๋กœ ํ…Œ์ด๋ธ”์„ ์ƒ์„ฑํ•ด ์ฃผ๊ธฐ๋Š” ํ•˜๋Š”๋ฐ, ์šฐ์„  postgres์—์„œ ์œ ์ €์™€ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๊ณต๊ฐ„์€ ๋งŒ๋“ค์–ด ์ฃผ์–ด์•ผ ํ•œ๋‹ค.

psql -U postgres
CREATE USER simple_board WITH PASSWORD 'password';
CREATE DATABASE simple_board OWNER simple_board;

๐Ÿง  model.User.java

@Entity
@Table(name="users")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class User {
    // ๊ธฐ๋ณธ ํ‚ค(primary key) ํ•„๋“œ๋กœ ์ง€์ •
    @Id

    // ๊ธฐ๋ณธ ํ‚ค๋ฅผ DB์—์„œ ์ž๋™ ์ฆ๊ฐ€์‹œํ‚ค๋„๋ก ์„ค์ •.
    // PostgreSQL์˜ SERIAL ๋˜๋Š” IDENTITY ์ปฌ๋Ÿผ๊ณผ ํ˜ธํ™˜๋จ
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    // username ์ปฌ๋Ÿผ ์„ค์ •:
    // - null ๋ถˆํ—ˆ (NOT NULL)
    // - ์œ ์ผํ•œ ๊ฐ’์ด์–ด์•ผ ํ•จ (UNIQUE)
    // - ์ตœ๋Œ€ ๊ธธ์ด 50์ž (VARCHAR(50))
    // PostgreSQL์—์„œ๋Š” VARCHAR(n)์œผ๋กœ ์ž๋™ ๋งคํ•‘๋จ
    @Column(nullable = false, unique = true, length = 50)
    private String username;
}
coldesc
idGenerationType.IDENTITY๋ฅผ ์‚ฌ์šฉํ•จ์œผ๋กœ์จ AUTO_INCREMENT ์™€ ๋™์ผํ•œ ํšจ๊ณผ
username๋„๊ฐ’ ์ œ์™ธ / ์œ ๋‹ˆํฌ๋ฅผ ๋‹ฌ์•„์ค˜์•ผ atomicํ•ด์ง / ํ”„๋ก ํŠธ๋‹จ๊ณผ ๋ณ„๊ฐœ๋กœ db์—์„œ๋„ ๊ธธ์ด ์žก์•„์ค˜์•ผํ•จ.

โœ… ๋ช‡๊ฐœ์˜ ์–ด๋…ธํ…Œ์ด์…˜ ๋งŒ์œผ๋กœ JPA๊ฐ€ ํ…Œ์ด๋ธ” ์ƒ์„ฑ์„ ํ•ด์ค€๋‹ค. db์—์„œ ํ™•์ธํ•ด ๋ณด์ž.

๋จผ์ € db ์—ฐ๊ฒฐ ์„ค์ •
application.yml

spring:
  application:
    name: simple_board
  datasource:
    url: jdbc:postgresql://${DB_HOST}:${DB_PORT}/${DB_NAME}
    username: ${DB_USERNAME}
    password: ${DB_PASSWORD}
  jpa:
    hibernate:
      ddl-auto: update
    show-sql: true

โœ… ์‰˜์—์„œ ํ™•์ธํ•ด๋ณด๋ฉด ๋”ฐ๋กœ ํ…Œ์ด๋ธ” ์ƒ์„ฑ ์ฟผ๋ฆฌ๋ฅผ ์ž…๋ ฅํ•˜์ง€ ์•Š์•˜๋Š”๋ฐ db์— ํ…Œ์ด๋ธ”์ด ๋งŒ๋“ค์–ด์ง„ ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

psql -U simple_board
simple_board=> \dt
             ๋ฆด๋ ˆ์ด์…˜ ๋ชฉ๋ก
 ์Šคํ‚ค๋งˆ | ์ด๋ฆ„  |  ํ˜•ํƒœ  |    ์†Œ์œ ์ฃผ
--------+-------+--------+--------------
 public | users | ํ…Œ์ด๋ธ” | simple_board
(1๊ฐœ ํ–‰)

๐Ÿง  model.Post.java

@Entity
@Table(name="posts")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Post {

    // ๊ธฐ๋ณธ ํ‚ค๋กœ ์ง€์ •. ๊ฐ ๊ฒŒ์‹œ๊ธ€์˜ ๊ณ ์œ  ์‹๋ณ„์ž
    @Id
    // PostgreSQL์—์„œ IDENTITY ์ „๋žต์„ ์‚ฌ์šฉํ•˜์—ฌ ์ž๋™ ์ฆ๊ฐ€ (SERIAL/IDENTITY)
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    // ๊ฒŒ์‹œ๊ธ€ ์ œ๋ชฉ ์ปฌ๋Ÿผ ์„ค์ •
    // - null ๋ถˆ๊ฐ€ (NOT NULL)
    // - ์ตœ๋Œ€ 200์ž (VARCHAR(200))
    @Column(nullable = false, length = 200)
    private String title;

    // ๊ฒŒ์‹œ๊ธ€ ๋‚ด์šฉ ์ปฌ๋Ÿผ ์„ค์ •
    // - null ๋ถˆ๊ฐ€ (NOT NULL)
    // - ๊ธธ์ด ์ œํ•œ ์—†์Œ โ†’ PostgreSQL์—์„œ๋Š” TEXT ํƒ€์ž…์œผ๋กœ ๋งคํ•‘๋  ์ˆ˜ ์žˆ์Œ
    @Column(nullable=false)
    private String content;

    // ์ž‘์„ฑ ์‹œ๊ฐ„ ์ปฌ๋Ÿผ
    // - null ๋ถˆ๊ฐ€ (NOT NULL)
    // - Java LocalDateTime์€ PostgreSQL์˜ TIMESTAMP๋กœ ๋งคํ•‘๋จ
    @Column(nullable = false)
    private LocalDateTime createdAt;

    // ์ž‘์„ฑ์ž์™€์˜ ๋‹ค๋Œ€์ผ ๊ด€๊ณ„ (N:1)
    // - ํ•˜๋‚˜์˜ ์œ ์ €(User)๊ฐ€ ์—ฌ๋Ÿฌ ๊ฐœ์˜ ๊ฒŒ์‹œ๊ธ€(Post)์„ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ์Œ
    // - ์ง€์—ฐ ๋กœ๋”ฉ: ์‹ค์ œ ์‚ฌ์šฉํ•  ๋•Œ ๋กœ๋”ฉ๋จ (์„ฑ๋Šฅ ์ตœ์ ํ™”)
    // - optional=false: ๋ฐ˜๋“œ์‹œ author๊ฐ€ ์žˆ์–ด์•ผ ํ•จ (NOT NULL)
    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    
    // ์™ธ๋ž˜ ํ‚ค ์„ค์ •
    // - posts ํ…Œ์ด๋ธ”์˜ author_id ์ปฌ๋Ÿผ์ด users ํ…Œ์ด๋ธ”์˜ id ์ปฌ๋Ÿผ์„ ์ฐธ์กฐ
    @JoinColumn(name="author_id")
    private User author; // user_id

    // ๋Œ“๊ธ€๊ณผ์˜ ์ผ๋Œ€๋‹ค ๊ด€๊ณ„ (1:N)
    // - ํ•˜๋‚˜์˜ ๊ฒŒ์‹œ๊ธ€(Post)์ด ์—ฌ๋Ÿฌ ๊ฐœ์˜ ๋Œ“๊ธ€(Comment)์„ ๊ฐ€์งˆ ์ˆ˜ ์žˆ์Œ
    // - mappedBy="post": Comment ์—”ํ‹ฐํ‹ฐ์˜ post ํ•„๋“œ๊ฐ€ ์™ธ๋ž˜ ํ‚ค ์ฃผ์ธ
    // - cascade = ALL: ๊ฒŒ์‹œ๊ธ€์„ ์ €์žฅ/์‚ญ์ œํ•  ๋•Œ ๋Œ“๊ธ€๋„ ํ•จ๊ป˜ ์ €์žฅ/์‚ญ์ œ๋จ
    // - orphanRemoval = true: ๋Œ“๊ธ€์ด ๊ด€๊ณ„์—์„œ ์ œ๊ฑฐ๋˜๋ฉด DB์—์„œ๋„ ์‚ญ์ œ๋จ
    @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true)
    
    // ๋Œ“๊ธ€ ์ •๋ ฌ ๊ธฐ์ค€ ์ง€์ •
    // - createdAt ์˜ค๋ฆ„์ฐจ์ˆœ์œผ๋กœ ์ •๋ ฌ๋œ ์ƒํƒœ๋กœ comments ๋ฆฌ์ŠคํŠธ์— ๋‹ด๊น€
    @OrderBy("createdAt ASC")
    private List<Comment> comments;
}
coldesc
idstrategy = GenerationType.IDENTITY ๋กœ atomic ๊ฐ’ ์ƒ์„ฑ
title์šฐ์„  length๋กœ ๋ง‰์•„์ฃผ๊ธด ํ•˜๋Š”๋ฐ valid ์—๋Ÿฌ์ฒ˜๋ฆฌ๋Š” ๋ฆฌํฌ์ง€ํ† ๋ฆฌ์—์„œ
author- post ์กฐํšŒ -> user data ์ž๋™์œผ๋กœ ์กฐํšŒ
- lazy๋Š” ์ดˆ๋ฐ˜์—๋Š” user๋Š” null ๊ฐ’์„ ์คฌ๋‹ค๊ฐ€ ํ•„์š”ํ•˜๋ฉด ์กฐํšŒํ•ด์„œ ์ค€๋‹ค. ์ผ๋ฐ˜์ ์œผ๋กœ lazy๋ฅผ ๊ธฐ๋ณธ์œผ๋กœ ์“ด๋‹ค.
- optional false : ํ•„์ˆ˜
- @JoinColumn ๋งŒ ์“ฐ๋ฉด user_id ์ปฌ๋Ÿผ์ด ์ƒ์„ฑ๋˜๋Š”๋ฐ post ๊ด€์ ์—์„œ user๋Š” author ์ด๋ฏ€๋กœ author_id ๋กœ ์ƒ์„ฑ

๐Ÿง  model.Comment.java

@Entity // ์ด ํด๋ž˜์Šค๊ฐ€ JPA ์—”ํ‹ฐํ‹ฐ์ž„์„ ๋ช…์‹œ. DB ํ…Œ์ด๋ธ”๊ณผ ๋งคํ•‘๋จ
@Table(name = "comments") // ๋งคํ•‘๋  ํ…Œ์ด๋ธ” ์ด๋ฆ„์„ "comments"๋กœ ์ง€์ •
@Data // Lombok: getter, setter, toString, equals, hashCode, requiredArgsConstructor ์ž๋™ ์ƒ์„ฑ
@NoArgsConstructor // Lombok: ๊ธฐ๋ณธ ์ƒ์„ฑ์ž ์ƒ์„ฑ
@AllArgsConstructor // Lombok: ๋ชจ๋“  ํ•„๋“œ๋ฅผ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๊ฐ–๋Š” ์ƒ์„ฑ์ž ์ƒ์„ฑ
@Builder // Lombok: ๋นŒ๋” ํŒจํ„ด ์‚ฌ์šฉ ๊ฐ€๋Šฅ
public class Comment {

    @Id // ๊ธฐ๋ณธ ํ‚ค(primary key) ํ•„๋“œ๋กœ ์ง€์ •
    @GeneratedValue(strategy = GenerationType.IDENTITY) 
    // PostgreSQL์—์„œ IDENTITY ์ „๋žต ์‚ฌ์šฉ โ†’ SERIAL/IDENTITY๋กœ ์ž๋™ ์ฆ๊ฐ€
    private Integer id;

    @Column(nullable = false)
    // ๋Œ“๊ธ€ ๋‚ด์šฉ์€ null์„ ํ—ˆ์šฉํ•˜์ง€ ์•Š์Œ โ†’ NOT NULL
    private String text;

    @Column(nullable = false)
    // ๋Œ“๊ธ€ ์ž‘์„ฑ ์‹œ๊ฐ, null ๋ถˆ๊ฐ€ โ†’ NOT NULL
    // LocalDateTime์€ PostgreSQL์—์„œ TIMESTAMP๋กœ ๋งคํ•‘๋จ
    private LocalDateTime createdAt;

    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    // ๋Œ“๊ธ€๊ณผ ์ž‘์„ฑ์ž(User) ๊ฐ„์˜ ๋‹ค๋Œ€์ผ(N:1) ๊ด€๊ณ„
    // ์ง€์—ฐ ๋กœ๋”ฉ ์„ค์ • (ํ•„์š”ํ•  ๋•Œ๊นŒ์ง€ author ๋กœ๋”ฉ ์ง€์—ฐ)
    // optional = false โ†’ ์ž‘์„ฑ์ž๋Š” ๋ฐ˜๋“œ์‹œ ์กด์žฌํ•ด์•ผ ํ•จ
    @JoinColumn(name = "author_id")
    // ์™ธ๋ž˜ ํ‚ค ์ด๋ฆ„์„ author_id๋กœ ์ง€์ • (users ํ…Œ์ด๋ธ”์˜ id ์ฐธ์กฐ)
    private User author;

    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    // ๋Œ“๊ธ€๊ณผ ๊ฒŒ์‹œ๊ธ€(Post) ๊ฐ„์˜ ๋‹ค๋Œ€์ผ(N:1) ๊ด€๊ณ„
    // ํ•˜๋‚˜์˜ ๊ฒŒ์‹œ๊ธ€์— ์—ฌ๋Ÿฌ ๋Œ“๊ธ€์ด ๋‹ฌ๋ฆด ์ˆ˜ ์žˆ์Œ
    @JoinColumn(name = "post_id")
    // ์™ธ๋ž˜ ํ‚ค ์ด๋ฆ„์„ post_id๋กœ ์ง€์ • (posts ํ…Œ์ด๋ธ”์˜ id ์ฐธ์กฐ)
    private Post post;
}

Repository๋Š” ์ธํ„ฐํŽ˜์ด์Šค๋กœ ๋งŒ๋“ค๊ณ  JpaRepository๋ฅผ ์ƒ์†๋งŒ ํ•ด์ฃผ๋ฉด JPA๊ฐ€ ์ž๋™์œผ๋กœ CRUD๋ฅผ ๊ตฌํ˜„ํ•ด์ค€๋‹ค. ์™€ ๋งˆ๋ฒ•! ๋งŒ์•ฝ class๋กœ ์“ฐ๋ฉด ์‹คํ–‰์€ ๋˜์ง€๋งŒ jpa ๊ธฐ๋Šฅ ๋ชป์“ด๋‹ค.
๐Ÿ—„๏ธ UserRepository

// User ์—”ํ‹ฐํ‹ฐ๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” JPA ๋ฆฌํฌ์ง€ํ† ๋ฆฌ ์ธํ„ฐํŽ˜์ด์Šค
// - JpaRepository<์—”ํ‹ฐํ‹ฐ ํƒ€์ž…, ๊ธฐ๋ณธ ํ‚ค ํƒ€์ž…>์„ ์ƒ์†๋ฐ›์Œ
// - ๊ธฐ๋ณธ์ ์ธ CRUD ๋ฉ”์„œ๋“œ๋“ค์ด ์ž๋™์œผ๋กœ ๊ตฌํ˜„๋จ
public interface UserRepository extends JpaRepository<User, Integer> {

    // username ์ปฌ๋Ÿผ์œผ๋กœ ์œ ์ €๋ฅผ ์กฐํšŒํ•˜๋Š” ์ปค์Šคํ…€ ๋ฉ”์„œ๋“œ
    // - Optional๋กœ ๊ฐ์‹ธ null-safeํ•˜๊ฒŒ ์ฒ˜๋ฆฌ (์กฐํšŒ ๊ฒฐ๊ณผ ์—†์„ ๊ฒฝ์šฐ : null)
    // - ๋ฉ”์„œ๋“œ ์ด๋ฆ„ ๊ธฐ๋ฐ˜ ์ฟผ๋ฆฌ ์ƒ์„ฑ (Spring Data JPA๊ฐ€ ์ž๋™ ๊ตฌํ˜„)
    Optional<User> findByUsername(String username);
}

๐Ÿ—„๏ธ PostRepository

// Post ์—”ํ‹ฐํ‹ฐ๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” JPA ๋ฆฌํฌ์ง€ํ† ๋ฆฌ ์ธํ„ฐํŽ˜์ด์Šค
// - ๊ธฐ๋ณธ CRUD ์™ธ์— ํ•„์š”์‹œ ์ปค์Šคํ…€ ์ฟผ๋ฆฌ ์ถ”๊ฐ€ ๊ฐ€๋Šฅ
public interface PostRepository extends JpaRepository<Post, Integer> {
}

๐Ÿ—„๏ธ CommentRepository

// Comment ์—”ํ‹ฐํ‹ฐ๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” JPA ๋ฆฌํฌ์ง€ํ† ๋ฆฌ ์ธํ„ฐํŽ˜์ด์Šค
// - ๋ชจ๋“  ๋Œ“๊ธ€ ๊ด€๋ จ DB ์ž‘์—…์„ ์ถ”์ƒํ™”
public interface CommentRepository extends JpaRepository<Comment, Integer> {
}

โœ… JPA๊ฐ€ ๋‚ด๋ถ€์ ์œผ๋กœ ๋™์ž‘ํ•˜๋Š” ๊ณผ์ •

  1. ์ฟผ๋ฆฌ ์ƒ์„ฑ
  • JPA๋Š” ๋ฉ”์„œ๋“œ ์ด๋ฆ„์„ ๊ธฐ๋ฐ˜์œผ๋กœ ์ฟผ๋ฆฌ๋ฅผ ์ƒ์„ฑํ•œ๋‹ค. (Query Derivation)
Optional<User> findByUsername(String username);
SELECT u FROM User u WHERE u.username = :username
  1. Spring์ด ๋‚ด๋ถ€์ ์œผ๋กœ ํ”„๋ก์‹œ ํด๋ž˜์Šค ์ƒ์„ฑ
  • Spring ๋ถ€ํŒ… ์‹œ์ ์— Spring์€ UserRepository๋ฅผ ๊ตฌํ˜„ํ•œ ํ”„๋ก์‹œ ๊ฐ์ฒด๋ฅผ ์ž๋™์œผ๋กœ ์ƒ์„ฑํ•œ๋‹ค.
  • ๊ทธ ํ”„๋ก์‹œ ๊ฐ์ฒด๋Š” SimpleJpaRepository๋ผ๋Š” ๋‚ด๋ถ€ ๊ตฌํ˜„์ฒด๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๋งŒ๋“ค์–ด์ง„๋‹ค.
  • ๊ทธ๋ฆฌ๊ณ  EntityManager๋ฅผ ํ†ตํ•ด DB์— ์ ‘๊ทผํ•œ๋‹ค.
UserRepository โ†’ (ํ”„๋ก์‹œ) โ†’ SimpleJpaRepository โ†’ EntityManager (JPA)
  1. ๋งŒ์•ฝ ๋ฉ”์„œ๋“œ ์ด๋ฆ„๋งŒ์œผ๋กœ ๋ณต์žกํ•œ ์กฐ๊ฑด์„ ์ฒ˜๋ฆฌํ•˜๊ธฐ ์–ด๋ ต๋‹ค๋ฉด
    ์ด๋ ‡๊ฒŒ @Query ์–ด๋…ธํ…Œ์ด์…˜์œผ๋กœ JPQL ์ง์ ‘ ์ž‘์„ฑ๋„ ๊ฐ€๋Šฅํ•˜๋‹ค.
@Query("SELECT p FROM Post p WHERE p.title LIKE %:keyword%")
List<Post> searchByTitle(@Param("keyword") String keyword);

@Getter
@Setter
public class SignupDto {
    @NotBlank
    @Size(min=3, max=50)
    private String username;

    @NotBlank
    @Size(min=6, max=1000)
    private String password;
}
@Getter
@Setter
public class LoginDto {
    @NotBlank(message="์•„์ด๋””๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”")
    private String username;

    @NotBlank(message="๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”")
    private String password;
}
@Getter
@Setter
public class PostDto {
    private Integer id;

    @NotBlank(message = "์ œ๋ชฉ์„ ์ž…๋ ฅํ•˜์„ธ์š”")
    private String title;

    @NotBlank(message = "๋‚ด์šฉ์„ ์ž…๋ ฅํ•˜์„ธ์š”")
    private String content;
}
// ์ด ํด๋ž˜์Šค๊ฐ€ Spring MVC์˜ ์ปจํŠธ๋กค๋Ÿฌ์ž„์„ ๋‚˜ํƒ€๋ƒ„ (์š”์ฒญ-์‘๋‹ต ์ฒ˜๋ฆฌ ๋‹ด๋‹น)
@Controller

// final์ด ๋ถ™์€ ํ•„๋“œ(userRepository)์— ๋Œ€ํ•ด ์ƒ์„ฑ์ž๋ฅผ ์ž๋™์œผ๋กœ ์ƒ์„ฑ (Lombok)
@RequiredArgsConstructor
public class SignupController {

    // DI(์˜์กด์„ฑ ์ฃผ์ž…) ๋Œ€์ƒ: UserRepository ์ฃผ์ž…๋จ
    private final UserRepository userRepository;

    // ํšŒ์›๊ฐ€์ž… ํผ ํŽ˜์ด์ง€๋ฅผ ๋ณด์—ฌ์ฃผ๋Š” GET ์š”์ฒญ ์ฒ˜๋ฆฌ
    @GetMapping("/signup") // GET ๋ฐฉ์‹ /signup ์š”์ฒญ์ด ๋“ค์–ด์˜ค๋ฉด ์ด ๋ฉ”์„œ๋“œ ์‹คํ–‰
    public String signupForm(Model model) {
        // signup.html์—์„œ ์‚ฌ์šฉํ•  signupDto ๊ฐ์ฒด๋ฅผ ๋ชจ๋ธ์— ๋“ฑ๋ก
        model.addAttribute("signupDto", new SignupDto());
        // ๋ทฐ ์ด๋ฆ„ ๋ฐ˜ํ™˜ โ†’ templates/signup.html ๋ Œ๋”๋ง
        return "signup";
    }

    // ํšŒ์›๊ฐ€์ž… ํผ ์ œ์ถœ ์ฒ˜๋ฆฌ (POST ์š”์ฒญ)
    @PostMapping("/signup") // POST ๋ฐฉ์‹ /signup ์š”์ฒญ ์‹œ ์ด ๋ฉ”์„œ๋“œ ์‹คํ–‰
    public String signup(
            // ํด๋ผ์ด์–ธํŠธ์—์„œ ๋„˜์–ด์˜จ form ๋ฐ์ดํ„ฐ๋ฅผ SignupDto์— ๋ฐ”์ธ๋”ฉ
            // - name="username" โ†’ signupDto.setUsername(...)
            // - name="password" โ†’ signupDto.setPassword(...)
            // @Valid: DTO์— ์„ค์ •๋œ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ์–ด๋…ธํ…Œ์ด์…˜(@NotBlank ๋“ฑ)์„ ์ˆ˜ํ–‰ํ•จ
            @Valid @ModelAttribute SignupDto signupDto,

            // ๋ฐ”์ธ๋”ฉ/๊ฒ€์ฆ ๊ฒฐ๊ณผ๊ฐ€ ์ €์žฅ๋จ. ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ์— ์‹คํŒจํ•˜๋ฉด ์˜ค๋ฅ˜ ์ •๋ณด๊ฐ€ ์—ฌ๊ธฐ์— ๋“ค์–ด๊ฐ
            BindingResult bindingResult,

            // ๋ชจ๋ธ์— ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋ฅผ ๋‹ด๊ธฐ ์œ„ํ•ด ์ „๋‹ฌ๋ฐ›์Œ
            Model model
    ) {
        // ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ์‹คํŒจ ์‹œ ๋‹ค์‹œ signup.html ํŽ˜์ด์ง€๋กœ ์ด๋™ (์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€ ์ถœ๋ ฅ์šฉ)
        if(bindingResult.hasErrors()) return "signup";

        // username ์ค‘๋ณต ๊ฒ€์‚ฌ
        if(userRepository.findByUsername(signupDto.getUsername()).isPresent()) {
            // ๋ชจ๋ธ์— ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ์ถ”๊ฐ€
            model.addAttribute("error", "์ด๋ฏธ ์‚ฌ์šฉ์ค‘์ธ ์•„์ด๋””์ž…๋‹ˆ๋‹ค");
            // ๋‹ค์‹œ ํšŒ์›๊ฐ€์ž… ํผ ๋ณด์—ฌ์คŒ
            return "signup";
        }

        // ํšŒ์› ์ •๋ณด ์ €์žฅ (๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” ์‹ค์ œ ์„œ๋น„์Šค์—์„  ๋ฐ˜๋“œ์‹œ ์•”ํ˜ธํ™” ํ•„์š”!)
        userRepository.save(
                User.builder()
                        .username(signupDto.getUsername())
                        .password(signupDto.getPassword()) // ์•”ํ˜ธํ™” ์ƒ๋žต๋จ
                        .build()
        );

        // ํšŒ์›๊ฐ€์ž… ์„ฑ๊ณต ์‹œ ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ (URL ๋’ค์— ?registered ์ฟผ๋ฆฌ ํŒŒ๋ผ๋ฏธํ„ฐ ์ถ”๊ฐ€)
        return "redirect:/login?registered";
    }
}
์–ด๋…ธํ…Œ์ด์…˜ / ํด๋ž˜์Šค์„ค๋ช…
@ControllerSpring MVC์˜ ์ปจํŠธ๋กค๋Ÿฌ ์ง€์ •. ๋ฐ˜ํ™˜๊ฐ’์€ ๋ทฐ ์ด๋ฆ„์œผ๋กœ ํ•ด์„๋จ
@GetMapping, @PostMappingHTTP GET/POST ์š”์ฒญ์„ ํ•ด๋‹น ๋ฉ”์„œ๋“œ์™€ ๋งคํ•‘
@ModelAttributeํผ ๋ฐ์ดํ„ฐ โ†’ DTO ๊ฐ์ฒด๋กœ ๋ฐ”์ธ๋”ฉ
@ValidDTO์— ์„ค์ •๋œ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ(@NotNull, @Size ๋“ฑ)๋ฅผ ์‹คํ–‰
BindingResult์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ๊ฒฐ๊ณผ๋ฅผ ๋‹ด๋Š” ๊ฐ์ฒด. ์˜ค๋ฅ˜ ์—ฌ๋ถ€ ๋ฐ ๋ฉ”์‹œ์ง€ ํ™•์ธ ๊ฐ€๋Šฅ
model.addAttribute()๋ทฐ์— ์ „๋‹ฌํ•  ๋ฐ์ดํ„ฐ๋ฅผ ๋ชจ๋ธ์— ๋‹ด์Œ (ํ…œํ”Œ๋ฆฟ์—์„œ ์‚ฌ์šฉ ๊ฐ€๋Šฅ)
redirect:๋ทฐ๊ฐ€ ์•„๋‹ˆ๋ผ URL๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธํ•˜๋ผ๋Š” ์˜๋ฏธ (HTTP 302)

๐Ÿ“ก LoginController.java

// Spring MVC ์ปจํŠธ๋กค๋Ÿฌ๋กœ ์ง€์ •. HTTP ์š”์ฒญ์„ ์ฒ˜๋ฆฌํ•˜๊ณ  ๋ทฐ๋ฅผ ๋ฐ˜ํ™˜ํ•จ
@Controller

// final ํ•„๋“œ(userRepository)์— ๋Œ€ํ•ด ์ƒ์„ฑ์ž ์ž๋™ ์ƒ์„ฑ (์˜์กด์„ฑ ์ฃผ์ž…)
@RequiredArgsConstructor
public class LoginController {

    // User ์—”ํ‹ฐํ‹ฐ์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋Š” ๋ฆฌํฌ์ง€ํ† ๋ฆฌ (DB์™€ ์—ฐ๊ฒฐ๋จ)
    private final UserRepository userRepository;

    // ๋ฃจํŠธ(/) ๋˜๋Š” /login ๊ฒฝ๋กœ๋กœ GET ์š”์ฒญ์ด ๋“ค์–ด์˜ค๋ฉด ๋กœ๊ทธ์ธ ํผ์„ ๋ณด์—ฌ์คŒ
    @GetMapping({"/", "login"}) // "/" ๋˜๋Š” "/login" ๋ชจ๋‘ ์ด ๋ฉ”์„œ๋“œ๋กœ ์ฒ˜๋ฆฌ
    public String loginForm(Model model) {
        // login.html ํ…œํ”Œ๋ฆฟ์—์„œ ์‚ฌ์šฉํ•  loginDto ๊ฐ์ฒด๋ฅผ ๋ชจ๋ธ์— ๋‹ด์•„ ์ „๋‹ฌ
        model.addAttribute("loginDto", new LoginDto());
        // ๋ทฐ ์ด๋ฆ„ ๋ฐ˜ํ™˜ โ†’ templates/login.html ๋ Œ๋”๋ง๋จ
        return "login";
    }

    // ๋กœ๊ทธ์ธ ํผ์—์„œ POST ์š”์ฒญ์ด ๋“ค์–ด์˜ฌ ๋•Œ ์‹คํ–‰
    @PostMapping("/login")
    public String login(
            // ํด๋ผ์ด์–ธํŠธ form ๋ฐ์ดํ„ฐ ๋ฐ”์ธ๋”ฉ โ†’ LoginDto ๊ฐ์ฒด์— ๋งคํ•‘
            // @Valid: LoginDto์— ์ •์˜๋œ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ(@NotBlank ๋“ฑ) ์‹คํ–‰
            @Valid @ModelAttribute LoginDto loginDto,

            // ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ๊ฒฐ๊ณผ ์ €์žฅ (์˜ค๋ฅ˜ ๋ฐœ์ƒ ์—ฌ๋ถ€ ํ™•์ธ ๊ฐ€๋Šฅ)
            BindingResult bindingResult,

            // ์„ธ์…˜ ๊ฐ์ฒด: ๋กœ๊ทธ์ธ ์ƒํƒœ๋ฅผ ์ €์žฅํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ
            HttpSession httpSession,

            // ๋ชจ๋ธ: ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋ฅผ ๋ทฐ๋กœ ์ „๋‹ฌํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ
            Model model
    ) {
        // ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ์‹คํŒจ ์‹œ ๋‹ค์‹œ ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€๋กœ ์ด๋™
        if (bindingResult.hasErrors()) return "login";

        // username์œผ๋กœ ์‚ฌ์šฉ์ž ์กฐํšŒ (Optional โ†’ ์—†์œผ๋ฉด null ๋ฐ˜ํ™˜)
        User user = userRepository.findByUsername(loginDto.getUsername()).orElse(null);

        // ์•„์ด๋””๊ฐ€ ์—†๊ฑฐ๋‚˜ ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์ผ์น˜ํ•˜์ง€ ์•Š์„ ๊ฒฝ์šฐ
        if (user == null || !user.getPassword().equals(loginDto.getPassword())) {
            // ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋ฅผ ๋ชจ๋ธ์— ๋‹ด๊ณ  ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€๋กœ ๋Œ์•„๊ฐ
            model.addAttribute("error", "์•„์ด๋””/๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค");
            return "login";
        }

        // ๋กœ๊ทธ์ธ ์„ฑ๊ณต ์‹œ, ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ์„ธ์…˜์— ์ €์žฅ
        // ์ดํ›„ ์š”์ฒญ์—์„œ๋„ ๋กœ๊ทธ์ธ ์ƒํƒœ๋ฅผ ์œ ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉ
        httpSession.setAttribute("user", user);

        // ๊ฒŒ์‹œ๊ธ€ ๋ชฉ๋ก ํŽ˜์ด์ง€๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ
        return "redirect:/posts";
    }
}
๊ฐœ๋…์„ค๋ช…
@GetMapping({"/", "login"})์—ฌ๋Ÿฌ URL์„ ํ•˜๋‚˜์˜ ๋ฉ”์„œ๋“œ๋กœ ์ฒ˜๋ฆฌํ•จ
@ModelAttributeํด๋ผ์ด์–ธํŠธ์˜ form ํ•„๋“œ๋ฅผ DTO์— ๋ฐ”์ธ๋”ฉํ•จ
@ValidDTO์— ์„ค์ •๋œ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ๋ฅผ ์ž๋™ ์ˆ˜ํ–‰ํ•จ
BindingResult์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ๊ฒฐ๊ณผ๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋Š” ๊ฐ์ฒด (์—๋Ÿฌ ์žˆ์œผ๋ฉด ์ฒ˜๋ฆฌ)
HttpSession์„œ๋ฒ„๊ฐ€ ํด๋ผ์ด์–ธํŠธ์˜ ๋กœ๊ทธ์ธ ์ƒํƒœ๋ฅผ ์œ ์ง€ํ•˜๊ธฐ ์œ„ํ•œ ์ €์žฅ์†Œ
model.addAttribute()๋ทฐ์— ๋ฐ์ดํ„ฐ๋ฅผ ์ „๋‹ฌํ•จ (<span th:text="${error}">) ๋“ฑ์œผ๋กœ ์ถœ๋ ฅ ๊ฐ€๋Šฅ
redirect:/posts๋กœ๊ทธ์ธ ์„ฑ๊ณต ํ›„ ๊ฒŒ์‹œ๊ธ€ ๋ชฉ๋ก ํŽ˜์ด์ง€๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ (HTTP 302)

๐Ÿ“ก PostController.java

// ์ด ํด๋ž˜์Šค๊ฐ€ Spring MVC ์ปจํŠธ๋กค๋Ÿฌ์ž„์„ ๋‚˜ํƒ€๋ƒ„
@Controller

// ์š”์ฒญ ๊ฒฝ๋กœ "/posts"๋กœ ์‹œ์ž‘ํ•˜๋Š” ๋ชจ๋“  ์š”์ฒญ์„ ์ด ์ปจํŠธ๋กค๋Ÿฌ์—์„œ ์ฒ˜๋ฆฌ
@RequestMapping("/posts")

// final ํ•„๋“œ์— ๋Œ€ํ•ด ์ƒ์„ฑ์ž๋ฅผ ์ž๋™์œผ๋กœ ์ƒ์„ฑํ•˜์—ฌ ์˜์กด์„ฑ ์ฃผ์ž… (Lombok)
@RequiredArgsConstructor
public class PostController {

    // ๊ฒŒ์‹œ๊ธ€ ๋ฐ์ดํ„ฐ๋ฅผ DB์—์„œ ์กฐํšŒ/์ €์žฅํ•˜๋Š” ๋ฐ ์‚ฌ์šฉํ•˜๋Š” JPA ๋ฆฌํฌ์ง€ํ† ๋ฆฌ
    private final PostRepository postRepository;

    // ๋Œ“๊ธ€ ๊ด€๋ จ DB ์ž‘์—…์„ ์œ„ํ•œ ๋ฆฌํฌ์ง€ํ† ๋ฆฌ (ํ–ฅํ›„ ๋Œ“๊ธ€ ์ถ”๊ฐ€ ์‹œ ์‚ฌ์šฉ)
    private final CommentRepository commentRepository;

    // ํ˜„์žฌ ๋กœ๊ทธ์ธ๋œ ์‚ฌ์šฉ์ž๋ฅผ ์„ธ์…˜์—์„œ ๊บผ๋‚ด์˜ค๋Š” ํ—ฌํผ ๋ฉ”์„œ๋“œ
    // ์„ธ์…˜์— ์ €์žฅ๋œ "user" ์†์„ฑ์„ User ๊ฐ์ฒด๋กœ ์บ์ŠคํŒ…ํ•ด์„œ ๋ฐ˜ํ™˜
    private User currentUser(HttpSession httpSession) {
        return (User) httpSession.getAttribute("user");
    }

    // ๊ฒŒ์‹œ๊ธ€ ๋ชฉ๋ก ํŽ˜์ด์ง€ ์š”์ฒญ (GET /posts)
    @GetMapping
    public String list(Model model) {
        // ์ „์ฒด ๊ฒŒ์‹œ๊ธ€ ๋ฆฌ์ŠคํŠธ๋ฅผ ๋ชจ๋ธ์— ๋‹ด์•„์„œ post-list.html๋กœ ์ „๋‹ฌ
        model.addAttribute("posts", postRepository.findAll());
        return "post-list";
    }

    // ๊ฒŒ์‹œ๊ธ€ ์ž‘์„ฑ ํผ ์š”์ฒญ (GET /posts/add)
    @GetMapping("/add")
    public String addForm(Model model, HttpSession httpSession) {
        // ๋กœ๊ทธ์ธํ•˜์ง€ ์•Š์€ ์‚ฌ์šฉ์ž๋Š” ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ
        if (currentUser(httpSession) == null) return "redirect:/login";

        // ๋นˆ PostDto๋ฅผ ๋ชจ๋ธ์— ๋‹ด์•„ ํ…œํ”Œ๋ฆฟ์—์„œ form ๋ฐ”์ธ๋”ฉ์— ์‚ฌ์šฉ
        model.addAttribute("postDto", new PostDto());

        // ๊ฒŒ์‹œ๊ธ€ ์ž‘์„ฑ ํผ ํŽ˜์ด์ง€ ๋ Œ๋”๋ง (post-form.html)
        return "post-form";
    }

    // ๊ฒŒ์‹œ๊ธ€ ์ž‘์„ฑ ํผ ์ œ์ถœ ์ฒ˜๋ฆฌ (POST /posts/add)
    @PostMapping("/add")
    public String add(
            // form ๋ฐ์ดํ„ฐ๊ฐ€ postDto์— ๋ฐ”์ธ๋”ฉ๋˜๊ณ , ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ์ˆ˜ํ–‰
            @Valid @ModelAttribute PostDto postDto,

            // ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ๊ฒฐ๊ณผ ์ €์žฅ. ์˜ค๋ฅ˜ ์žˆ์œผ๋ฉด ์ฒ˜๋ฆฌ ๊ฐ€๋Šฅ
            BindingResult bindingResult,

            // ์„ธ์…˜์„ ํ†ตํ•ด ๋กœ๊ทธ์ธ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ค๊ธฐ ์œ„ํ•จ
            HttpSession httpSession
    ) {
        // ์ž…๋ ฅ๊ฐ’ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ์‹คํŒจ ์‹œ, ๋‹ค์‹œ ์ž‘์„ฑ ํผ ํŽ˜์ด์ง€๋กœ ์ด๋™
        if (bindingResult.hasErrors()) return "post-form";

        // ์„ธ์…˜์—์„œ ๋กœ๊ทธ์ธ๋œ ์‚ฌ์šฉ์ž ์ •๋ณด ๊ฐ€์ ธ์˜ด
        User user = currentUser(httpSession);

        // ๊ฒŒ์‹œ๊ธ€ ์—”ํ‹ฐํ‹ฐ ์ƒ์„ฑ: ์ œ๋ชฉ, ๋‚ด์šฉ, ์ž‘์„ฑ์ž, ์ž‘์„ฑ์‹œ๊ฐ„ ์„ค์ •
        Post post = Post.builder()
                .title(postDto.getTitle())
                .content(postDto.getContent())
                .author(user) // ํ˜„์žฌ ๋กœ๊ทธ์ธ ์‚ฌ์šฉ์ž
                .createdAt(LocalDateTime.now()) // ํ˜„์žฌ ์‹œ๊ฐ
                .build();

        // ๊ฒŒ์‹œ๊ธ€ ์ €์žฅ
        postRepository.save(post);

        // ๊ฒŒ์‹œ๊ธ€ ๋ชฉ๋ก ํŽ˜์ด์ง€๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ
        return "redirect:/posts";
    }
}
์š”์†Œ์„ค๋ช…
@RequestMapping("/posts")์ด ์ปจํŠธ๋กค๋Ÿฌ ๋‚ด ๋ชจ๋“  URL์€ /posts๋กœ ์‹œ์ž‘
@GetMapping, @PostMapping๊ฐ๊ฐ GET/POST ์š”์ฒญ์„ ์ฒ˜๋ฆฌ
@Valid, BindingResultDTO ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ๋ฐ ๊ฒ€์ฆ ๊ฒฐ๊ณผ ์ฒ˜๋ฆฌ
HttpSession๋กœ๊ทธ์ธ ์ •๋ณด๋ฅผ ์„œ๋ฒ„ ์„ธ์…˜์— ์ €์žฅํ•˜๊ณ  ๊ฐ€์ ธ์˜ค๋Š” ๋ฐ ์‚ฌ์šฉ
currentUser()์„ธ์…˜์—์„œ ๋กœ๊ทธ์ธ๋œ ์‚ฌ์šฉ์ž๋ฅผ ์•ˆ์ „ํ•˜๊ฒŒ ๊ฐ€์ ธ์˜ค๋Š” ์œ ํ‹ธ ํ•จ์ˆ˜
model.addAttribute("postDto", new PostDto())form ๋ฐ”์ธ๋”ฉ์„ ์œ„ํ•œ ๊ฐ์ฒด ์ „๋‹ฌ
LocalDateTime.now()๊ฒŒ์‹œ๊ธ€ ์ž‘์„ฑ ์‹œ๊ฐ„์„ ํ˜„์žฌ ์‹œ๊ฐ„์œผ๋กœ ์ง€์ •

๐Ÿ–ผ๏ธ signup.html

<h1>๐Ÿ“œ ํšŒ์›๊ฐ€์ž…</h1>

<!-- ์„œ๋ฒ„์—์„œ ๋ชจ๋ธ๋กœ ์ „๋‹ฌ๋œ 'error' ์†์„ฑ์ด ์žˆ์„ ๋•Œ๋งŒ ์ถœ๋ ฅ๋˜๋Š” ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€ ์˜์—ญ -->
<!-- ์˜ˆ: ์•„์ด๋”” ์ค‘๋ณต ์‹œ "์ด๋ฏธ ์‚ฌ์šฉ์ค‘์ธ ์•„์ด๋””์ž…๋‹ˆ๋‹ค" ๋ฉ”์‹œ์ง€ ํ‘œ์‹œ -->
<div th:if="${error}" class="error" th:text="${error}"></div>

<!-- ํšŒ์›๊ฐ€์ž… ํผ -->
<!-- th:action โ†’ ํผ ์ œ์ถœ ๊ฒฝ๋กœ๋ฅผ ์ง€์ • (/signup) -->
<!-- th:object โ†’ ์ด ํผ์ด ๋ฐ”์ธ๋”ฉ๋  ๊ฐ์ฒด ์ง€์ • (SignupDto) -->
<form th:action="@{/signup}"
      th:object="${signupDto}"
      method="post"
      class="form-container">

    <!-- ์•„์ด๋”” ์ž…๋ ฅ ํ•„๋“œ -->
    <!-- th:field="*{username}" โ†’ signupDto.username์— ๋ฐ”์ธ๋”ฉ๋จ -->
    <!-- ์ž๋™์œผ๋กœ name="username"๊ณผ value ๋ฐ”์ธ๋”ฉ๋จ -->
    <label>
        ์•„์ด๋””:
        <input type="text" th:field="*{username}"/>
    </label>

    <!-- username ํ•„๋“œ์— ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ์˜ค๋ฅ˜๊ฐ€ ์žˆ์„ ๊ฒฝ์šฐ ๋ฉ”์‹œ์ง€๋ฅผ ์ถœ๋ ฅ -->
    <!-- ์˜ˆ: "์•„์ด๋””๋Š” ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค", "์•„์ด๋””๋Š” 4์ž ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค" ๋“ฑ -->
    <div th:if="${#fields.hasErrors('username')}" th:errors="*{username}"></div>

    <!-- ๋น„๋ฐ€๋ฒˆํ˜ธ ์ž…๋ ฅ ํ•„๋“œ -->
    <!-- ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ signupDto.password์™€ ๋ฐ”์ธ๋”ฉ๋จ -->
    <label>
        ๋น„๋ฐ€๋ฒˆํ˜ธ:
        <input type="password" th:field="*{password}"/>
    </label>

    <!-- password ํ•„๋“œ์— ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ์˜ค๋ฅ˜๊ฐ€ ์žˆ์„ ๊ฒฝ์šฐ ๋ฉ”์‹œ์ง€๋ฅผ ์ถœ๋ ฅ -->
    <div th:if="${#fields.hasErrors('password')}" th:errors="*{password}"></div>

    <!-- ํšŒ์›๊ฐ€์ž… ์ œ์ถœ ๋ฒ„ํŠผ -->
    <button type="submit">๊ฐ€์ž…ํ•˜๊ธฐ</button>

    <!-- ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€๋กœ ์ด๋™ํ•˜๋Š” ๋งํฌ -->
    <a th:href="@{/login}" class="cancel">๋กœ๊ทธ์ธ</a>
</form>

๐Ÿ–ผ๏ธ login.html

<body class="container">
<!-- ์ „์ฒด ํŽ˜์ด์ง€์— ์ ์šฉํ•  CSS ํด๋ž˜์Šค "container" (๋ ˆ์ด์•„์›ƒ ์ •๋ ฌ์šฉ) -->

<h1>๐Ÿ”‘ ๋กœ๊ทธ์ธ</h1>
<!-- ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€ ์ œ๋ชฉ (๐Ÿ”‘ ์ด๋ชจ์ง€๋กœ ์‹œ๊ฐ์  ๊ฐ•์กฐ) -->

<!-- ์„œ๋ฒ„์—์„œ ๋ชจ๋ธ๋กœ ์ „๋‹ฌ๋œ error ๋ฉ”์‹œ์ง€๊ฐ€ ์žˆ์„ ๊ฒฝ์šฐ ์ถœ๋ ฅ -->
<!-- ์˜ˆ: ๋กœ๊ทธ์ธ ์‹คํŒจ ์‹œ "์•„์ด๋””/๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค" -->
<div th:if="${error}" class="error" th:text="${error}"></div>

<!-- ๋กœ๊ทธ์ธ ํผ -->
<form th:action="@{/login}"           <!-- ๋กœ๊ทธ์ธ ์ฒ˜๋ฆฌ ๊ฒฝ๋กœ (POST /login) -->
      th:object="${loginDto}"         <!-- ์ด ํผ์ด loginDto ๊ฐ์ฒด์™€ ๋ฐ”์ธ๋”ฉ๋จ -->
      method="post"                   <!-- HTTP POST ๋ฐฉ์‹์œผ๋กœ ์ „์†ก -->
      class="form-container">         <!-- CSS ํด๋ž˜์Šค (์Šคํƒ€์ผ๋ง์šฉ) -->

    <!-- ์•„์ด๋”” ์ž…๋ ฅ ํ•„๋“œ -->
    <label>
        ์•„์ด๋””:
        <!-- loginDto.username๊ณผ ๋ฐ”์ธ๋”ฉ๋จ -->
        <!-- ์ž๋™์œผ๋กœ name="username"๊ณผ value="..." ์†์„ฑ์ด ์ ์šฉ๋จ -->
        <input type="text" th:field="*{username}"/>
    </label>

    <!-- username ํ•„๋“œ์— ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ์˜ค๋ฅ˜๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ ๋ฉ”์‹œ์ง€ ์ถœ๋ ฅ -->
    <div th:if="${#fields.hasErrors('username')}" th:errors="*{username}"></div>

    <!-- ๋น„๋ฐ€๋ฒˆํ˜ธ ์ž…๋ ฅ ํ•„๋“œ -->
    <label>
        ๋น„๋ฐ€๋ฒˆํ˜ธ:
        <!-- loginDto.password์™€ ๋ฐ”์ธ๋”ฉ๋จ -->
        <input type="password" th:field="*{password}"/>
    </label>

    <!-- password ํ•„๋“œ์— ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ์˜ค๋ฅ˜๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ ๋ฉ”์‹œ์ง€ ์ถœ๋ ฅ -->
    <div th:if="${#fields.hasErrors('password')}" th:errors="*{password}"></div>

    <!-- ๋กœ๊ทธ์ธ ๋ฒ„ํŠผ -->
    <button type="submit">๋กœ๊ทธ์ธ</button>

    <!-- ํšŒ์›๊ฐ€์ž… ํŽ˜์ด์ง€๋กœ ์ด๋™ํ•˜๋Š” ๋งํฌ -->
    <a th:href="@{/signup}" class="cancel">ํšŒ์›๊ฐ€์ž…</a>
</form>
</body>

๐Ÿ–ผ๏ธ post-list.html

<body>
<h1>๐Ÿ“‹ ๊ฒŒ์‹œํŒ</h1>
<p>
    <a th:href="@{/posts/add}">
        <button>โž• ์ƒˆ ๊ธ€</button>
    </a>
    <a th:href="@{/logout}">
        <button class="cancel">๋กœ๊ทธ์•„์›ƒ</button>
    </a>
</p>

<ul class="post-list">
    <li th:each="post: ${posts}">
        <div class="post">
            <h2>
                <a th:href="@{'/posts/' + ${post.id}}" th:text="${post.title}"></a>
            </h2>
            <p>
                ์ž‘์„ฑ์ž:
                <span th:text="${post.author.username}"></span>
                |
                <span th:text="${#temporals.format(post.createdAt, 'yyyy-MM-dd HH:mm')}"></span>
            </p>
        </div>
    </li>
</ul>
</body>

๐Ÿ–ผ๏ธ post-form.html

<h1>์ƒˆ ๊ฒŒ์‹œ๊ธ€</h1>
<div th:if="${error}" class="error" th:text="${error}"></div>
<form th:action="@{/posts/add}"
      th:object="${postDto}"
      method="post"
      class="form-container">
    <p>
        <label>
            ์ œ๋ชฉ:
            <input type="text" th:field="*{title}" />
        </label>
    </p>
    <p>
        <label>
            ๋‚ด์šฉ:
            <textarea rows="6" th:field="*{content}"/></textarea>
        </label>
    </p>
    <button type="submit">์ €์žฅ</button>
    <a th:href="@{/posts}" class="cancel">์ทจ์†Œ</a>
</form>

๐ŸŒฟ Thymeleaf ๋ฌธ๋ฒ• ์ •๋ฆฌ

๐Ÿ“ฆ 1. ๋ณ€์ˆ˜ ํ‘œํ˜„

๋ฌธ๋ฒ•์„ค๋ช…์˜ˆ์‹œ
${...}Model์— ๋‹ด๊ธด ๊ฐ’์„ ์ฐธ์กฐ${error}, ${user.username}
*{...}th:object ๊ธฐ์ค€ ๊ฐ์ฒด์˜ ํ•„๋“œ ์ฐธ์กฐ*{username}, *{password}
@{...}URL ๋งํฌ ํ‘œํ˜„ (์ปจํ…์ŠคํŠธ ๊ฒฝ๋กœ ์ž๋™ ํฌํ•จ)@{/login}, @{/posts/{id}(id=1)}

๐Ÿงฉ 2. Form ์ฒ˜๋ฆฌ (๋ฐ”์ธ๋”ฉ & ์œ ํšจ์„ฑ ๊ฒ€์ฆ)

๋ฌธ๋ฒ•์„ค๋ช…์˜ˆ์‹œ
th:object<form>๊ณผ ๋ฐ”์ธ๋”ฉํ•  ๊ฐ์ฒด ์„ค์ •<form th:object="${loginDto}">
th:fieldname, id, value ์ž๋™ ์„ค์ •<input th:field="*{username}"/>
th:errorsํ•ด๋‹น ํ•„๋“œ์˜ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ๋ฉ”์‹œ์ง€ ์ถœ๋ ฅ<div th:errors="*{username}"/>
#fields.hasErrors('ํ•„๋“œ๋ช…')ํ•„๋“œ์— ์˜ค๋ฅ˜๊ฐ€ ์žˆ๋Š”์ง€ ๊ฒ€์‚ฌth:if="${#fields.hasErrors('password')}"

๐Ÿ” 3. ์กฐ๊ฑด๋ฌธ & ๋ฐ˜๋ณต๋ฌธ

๋ฌธ๋ฒ•์„ค๋ช…์˜ˆ์‹œ
th:if์กฐ๊ฑด์ด true์ผ ๊ฒฝ์šฐ ํ•ด๋‹น ์š”์†Œ ๋ Œ๋”๋ง<div th:if="${error}">์—๋Ÿฌ</div>
th:each๋ฆฌ์ŠคํŠธ ๋“ฑ์„ ๋ฐ˜๋ณต ์ถœ๋ ฅ<tr th:each="post : ${posts}">...</tr>

โœ… th:each ์˜ˆ์‹œ

<ul>
  <li th:each="user : ${users}" th:text="${user.username}"></li>
</ul>

๐ŸŽจ 4. ๋™์  ์†์„ฑ ์ฒ˜๋ฆฌ (์Šคํƒ€์ผ ๋“ฑ)

๋ฌธ๋ฒ•์„ค๋ช…์˜ˆ์‹œ
th:classappend์กฐ๊ฑด์— ๋”ฐ๋ผ ํด๋ž˜์Šค ์ถ”๊ฐ€th:classappend="${post.pinned} ? 'highlight' : ''"
th:style์ธ๋ผ์ธ ์Šคํƒ€์ผ ๋™์  ์„ค์ •th:style="'color:' + ${color}"
th:attr๊ธฐํƒ€ HTML ์†์„ฑ ๋™์ ์œผ๋กœ ์„ค์ •th:attr="data-id=${user.id}"

โœ… th:classappend ์˜ˆ์‹œ

<div class="post" th:classappend="${post.pinned} ? 'pinned' : ''">
  ๊ฒŒ์‹œ๊ธ€
</div>

๐Ÿงช 5. ๊ธฐํƒ€ ์œ ์šฉํ•œ ํ‘œํ˜„์‹

๋ฌธ๋ฒ•์„ค๋ช…์˜ˆ์‹œ
th:textHTML Escape ํ›„ ํ…์ŠคํŠธ ์ถœ๋ ฅ<span th:text="${username}"></span>
th:utextEscape ์—†์ด HTML ํƒœ๊ทธ๊นŒ์ง€ ๋ Œ๋”๋ง<div th:utext="${content}"></div>
th:value<input> ๋“ฑ์— ๊ฐ’ ๋ช…์‹œ์ ์œผ๋กœ ์„ค์ •<input th:value="${user.email}"/>
th:href๋งํฌ URL ์„ค์ •<a th:href="@{/signup}">ํšŒ์›๊ฐ€์ž…</a>

๐ŸŽฏ ํ•ต์‹ฌ ์ •๋ฆฌ ํฌ์ธํŠธ

  • ${} โ†’ ๋ชจ๋ธ ๋ฐ์ดํ„ฐ ์ ‘๊ทผ
  • *{} โ†’ ํผ ๊ฐ์ฒด(th:object) ๋‚ด๋ถ€ ํ•„๋“œ ์ ‘๊ทผ
  • @{} โ†’ URL ๊ฒฝ๋กœ ํ‘œํ˜„
  • #fields โ†’ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ๋„๊ตฌ
  • th:each โ†’ ๋ฆฌ์ŠคํŠธ ๋ฐ˜๋ณต ์ถœ๋ ฅ
  • th:classappend โ†’ ์กฐ๊ฑด๋ถ€ ์Šคํƒ€์ผ ์ฒ˜๋ฆฌ

โœจ ์‚ฌ์šฉ ํŒ

  • th:field๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด value, name, id๋ฅผ ์ž๋™์œผ๋กœ ์„ค์ •ํ•ด์ฃผ๋ฏ€๋กœ ํผ ์ฒ˜๋ฆฌ๊ฐ€ ๋งค์šฐ ๊ฐ„๊ฒฐํ•ด์ง‘๋‹ˆ๋‹ค.
  • th:errors๋Š” BindingResult์™€ ํ•จ๊ป˜ ์‚ฌ์šฉํ•  ๋•Œ๋งŒ ์ถœ๋ ฅ๋ฉ๋‹ˆ๋‹ค.
  • th:each, th:if๋Š” JSP์˜ c:forEach, c:if๋ฅผ ๋Œ€์ฒดํ•ฉ๋‹ˆ๋‹ค.
profile
๐Ÿ—‚๏ธ hamstern

0๊ฐœ์˜ ๋Œ“๊ธ€