Skip to content

[recap] JPA N+1 문제 #8

@kgy1008

Description

@kgy1008

무엇을 μ•Œκ²Œ λ˜μ—ˆλ‚˜μš”?

ν”„λ‘μ‹œ 객체 (Proxy Object)

λ¨Όμ € ν”„λ‘μ‹œ 객체에 λŒ€ν•œ κ°œλ…λΆ€ν„° κ°„λ‹¨νžˆ μ‚΄νŽ΄λ΄…μ‹œλ‹€.

ν”„λ‘μ‹œ κ°μ²΄λŠ” μ—”ν‹°ν‹°μ˜ μ‹€μ œ 데이터λ₯Ό λ°μ΄ν„°λ² μ΄μŠ€μ—μ„œ κ°€μ Έμ˜€λŠ” μ‹œμ μ„ μ§€μ—°μ‹œν‚€κΈ° μœ„ν•΄ 원본(νƒ€κ²Ÿ) 객체λ₯Ό λŒ€μ‹ ν•΄μ„œ 호좜될 κ°€μ§œ 객체이닀.

ν”„λ‘μ‹œ κ°μ²΄λŠ” ν΄λΌμ΄μ–ΈνŠΈ μ½”λ“œμ™€ μ‹€μ œ λ°μ΄ν„°λ² μ΄μŠ€μ—μ„œ λ‘œλ“œλœ μ—”ν‹°ν‹° 객체(νƒ€κ²Ÿ 객체) 사이에 μœ„μΉ˜ν•˜κΈ° λ•Œλ¬Έμ— ν΄λΌμ΄μ–ΈνŠΈλŠ” μ‹€μ œ μ—”ν‹°ν‹° 객체에 직접 μ ‘κ·Όν•˜μ§€ μ•Šκ³ , ν”„λ‘μ‹œ 객체λ₯Ό 톡해 κ°„μ ‘μ μœΌλ‘œ μ ‘κ·Όν•˜κ²Œ λ©λ‹ˆλ‹€.

μ‰½κ²Œ λΉ„μœ ν•˜μžλ©΄ νƒ€κ²Ÿ 객체λ₯Ό μ§‘ 주인이라고 μƒκ°ν–ˆμ„ λ•Œ, ν”„λ‘μ‹œ κ°μ²΄λŠ” μ§‘ 주인을 λŒ€μ‹ ν•΄μ„œ 계약을 μš”μ²­λ°›λŠ” μ€‘κ°œμΈμœΌλ‘œ μƒκ°ν•˜λ©΄ λ˜κ² λ„€μš”!

μ§€μ—° λ‘œλ”© (Lazy Loading)

μ§€μ—° λ‘œλ”©μ€ μ—”ν‹°ν‹°κ°€ λ‘œλ“œλ  λ•Œ, μ—°κ΄€λœ μ—”ν‹°ν‹°λ₯Ό μ¦‰μ‹œ λ‘œλ“œν•˜μ§€ μ•Šκ³  ν•„μš”ν•œ μ‹œμ μ— μ—°κ΄€λœ 객체의 데이터λ₯Ό λ‘œλ“œν•˜λŠ” λ°©μ‹μž…λ‹ˆλ‹€. @OneToMany λž‘ @ManyToMany λŠ” κΈ°λ³Έ 섀정이 μ§€μ—°λ‘œλ”©μ΄λΌκ³  ν•©λ‹ˆλ‹€:D

@ManyToOne(fetch = FetchType.LAZY)  // μ§€μ—° λ‘œλ”© μ„€μ • 방법

연관관계λ₯Ό 맺은 μ—”ν‹°ν‹° μ΄λ•Œ μ‚¬μš©λ˜λŠ” 것이 λ°”λ‘œ ν”„λ‘μ‹œ κ°μ²΄μΈλ°μš”, μ§€μ—° λ‘œλ”© λ°©μ‹μ—μ„œλŠ” μ—°κ΄€λœ μ—”ν‹°ν‹° λ°μ΄ν„°λŠ” μ‹€μ œλ‘œ μ ‘κ·Όν•  λ•ŒκΉŒμ§€ λ‘œλ“œλ˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€. 즉, ν΄λΌμ΄μ–ΈνŠΈ μ½”λ“œκ°€ 객체의 λ©”μ„œλ“œλ₯Ό ν˜ΈμΆœν•΄μ•Ό λΉ„λ‘œμ†Œ ν”„λ‘μ‹œ κ°μ²΄λŠ” κ·Έ μˆœκ°„ λ°μ΄ν„°λ² μ΄μŠ€μ— μ ‘κ·Όν•˜μ—¬ μ‹€μ œ 데이터λ₯Ό λ‘œλ“œν•˜κ²Œ λ©λ‹ˆλ‹€.

