๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
๐Ÿ–ฅ๏ธ Backend

๐Ÿ”ง ๋ฐฑ์—”๋“œ ๊ฐœ๋ฐœ์ž๋ฅผ ์œ„ํ•œ JPA ํ•ต์‹ฌ ์ •๋ฆฌ

by hyebin (Helia) 2025. 5. 30.
๋ฐ˜์‘ํ˜•

์›น ์„œ๋น„์Šค๋ฅผ ๋งŒ๋“ค๋‹ค ๋ณด๋ฉด ์‚ฌ์šฉ์ž์˜ ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•˜๊ฑฐ๋‚˜ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ผ์ด ์ •๋ง ๋งŽ์Šต๋‹ˆ๋‹ค.

ํšŒ์›๊ฐ€์ž… ์‹œ ์ž…๋ ฅํ•œ ์ •๋ณด, ๊ฒŒ์‹œํŒ์˜ ๊ธ€๊ณผ ๋Œ“๊ธ€, ์ฃผ๋ฌธ ๋‚ด์—ญ ๋“ฑ ์ˆ˜๋งŽ์€ ๋ฐ์ดํ„ฐ๊ฐ€ ์˜ค๊ฐ€์ฃ .

 

๊ทธ๋ ‡๋‹ค๋ฉด ์ด๋Ÿฐ ๋ฐ์ดํ„ฐ๋Š” ์–ด๋””์—, ์–ด๋–ป๊ฒŒ ์ €์žฅ๋˜๊ณ  ๊ด€๋ฆฌ๋ ๊นŒ์š”?

 

Spring Boot๋กœ API๋ฅผ ๋งŒ๋“ค์—ˆ๋‹ค๋ฉด , ์ด์ œ ๋‹ค์Œ ๋‹จ๊ณ„๋Š” ๋ฐ”๋กœ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์™€์˜ ์—ฐ๋™์ž…๋‹ˆ๋‹ค.

๋‹จ์ˆœํžˆ save()๋งŒ ํ•œ๋‹ค๊ณ  ๋๋‚˜๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ, JPA์˜ ๋™์ž‘ ์›๋ฆฌ, Entity์™€ Repository ์„ค๊ณ„, ์—ฐ๊ด€๊ด€๊ณ„ ๋งคํ•‘, ํŠธ๋žœ์žญ์…˜ ๊ด€๋ฆฌ, ๊ทธ๋ฆฌ๊ณ  ์„ฑ๋Šฅ ์ตœ์ ํ™” ์ „๋žต๊นŒ์ง€ ํ•จ๊ป˜ ์ดํ•ดํ•ด์•ผ ์•ˆ์ •์ ์ด๊ณ  ํšจ์œจ์ ์ธ ๋ฐฑ์—”๋“œ ์‹œ์Šคํ…œ์„ ๋งŒ๋“ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

์ด๋ฒˆ ๊ธ€์—์„œ๋Š” ์‹ค๋ฌด์—์„œ ์ž์ฃผ ๋งˆ์ฃผ์น˜๋Š” ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ์˜ ํ•ต์‹ฌ ๊ฐœ๋…๋“ค์„ ์ค‘์‹ฌ์œผ๋กœ, JPA์™€ ํ•จ๊ป˜ ๋ฐฑ์—”๋“œ ๊ฐœ๋ฐœ์ž๊ฐ€ ๋ฐ˜๋“œ์‹œ ์•Œ์•„์•ผ ํ•  ๋‚ด์šฉ๋“ค์„ ์ •๋ฆฌํ•ด ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.


๐Ÿ”Œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ์˜ ๊ธฐ๋ณธ๊ธฐ: JDBC์™€ ConnectionPool

JDBC๋ž€?

JDBC(Java Database Connectivity)๋Š” ์ž๋ฐ”์—์„œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ ‘๊ทผํ•˜๊ธฐ ์œ„ํ•œ ํ‘œ์ค€ API์ž…๋‹ˆ๋‹ค.

DB์— ์—ฐ๊ฒฐํ•˜๊ณ , SQL์„ ์‹คํ–‰ํ•˜๋ฉฐ, ๊ฒฐ๊ณผ๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š” ์—ญํ• ์„ ํ•˜์ฃ .

 

