@Entity, @Id, @Column, @OneToMany ๋ฑ| ์ด๋ ธํ ์ด์ | ์ค๋ช |
|---|---|
@Entity | ์ํฐํฐ ํด๋์ค๋ก ํ์ |
@Table(name=) | ํ ์ด๋ธ ๋ช ์ง์ |
@Id | ๊ธฐ๋ณธ ํค ํ๋ ์ง์ |
@GeneratedValue | ํค ์๋ ์์ฑ ์ ๋ต ์ง์ |
@Column | ์ปฌ๋ผ ๋งคํ ์์ฑ(์ด๋ฆ, ๋ ํ์ฉ ๋ฑ) |
@ManyToOne | N:1 ์ฐ๊ด ๋งคํ (์ธ๋ํค) |
@OneToMany | 1:N ์ฐ๊ด ๋งคํ (mappedBy ์ฌ์ฉ) |
@JoinColumn | FK ์ปฌ๋ผ ๋ช ์ง์ |
@Embedded | ๊ฐ ํ์ (๋ด์ฅ ๊ฐ์ฒด) ๋งคํ |
โ ์ค์ต ์์กด์ฑ ๋ชฉ๋ก
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;
}
| col | desc |
|---|---|
| id | GenerationType.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;
}
| col | desc |
|---|---|
| id | strategy = 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> {
}
Optional<User> findByUsername(String username);
SELECT u FROM User u WHERE u.username = :username
UserRepository โ (ํ๋ก์) โ SimpleJpaRepository โ EntityManager (JPA)
@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";
}
}
| ์ด๋ ธํ ์ด์ / ํด๋์ค | ์ค๋ช |
|---|---|
@Controller | Spring MVC์ ์ปจํธ๋กค๋ฌ ์ง์ . ๋ฐํ๊ฐ์ ๋ทฐ ์ด๋ฆ์ผ๋ก ํด์๋จ |
@GetMapping, @PostMapping | HTTP GET/POST ์์ฒญ์ ํด๋น ๋ฉ์๋์ ๋งคํ |
@ModelAttribute | ํผ ๋ฐ์ดํฐ โ DTO ๊ฐ์ฒด๋ก ๋ฐ์ธ๋ฉ |
@Valid | DTO์ ์ค์ ๋ ์ ํจ์ฑ ๊ฒ์ฌ(@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์ ๋ฐ์ธ๋ฉํจ |
@Valid | DTO์ ์ค์ ๋ ์ ํจ์ฑ ๊ฒ์ฌ๋ฅผ ์๋ ์ํํจ |
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, BindingResult | DTO ์ ํจ์ฑ ๊ฒ์ฌ ๋ฐ ๊ฒ์ฆ ๊ฒฐ๊ณผ ์ฒ๋ฆฌ |
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>
| ๋ฌธ๋ฒ | ์ค๋ช | ์์ |
|---|---|---|
${...} | Model์ ๋ด๊ธด ๊ฐ์ ์ฐธ์กฐ | ${error}, ${user.username} |
*{...} | th:object ๊ธฐ์ค ๊ฐ์ฒด์ ํ๋ ์ฐธ์กฐ | *{username}, *{password} |
@{...} | URL ๋งํฌ ํํ (์ปจํ ์คํธ ๊ฒฝ๋ก ์๋ ํฌํจ) | @{/login}, @{/posts/{id}(id=1)} |
| ๋ฌธ๋ฒ | ์ค๋ช | ์์ |
|---|---|---|
th:object | <form>๊ณผ ๋ฐ์ธ๋ฉํ ๊ฐ์ฒด ์ค์ | <form th:object="${loginDto}"> |
th:field | name, id, value ์๋ ์ค์ | <input th:field="*{username}"/> |
th:errors | ํด๋น ํ๋์ ์ ํจ์ฑ ๊ฒ์ฌ ๋ฉ์์ง ์ถ๋ ฅ | <div th:errors="*{username}"/> |
#fields.hasErrors('ํ๋๋ช
') | ํ๋์ ์ค๋ฅ๊ฐ ์๋์ง ๊ฒ์ฌ | th:if="${#fields.hasErrors('password')}" |
| ๋ฌธ๋ฒ | ์ค๋ช | ์์ |
|---|---|---|
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>
| ๋ฌธ๋ฒ | ์ค๋ช | ์์ |
|---|---|---|
th:classappend | ์กฐ๊ฑด์ ๋ฐ๋ผ ํด๋์ค ์ถ๊ฐ | th:classappend="${post.pinned} ? 'highlight' : ''" |
th:style | ์ธ๋ผ์ธ ์คํ์ผ ๋์ ์ค์ | th:style="'color:' + ${color}" |
th:attr | ๊ธฐํ HTML ์์ฑ ๋์ ์ผ๋ก ์ค์ | th:attr="data-id=${user.id}" |
<div class="post" th:classappend="${post.pinned} ? 'pinned' : ''">
๊ฒ์๊ธ
</div>
| ๋ฌธ๋ฒ | ์ค๋ช | ์์ |
|---|---|---|
th:text | HTML Escape ํ ํ ์คํธ ์ถ๋ ฅ | <span th:text="${username}"></span> |
th:utext | Escape ์์ด HTML ํ๊ทธ๊น์ง ๋ ๋๋ง | <div th:utext="${content}"></div> |
th:value | <input> ๋ฑ์ ๊ฐ ๋ช
์์ ์ผ๋ก ์ค์ | <input th:value="${user.email}"/> |
th:href | ๋งํฌ URL ์ค์ | <a th:href="@{/signup}">ํ์๊ฐ์
</a> |