μ½”λ“œλ‘œ ν•œλ²ˆ μ‚΄νŽ΄λ΄…μ‹œλ‹€!

public Blog getBlogByPostId(Long postId) {
    Post post = postRepository.findById(postId).orElseThrow(() -> new NotFoundException("Post not found"));
    Blog blog = post.getBlog();  
    return blog;
}

Post μ—”ν‹°ν‹° λ‚΄μ—μ„œ Blog에 λŒ€ν•œ 접근은 FetchType.LAZY 즉, μ§€μ—° λ‘œλ”©μœΌλ‘œ μ„€μ •λ˜μ–΄ 있기 λ•Œλ¬Έμ— Post 객체만 λ¨Όμ € λ‘œλ“œλ˜κ³  Blog에 λŒ€ν•œ ν”„λ‘μ‹œ 객체가 μƒμ„±λ˜κ²Œ λ©λ‹ˆλ‹€. κ·Έ ν›„, getter ν•¨μˆ˜λ₯Ό 톡해 post.getBlog() λ₯Ό ν˜ΈμΆœν•˜λ©΄, ν”„λ‘μ‹œ κ°μ²΄λŠ” μ‹€μ œ 데이터가 ν•„μš”ν•œ μ‹œμ μ΄κΈ° λ•Œλ¬Έμ— λΉ„λ‘œμ†Œ λ°μ΄ν„°λ² μ΄μŠ€μ— μ ‘κ·Όν•˜μ—¬ Blog 데이터λ₯Ό λ‘œλ“œν•˜κ²Œ λ˜λŠ” 것이죠!

μ¦‰μ‹œ λ‘œλ”© (Eager Loading)

μ¦‰μ‹œ λ‘œλ”©μ΄λž€ 말 κ·ΈλŒ€λ‘œ 데이터λ₯Ό μ‘°νšŒν•  λ•Œ, μ—°κ΄€λœ λͺ¨λ“  객체의 λ°μ΄ν„°κΉŒμ§€ ν•œ λ²ˆμ— λΆˆλŸ¬μ˜€λŠ” λ°©μ‹μž…λ‹ˆλ‹€. @ManyToOne λž‘ @OneToOne λŠ” κΈ°λ³Έ 섀정이 μ¦‰μ‹œ λ‘œλ”©μ΄λΌκ³  ν•˜λ„€μš”:D

@OneToMany(fetch = FetchType.EAGER)  // μ¦‰μ‹œ λ‘œλ”© μ„€μ • 방법

μ—­μ‹œ μ½”λ“œλ‘œ ν•œλ²ˆ μ‚΄νŽ΄λ΄…μ‹œλ‹€.

μ•„κΉŒμ˜ μ½”λ“œμ—μ„œ λ§Œμ•½ Post 엔티티와 Blog μ—”ν‹°ν‹°κ°€ FetchType.EAGER 즉, μ¦‰μ‹œ λ‘œλ”©μœΌλ‘œ μ„€μ •λ˜μ–΄ μžˆλ‹€λ©΄ μ–΄λ–»κ²Œ λ κΉŒμš”? postRepository.findById(postId) λ©”μ„œλ“œλ₯Ό 톡해 Post μ—”ν‹°ν‹°λ₯Ό μ‘°νšŒν•  λ•Œ μ¦‰μ‹œ Post 데이터와 μ—°κ΄€λœ Blog 데이터가 ν•¨κ»˜ λ‘œλ“œλ©λ‹ˆλ‹€. ν”„λ‘μ‹œ 객체의 생성 없이 λ³„λ„μ˜ 쿼리λ₯Ό 톡해 λ°”λ‘œ Blog 엔티티에 μ ‘κ·Όν•˜λŠ” 것이죠!

이처럼 μ¦‰μ‹œ λ‘œλ”© 방식은 μ§€μ—° λ‘œλ”© 방식에 λΉ„ν•΄ μ—°κ΄€λœ 데이터가 ν•„μš”ν•œ μž‘μ—…μ—μ„œ λΉ λ₯΄κ²Œ 처리λ₯Ό ν•  수 μžˆλ‹€λŠ” μž₯점이 μžˆκ² λ„€μš”! ν•˜μ§€λ§Œ!!! 특히 μ‹€λ¬΄μ—μ„œλŠ” μ¦‰μ‹œ λ‘œλ”©μ„ 가급적 μ‚¬μš©ν•˜μ§€ μ•ŠλŠ”λ‹€κ³  ν•©λ‹ˆλ‹€! 이 μ΄μœ λ‘œλŠ” μ•žμœΌλ‘œ μ„€λͺ…ν•  JPA N+1 λ¬Έμ œμ™€λ„ 연관이 μžˆμŠ΅λ‹ˆλ‹€.