ํ•˜์ง€๋งŒ ์ˆœ์ˆ˜ JDBC ๋ฐฉ์‹์€ ์‹ค๋ฌด์—์„œ ์—ฌ๋Ÿฌ ํ•œ๊ณ„๋ฅผ ๋“œ๋Ÿฌ๋ƒ…๋‹ˆ๋‹ค. ์š”์ฒญ์ด ๋“ค์–ด์˜ฌ ๋•Œ๋งˆ๋‹ค ๋งค๋ฒˆ DB ์—ฐ๊ฒฐ์„ ์ƒˆ๋กœ ์ƒ์„ฑํ•ด์•ผ ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์„ฑ๋Šฅ์— ๋ถ€๋‹ด์ด ํฌ๊ณ , try-catch-finally ๊ตฌ์กฐ๊ฐ€ ๋ฐ˜๋ณต๋ผ ์ฝ”๋“œ๊ฐ€ ์ง€์ €๋ถ„ํ•ด์ง€๊ธฐ ์‰ฝ์Šต๋‹ˆ๋‹ค. ๊ฒŒ๋‹ค๊ฐ€ ์ž๋ฐ” ์ฝ”๋“œ ์•ˆ์— SQL์ด ์„ž์ด๋‹ค ๋ณด๋‹ˆ ์œ ์ง€๋ณด์ˆ˜๋„ ์–ด๋ ต์Šต๋‹ˆ๋‹ค.

 

์ด๋Ÿฌํ•œ ์ด์œ ๋กœ ์šฐ๋ฆฌ๋Š” ์ปค๋„ฅ์…˜ ํ’€๊ณผ ๊ฐ™์€ ๊ธฐ์ˆ ์„ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜๋ฉฐ, ๋‚˜์•„๊ฐ€ JPA ๊ฐ™์€ ORM ๋„๊ตฌ๋กœ ๋” ํšจ์œจ์ ์ธ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ๋ฅผ ํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

 

Connection Pool ์ด๋ž€?

์ปค๋„ฅ์…˜ ํ’€์€ ๋ฏธ๋ฆฌ ์—ฌ๋Ÿฌ ๊ฐœ์˜ DB ์—ฐ๊ฒฐ์„ ์ƒ์„ฑํ•ด ๋‘๊ณ , ์š”์ฒญ์ด ๋“ค์–ด์˜ฌ ๋•Œ๋งˆ๋‹ค ํ•˜๋‚˜์”ฉ ๋นŒ๋ ค์ฃผ๋Š” ๊ตฌ์กฐ์ž…๋‹ˆ๋‹ค.

Spring Boot์—์„œ๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ HikariCP๋ฅผ ์‚ฌ์šฉํ•ด ํšจ์œจ์ ์ธ ์—ฐ๊ฒฐ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

# application.yml
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/app
    username: root
    password: pass
    driver-class-name: com.mysql.cj.jdbc.Driver
    
    # HikariCP ์„ค์ •
    hikari:
      maximum-pool-size: 20        # ์ตœ๋Œ€ ์—ฐ๊ฒฐ ์ˆ˜
      minimum-idle: 5              # ์ตœ์†Œ ์œ ์ง€ ์—ฐ๊ฒฐ ์ˆ˜
      connection-timeout: 3000     # ์—ฐ๊ฒฐ ๋Œ€๊ธฐ ์‹œ๊ฐ„ (ms)
      idle-timeout: 600000         # ์œ ํœด ์—ฐ๊ฒฐ ์ œ๊ฑฐ ์‹œ๊ฐ„ (ms)
      max-lifetime: 1800000        # ์—ฐ๊ฒฐ ์ตœ๋Œ€ ์ˆ˜๋ช… (ms)
      leak-detection-threshold: 60000  # ์—ฐ๊ฒฐ ๋ˆ„์ˆ˜ ๊ฐ์ง€ ์‹œ๊ฐ„
๐Ÿ’ก ์‹ค๋ฌด ํŒ: ConnectionPool ํฌ๊ธฐ๋Š” CPU ์ฝ”์–ด ์ˆ˜์˜ 2-3๋ฐฐ ์ •๋„๋กœ ์„ค์ •ํ•˜๋Š” ๊ฒƒ์ด ์ผ๋ฐ˜์ ์ž…๋‹ˆ๋‹ค.

๐Ÿ” ORM๊ณผ JPA ์ƒํƒœ๊ณ„ ์ดํ•ดํ•˜๊ธฐ

ORM์ด๋ž€?

ORM(Object-Relational Mapping)์€ ๊ฐ์ฒด์™€ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์˜ ํ…Œ์ด๋ธ”์„ ๋งคํ•‘ํ•ด ์ฃผ๋Š” ๊ธฐ์ˆ ์ž…๋‹ˆ๋‹ค.

์šฐ๋ฆฌ๋Š” ์ž๋ฐ” ๊ฐ์ฒด๋ฅผ ๋‹ค๋ฃจ์ง€๋งŒ, ORM์ด ์ด๋ฅผ SQL๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ DB์— ๋ฐ˜์˜ํ•ด ์ค๋‹ˆ๋‹ค.

ORM์ด ๋„์ž…๋˜๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์žฅ์ ์ด ์žˆ์Šต๋‹ˆ๋‹ค:

  • SQL ์—†์ด ๊ฐ์ฒด ์ค‘์‹ฌ ๊ฐœ๋ฐœ ๊ฐ€๋Šฅ
  • DB ๋…๋ฆฝ์ ์ธ ์ฝ”๋“œ ์ž‘์„ฑ
  • ํŠธ๋žœ์žญ์…˜๊ณผ ์บ์‹œ๋ฅผ ํ†ตํ•œ ์„ฑ๋Šฅ ์ตœ์ ํ™”

JPA, Hibernate, Spring Data JPA ๊ด€๊ณ„

๊ธฐ์ˆ  ์—ญํ•  ์„ค๋ช…
JPA Java ORM ํ‘œ์ค€ ์ŠคํŽ™ ์ธํ„ฐํŽ˜์ด์Šค/์–ด๋…ธํ…Œ์ด์…˜ ์ •์˜
Hibernate JPA ๊ตฌํ˜„์ฒด ์‹ค์ œ ORM ๊ธฐ๋Šฅ ์ œ๊ณต
Spring Data JPA Spring์˜ JPA ์ถ”์ƒํ™” Repository ํŒจํ„ด์œผ๋กœ ํŽธ์˜ ๊ธฐ๋Šฅ ์ œ๊ณต

- JPA๋Š” ํ‘œ์ค€ ์ธํ„ฐํŽ˜์ด์Šค,

- Hibernate๋Š” ์‹ค์ œ ๋™์ž‘์„ ์ˆ˜ํ–‰ํ•˜๋Š” ๊ตฌํ˜„์ฒด,

- Spring Data JPA๋Š” Repository ๊ธฐ๋ฐ˜ ์ถ”์ƒํ™”๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.


๐Ÿงฉ Entity์™€ Repository ์„ค๊ณ„

Entity๋ž€?

Entity๋Š” DB ํ…Œ์ด๋ธ”์— ๋งคํ•‘๋˜๋Š” ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค.

๊ฐ Entity๋Š” ๊ณ ์œ ํ•œ ID์™€ ์ปฌ๋Ÿผ์„ ๊ฐ€์ง€๊ณ , JPA ์–ด๋…ธํ…Œ์ด์…˜์œผ๋กœ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค

@Entity
public class User {
    @Id @GeneratedValue
    private Long id;
    private String name;
    private String email;
    
    // Getter, Setter, Constructor
}

 

Repository๋ž€?

Repository๋Š” Entity์— ๋Œ€ํ•œ CRUD ์ž‘์—…์„ ์ถ”์ƒํ™”ํ•œ ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค.

Spring Data JPA์—์„œ๋Š” JpaRepository๋ฅผ ์ƒ์†๋ฐ›์•„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findByEmail(String email);
}
๐Ÿ’ก ๋ณต์žกํ•œ SQL ์—†์ด๋„ ๋ฐ์ดํ„ฐ ์กฐ์ž‘์ด ๊ฐ€๋Šฅํ•˜๊ณ , ์ปค์Šคํ…€ ์ฟผ๋ฆฌ๋„ ๋ฉ”์„œ๋“œ๋กœ ์ •์˜ํ•  ์ˆ˜ ์žˆ์–ด์š”.

 


๐Ÿ”— ์—ฐ๊ด€๊ด€๊ณ„ ๋งคํ•‘ (1:1, 1:N, N:M)

JPA์—์„œ๋Š” ์—”ํ‹ฐํ‹ฐ ๊ฐ„ ๊ด€๊ณ„๋„ ๊ฐ์ฒด์ฒ˜๋Ÿผ ํ‘œํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

  • 1:1 ๊ด€๊ณ„@OneToOne
  • 1:N / N:1 ๊ด€๊ณ„@OneToMany, @ManyToOne
  • N:M ๊ด€๊ณ„@ManyToMany (๋Œ€์‹  ์ค‘๊ฐ„ ์—”ํ‹ฐํ‹ฐ ๊ถŒ์žฅ)

 

์˜ˆ: ์‚ฌ์šฉ์ž์™€ ๊ฒŒ์‹œ๊ธ€ (1:N)

@Entity
public class User {
    @OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
    private List<Post> posts = new ArrayList<>();
}

@Entity
public class Post {
    @ManyToOne(fetch = FetchType.LAZY)
    private User user;
}

 

์ง€์—ฐ๋กœ๋”ฉ(LAZY) vs ์ฆ‰์‹œ๋กœ๋”ฉ(EAGER)์€ ์„ฑ๋Šฅ์— ํฐ ์˜ํ–ฅ์„ ์ฃผ๋ฏ€๋กœ ์‹ ์ค‘ํžˆ ์„ ํƒํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

 

๐Ÿ’ก ์‹ค๋ฌด ํŒ:
- ๋Œ€๋ถ€๋ถ„์˜ ์—ฐ๊ด€๊ด€๊ณ„๋Š” LAZY๋กœ ์„ค์ •ํ•˜์„ธ์š”
- N:M ๊ด€๊ณ„๋Š” ์ค‘๊ฐ„ ํ…Œ์ด๋ธ”์„ ๋ณ„๋„ ์—”ํ‹ฐํ‹ฐ๋กœ ํ’€์–ด ๊ด€๋ฆฌํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.

โšก ํŠธ๋žœ์žญ์…˜๊ณผ ์˜์†์„ฑ ์ปจํ…์ŠคํŠธ

์˜์†์„ฑ ์ปจํ…์ŠคํŠธ๋ž€?

์˜์†์„ฑ ์ปจํ…์ŠคํŠธ๋Š” JPA์—์„œ ์—”ํ‹ฐํ‹ฐ๋ฅผ 1์ฐจ ์บ์‹œ์— ์ €์žฅํ•˜์—ฌ ๊ด€๋ฆฌํ•˜๋Š” ๊ณต๊ฐ„์ž…๋‹ˆ๋‹ค.

์—ฌ๊ธฐ์„œ ๋™์ผ ๊ฐ์ฒด๋ฅผ ์žฌ์‚ฌ์šฉํ•˜๊ณ , ๋ณ€๊ฒฝ ๊ฐ์ง€๋ฅผ ํ†ตํ•ด ํšจ์œจ์ ์ธ UPDATE๊ฐ€ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

ํŠธ๋žœ์žญ์…˜ ๊ด€๋ฆฌ

Spring์—์„œ๋Š” @Transactional ์–ด๋…ธํ…Œ์ด์…˜์œผ๋กœ ํŠธ๋žœ์žญ์…˜์„ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

@Service
@Transactional
public class UserService {
    public void register(User user) {
        userRepository.save(user);
        // ํŠธ๋žœ์žญ์…˜ ์ปค๋ฐ‹ ์‹œ์ ์— DB ๋ฐ˜์˜๋จ
    }
}

 

 

์˜์†์„ฑ ์ปจํ…์ŠคํŠธ์˜ ํ•ต์‹ฌ ์—ญํ• 

  • 1์ฐจ ์บ์‹œ: ๋™์ผํ•œ ๊ฐ์ฒด ์žฌ์‚ฌ์šฉ (์กฐํšŒ ์„ฑ๋Šฅ ํ–ฅ์ƒ)
  • Dirty Checking: ๋ณ€๊ฒฝ ๊ฐ์ง€ ํ›„ ์ž๋™ ์—…๋ฐ์ดํŠธ
  • ์ง€์—ฐ ์“ฐ๊ธฐ: ํŠธ๋žœ์žญ์…˜ ์ปค๋ฐ‹ ์‹œ ์ผ๊ด„ ๋ฐ˜์˜

๐Ÿš€ N+1 ๋ฌธ์ œ์™€ ์„ฑ๋Šฅ ์ตœ์ ํ™” ์ „๋žต

N+1 ๋ฌธ์ œ๋ž€?

N+1 ๋ฌธ์ œ๋Š” JPA์—์„œ ๊ฐ€์žฅ ํ”ํ•˜๊ฒŒ ๋ฐœ์ƒํ•˜๋Š” ์„ฑ๋Šฅ ์ด์Šˆ์ž…๋‹ˆ๋‹ค.

์—ฐ๊ด€๋œ ์—”ํ‹ฐํ‹ฐ๋ฅผ ์กฐํšŒํ•  ๋•Œ ์ถ”๊ฐ€ ์ฟผ๋ฆฌ๊ฐ€ N๋ฒˆ ์‹คํ–‰๋˜๋Š” ๋ฌธ์ œ์˜ˆ์š”.

 

๊ฒŒ์‹œ๊ธ€ 1๋ฒˆ ์กฐํšŒ + ์ž‘์„ฑ์ž N๋ฒˆ ์กฐํšŒ = ์ด N+1๋ฒˆ ์ฟผ๋ฆฌ ์‹คํ–‰๋˜๋Š” ๋ฌธ์ œ์ž…๋‹ˆ๋‹ค.

 

ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•

1. JPQL + Fetch Join

@Query("SELECT p FROM Post p JOIN FETCH p.user")
List<Post> findAllWithUser();

 

2. @EntityGraph

@EntityGraph(attributePaths = {"user"})
List<Post> findAll();

 

3. Batch Size ์„ค์ •

spring:
  jpa:
    properties:
      hibernate:
        default_batch_fetch_size: 100
๐Ÿ’ก ๋ฐ์ดํ„ฐ๊ฐ€ ๋งŽ์„์ˆ˜๋ก ํŽ˜์ด์ง• + fetch join์„ ์ ์ ˆํžˆ ์กฐํ•ฉํ•˜๋Š” ์ „๋žต์ด ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ› ๏ธ ์‹ค๋ฌด์—์„œ ์ž์ฃผ ์‚ฌ์šฉํ•˜๋Š” JPA ํŒจํ„ด๋“ค

JPA๋Š” ๋‹จ์ˆœํ•œ CRUD๋ฅผ ๋„˜์–ด ์‹ค๋ฌด์—์„œ ์œ ์šฉํ•˜๊ฒŒ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ๋‹ค์–‘ํ•œ ํŒจํ„ด๋“ค์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

์•„๋ž˜๋Š” ๊ทธ์ค‘ ์ž์ฃผ ์‚ฌ์šฉ๋˜๋Š” ํ•ต์‹ฌ ํŒจํ„ด๋“ค์ž…๋‹ˆ๋‹ค.

 

1. Soft Delete (๋…ผ๋ฆฌ์  ์‚ญ์ œ)

DB์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฌผ๋ฆฌ์ ์œผ๋กœ ์‚ญ์ œํ•˜์ง€ ์•Š๊ณ , deleted๋ผ๋Š” ํ•„๋“œ๋ฅผ ์ด์šฉํ•ด ๋…ผ๋ฆฌ์ ์œผ๋กœ ์‚ญ์ œ๋œ ๊ฒƒ์ฒ˜๋Ÿผ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฐฉ์‹์ž…๋‹ˆ๋‹ค.

์‹ค๋ฌด์—์„œ ๋ฐ์ดํ„ฐ ๋ณต๊ตฌ๋‚˜ ์ด๋ ฅ ๊ด€๋ฆฌ๋ฅผ ์œ„ํ•ด ์ž์ฃผ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.

@Entity
@SQLDelete(sql = "UPDATE users SET deleted = true WHERE id = ?")
@Where(clause = "deleted = false")
public class User extends BaseEntity {

    @Column(name = "deleted")
    private boolean deleted = false;

    public void delete() {
        this.deleted = true;
    }
}
๐Ÿ’ก @SQLDelete๋Š” ์‚ญ์ œ ์ฟผ๋ฆฌ๋ฅผ ์ปค์Šคํ„ฐ๋งˆ์ด์ง•ํ•˜๊ณ , @Where๋Š” ์กฐํšŒ ์‹œ ์กฐ๊ฑด์„ ์ž๋™์œผ๋กœ ์ถ”๊ฐ€ํ•ด ์ค๋‹ˆ๋‹ค.

 

2. ๋ฒ„์ „ ๊ด€๋ฆฌ (Optimistic Lock)

๋‚™๊ด€์  ๋ฝ์€ ๋™์‹œ์— ์—ฌ๋Ÿฌ ํŠธ๋žœ์žญ์…˜์ด ๊ฐ™์€ ๋ฐ์ดํ„ฐ๋ฅผ ์ˆ˜์ •ํ•˜๋ ค ํ•  ๋•Œ ์ถฉ๋Œ์„ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•œ ์ „๋žต์ž…๋‹ˆ๋‹ค.

@Version ํ•„๋“œ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์ž๋™์œผ๋กœ ๋ฒ„์ „์ด ์ฆ๊ฐ€ํ•˜๋ฉฐ ์ถฉ๋Œ ์—ฌ๋ถ€๋ฅผ ์ฒดํฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

@Entity
public class Product {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;
    private BigDecimal price;

    @Version
    private Long version; // JPA๊ฐ€ ์ž๋™์œผ๋กœ ์ฆ๊ฐ€์‹œํ‚ด

    public void updatePrice(BigDecimal newPrice) {
        this.price = newPrice;
    }
}
๐Ÿ’ก ๊ฐ™์€ ๋ฐ์ดํ„ฐ๋ฅผ ๋™์‹œ์— ์ˆ˜์ •ํ•˜๋ ค๋Š” ์‹œ๋„๊ฐ€ ๊ฐ์ง€๋˜๋ฉด OptimisticLockException์ด ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค.

 

3. ๊ฐ์‚ฌ(Audit) ์ •๋ณด ์ž๋™ ๊ด€๋ฆฌ

์ž‘์„ฑ์ž, ์ˆ˜์ •์ž, ์ƒ์„ฑ์ผ, ์ˆ˜์ •์ผ๊ณผ ๊ฐ™์€ ์ •๋ณด๋ฅผ ์ž๋™์œผ๋กœ ๊ด€๋ฆฌํ•˜๊ณ  ์‹ถ์„ ๋•Œ JPA Auditing ๊ธฐ๋Šฅ์„ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ด๋ฅผ ์œ„ํ•ด ์„ค์ • ํด๋ž˜์Šค์™€ @CreatedDate, @LastModifiedDate ๋“ฑ์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

@Configuration
@EnableJpaAuditing
public class JpaAuditingConfig {

    @Bean
    public AuditorAware<String> auditorAware() {
        return () -> {
            Authentication authentication = SecurityContextHolder
                .getContext().getAuthentication();

            if (authentication == null || !authentication.isAuthenticated()) {
                return Optional.of("system");
            }

            return Optional.of(authentication.getName());
        };
    }
}

 

๊ทธ๋ฆฌ๊ณ  BaseEntity ๊ฐ™์€ ์ถ”์ƒ ํด๋ž˜์Šค์— ๊ณตํ†ต ํ•„๋“œ๋ฅผ ์„ ์–ธํ•ด ๋‘๊ณ  ์ƒ์†๋ฐ›์•„ ์‚ฌ์šฉํ•˜๋ฉด ๊ด€๋ฆฌ๊ฐ€ ํŽธ๋ฆฌํ•ด์ง‘๋‹ˆ๋‹ค.

@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
@Getter
public abstract class BaseEntity {

    @CreatedDate
    @Column(updatable = false)
    private LocalDateTime createdAt;

    @LastModifiedDate
    private LocalDateTime updatedAt;

    @CreatedBy
    @Column(updatable = false)
    private String createdBy;

    @LastModifiedBy
    private String updatedBy;
}
๐Ÿ’ก ์—”ํ‹ฐํ‹ฐ์— @CreatedBy, @LastModifiedBy๋ฅผ ์„ค์ •ํ•˜๋ฉด ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ์ž๋™ ์ฃผ์ž…๋ฐ›์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿงก ๋งˆ๋ฌด๋ฆฌํ•˜๋ฉฐ

์ด๋ฒˆ ๊ธ€์—์„œ๋Š” ๋ฐฑ์—”๋“œ ๊ฐœ๋ฐœ์—์„œ ํ•ต์‹ฌ์ธ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๋™๋ถ€ํ„ฐ JPA ํ™œ์šฉ, ์„ฑ๋Šฅ ์ตœ์ ํ™”๊นŒ์ง€ ์‹ค๋ฌด์—์„œ ๊ผญ ํ•„์š”ํ•œ ๋‚ด์šฉ๋“ค์„ ์ •๋ฆฌํ•ด ๋ดค์Šต๋‹ˆ๋‹ค.

 

๐Ÿ“Œ ํ•ต์‹ฌ ์š”์•ฝ

  • ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ: JDBC์™€ ConnectionPool์˜ ์ค‘์š”์„ฑ, HikariCP ์„ค์ •
  • ORM๊ณผ JPA: ๊ฐ์ฒด-๊ด€๊ณ„ ๋งคํ•‘์˜ ์žฅ์ ๊ณผ JPA ์ƒํƒœ๊ณ„ ์ดํ•ด
  • Entity ์„ค๊ณ„: BaseEntity, Repository ํŒจํ„ด, ์ปค์Šคํ…€ ์ฟผ๋ฆฌ ํ™œ์šฉ
  • ์—ฐ๊ด€๊ด€๊ณ„ ๋งคํ•‘: 1:1, 1:N, N:M ๊ด€๊ณ„์˜ ์˜ฌ๋ฐ”๋ฅธ ๋งคํ•‘ ๋ฐฉ๋ฒ•
  • ํŠธ๋žœ์žญ์…˜ ๊ด€๋ฆฌ: ์˜์†์„ฑ ์ปจํ…์ŠคํŠธ์™€ ๋‹ค์–‘ํ•œ ํŠธ๋žœ์žญ์…˜ ์ „๋žต
  • ์„ฑ๋Šฅ ์ตœ์ ํ™”: N+1 ๋ฌธ์ œ ํ•ด๊ฒฐ๊ณผ ์ฟผ๋ฆฌ ์ตœ์ ํ™” ๊ธฐ๋ฒ•

ํŠนํžˆ ์‹ค๋ฌด์—์„œ๋Š” N+1 ๋ฌธ์ œ ํ•ด๊ฒฐ๊ณผ ์ ์ ˆํ•œ ํŽ˜์ด์ง• ์ „๋žต์ด ์„œ๋น„์Šค ์„ฑ๋Šฅ์— ํฐ ์˜ํ–ฅ์„ ๋ฏธ์นฉ๋‹ˆ๋‹ค.

Fetch Join, @EntityGraph, Batch Size ๋“ฑ์„ ์ƒํ™ฉ์— ๋งž๊ฒŒ ์ ์ ˆํžˆ ํ™œ์šฉํ•˜๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•ด์š”.

 

๋‹ค์Œ ๊ธ€์—์„œ๋Š” Spring Boot ๊ธฐ๋ฐ˜ ๊ฐ„๋‹จํ•œ API ์‹ค์Šต์„ ๋‹ค๋ค„๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค. 

๊ถ๊ธˆํ•œ ์ ์ด๋‚˜ ๋” ์ž์„ธํžˆ ์•Œ๊ณ  ์‹ถ์€ JPA ํ™œ์šฉ๋ฒ•์ด ์žˆ๋‹ค๋ฉด ๋Œ“๊ธ€๋กœ ๋‚จ๊ฒจ์ฃผ์„ธ์š”! ๐Ÿš€

๋ฐ˜์‘ํ˜•