JPA N+1 문제

JPA N+1 λ¬Έμ œλž€?

JPA N+1 λ¬Έμ œλž€ 데이터λ₯Ό μ‘°νšŒν•  λ•Œ, 1개의 쿼리둜 μš”μ²­μ΄ μ²˜λ¦¬ν•  κ²ƒμœΌλ‘œ κΈ°λŒ€ν–ˆμœΌλ‚˜ μ˜λ„ν•˜μ§€ μ•Šμ€ N개의 쿼리가 μΆ”κ°€μ μœΌλ‘œ 더 λ°œμƒν•˜λŠ” ν˜„μƒμ„ λ§ν•©λ‹ˆλ‹€.

말둜만 λ“€μœΌλ‹ˆ λ„ˆλ¬΄ λͺ¨ν˜Έν•œ 것 κ°™λ„€μš”. 이 μ—­μ‹œ μ½”λ“œλ₯Ό ν•œλ²ˆ μ‚΄νŽ΄λ΄…μ‹œλ‹€

public void getAllBlogTitleByPostId(Long postId) {
    List<Post> posts = postRepository.findByPostId(postId);
    for (Post post : posts) {
	System.out.println(post.getBlog().getTitle());
    }
}

ν•΄λ‹Ή λ©”μ†Œλ“œλ₯Ό μ‹€ν–‰μ‹œν‚€λ©΄ λ‹€μŒ μˆœμ„œμ— 따라 쿼리문이 λ°œμƒν•˜κ² μ£ !

  1. μ£Όμ–΄μ§„ postId에 λŒ€μ‘ν•˜λŠ” λͺ¨λ“  Post 객체듀을 λ°μ΄ν„°λ² μ΄μŠ€λ‘œλΆ€ν„° λ‘œλ“œν•˜λŠ” 쿼리λ₯Ό λ°œμƒμ‹œν‚¨λ‹€.
  2. λ¦¬μŠ€νŠΈμ—μ„œ 각 Post 객체에 λŒ€ν•΄ post.getBlog().getTitle() λ©”μ„œλ“œλ₯Ό ν˜ΈμΆœν•  λ•Œλ§ˆλ‹€, 각각의 Post 객체에 λŒ€ν•΄ κ°œλ³„μ μœΌλ‘œ Blogλ₯Ό λ‘œλ“œν•˜λŠ” μΆ”κ°€ 쿼리가 μ‹€ν–‰λœλ‹€. (N번의 쿼리 λ°œμƒ)

결과적으둜 첫번째 Post 객체λ₯Ό λ‘œλ“œν•˜λŠ” 쿼리 1κ°œμ™€ 각 Post 객체의 Blogλ₯Ό λ‘œλ“œν•˜λŠ” 좔가적인 쿼리 N개(각 Post λ§ˆλ‹€ 1개)κ°€ λ°œμƒν•˜κ²Œ λ˜μ–΄ 총 N+1회의 쿼리가 λ°œμƒν•˜κ²Œ λ©λ‹ˆλ‹€.

이런 JPA N+1 λ¬Έμ œλŠ” fetch νƒ€μž…μ΄ 원인인 것은 μ•„λ‹ˆμ§€λ§Œ(μ¦‰μ‹œ λ‘œλ”©, μ§€μ—° λ‘œλ”© λͺ¨λ‘ λ°œμƒν•  수 있음) μ¦‰μ‹œ λ‘œλ”©μΌ 경우, 특히 κ°œλ°œμžκ°€ μ œμ–΄ν•  수 μ—†λŠ” 쿼리가 μ‹€ν–‰λ˜κ³  더 자주 N+1λ¬Έμ œμ— λ§ˆμ£Όν•˜κΈ° λ•Œλ¬Έμ— μ‚¬μš©μ„ μ§€μ–‘ν•œλ‹€κ³  ν•˜λ„€μš”

μ–΄λ €μš΄ λ‚΄μš©μ΄ μžˆμ—ˆλ‹€λ©΄ 이λ₯Ό μ–΄λ–»κ²Œ ν•΄κ²°ν•˜μ˜€λ‚˜μš”?

  • ꡬ글링과 유튜브

μ–΄λ–€ 자료λ₯Ό μ°Έκ³ ν•˜μ˜€λ‚˜μš”?

https://incheol-jung.gitbook.io/docs/q-and-a/spring/n+1

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions