CS
  • CS-Study
  • database
    • B-Tree와 B+Tree
    • DB JOIN
    • DB Lock
    • DB 트래픽
    • DBCP (DB Connection Pool)
    • Flyway
    • Message Broker
    • MySQL InnoDB 스토리지 엔진
    • MySQL 엔진 아키텍처
    • RDB와 NoSQL
    • Redis
    • SQL Injection
    • 스키마 (Schema)
    • Table Scan과 Index Scan
    • Apache Kafka
    • Key
    • 뷰 (View)
    • 인덱스
    • 정규화
    • RDBMS, NoSQL의 클러스터링/리플리케이션 방식
    • 트랜잭션(Transaction)
    • 트랜잭션의 격리성(Transaction Isolation)
    • 프로시저와 트리거
    • DB 정규화 (Normalization)
  • etc
    • MSA
    • REST, REST API, RESTful
    • SOLID 원칙
    • TDD (Test-Driven Development)
    • 서버리스
    • 컨테이너와 도커
  • java
    • Collections
    • Garbage Collection
    • Generic
    • JDBC
    • Java Virtual Machine(JVM)
    • Java Thread
    • Java8 vs Java11 vs Java17
    • 객체지향 프로그래밍 OOP (Object Oriented Programing)
    • Optional
    • RxJava(Reactive Programming)
    • 문자열(String & StringBuffer & StringBuilder)
    • Synchronized
    • Virtual Thread
    • Wrapper Class
    • Equals()와 Hashcode()
    • final
    • Jackson 라이브러리
    • 리플렉션(Reflection)
    • static class와 static method
    • 스트림(Stream)과 람다(Lambda)
    • 스프링 프레임워크에서 사용되는 디자인 패턴
    • 예외처리(Exception)
    • Java Annotation
    • 추상클래스와 인터페이스
  • network
    • 3-way handshake
    • 4-way Handshake
    • DHCP(Dynamic Host Configuration Protocol)
    • DMZ(DeMilitarized Zone)
    • DNS(Domain Name System)
    • HTTP Method
    • HTTP 버전 비교
    • HTTP status code
    • HTTP
    • IP Address
    • Mutiplexing & Demultiplexing
    • OSI 7계층
    • SOP, CORS
    • TCP와 UDP
    • XSS와 CSRF
    • gRPC
    • Stateless와 Connectionless
    • 라우터 Router
    • 로드밸런서(Load Balancer)
    • 브라우저에 URL입력시 네트워크 상 일어나는 일
    • 서브넷 마스크, 게이트웨이
    • 웹 소켓과 소켓 통신
    • 쿠키(Cookie)와 세션(Session)
  • operating-system
    • IPC (Inter Process Communication)
    • 인터럽트
    • TLB
    • 스레싱 Thrashing
    • Thread Pool, Fork-Join
    • Thread Safe
    • 프로세스
    • 가상 메모리
    • 데드락 (DeadLock, 교착 상태)
    • 동기/비동기 & 블로킹/논블록킹
    • 동기화(Synchronization)
    • 메모리 할당과 단편화
    • 뮤텍스와 세마포어, 모니터
    • 세그먼테이션과 페이징
    • 운영체제
    • 캐시 메모리
    • Context switching(문맥 교환)
    • 컴파일
    • 파일 시스템
    • 페이지 교체 알고리즘(Page Replacement Algorithm)
    • 프로세서 스케줄링 알고리즘
    • 프로세스 주소 공간
  • spring
    • @Transactional
    • AOP(Aspect-Oriented Programming)
    • DTO, DAO, VO, Entity
    • DispatcherServlet
    • Hibernate, JPA, Spring Data JPA
    • Ioc와 DI
    • JPA 연관관계 맵핑
    • N+1 Problem
    • ORM
    • Persistence Context
    • SQL Mapper vs ORM vs QueryBuilder
    • Servlet Filter와 Spring Interceptor
    • Servlet
    • Spring MVC와 Spring Boot
    • Tomcat
    • WebFlux
Powered by GitBook
On this page
  • N+1 Problem
  • 왜 발생할까?
  • 해결 방법
  • 결론
  • Referenec
  1. spring

N+1 Problem

N+1 Problem

N + 1 문제란 연관관계가 설정된 엔티티를 조회할 때 연관된 엔티티를 조회하는 쿼리가 N + 1번 발생하는 문제임.

왜 발생할까?

  • JPA와 같은 ORM에서는 연관된 엔티티를 Proxy 객체로 Lazy Loading을 함.

    • Proxy 객체 : 실제 객체를 대신하여 사용자의 요청이 있을 때 실제 객체를 생성하여 반환하는 객체.

    • Lazy Loading : 연관된 엔티티를 조회하는 시점에 쿼리를 실행하여 조회하는 방식.

  • 지연로딩을 사용하는 이유?

    • 불필요한 데이터를 조회하지 않기 위함.

    • 일대다의 연관관계의 경우 많은 데이터를 한 번에 조회하게 되면 DB 성능에 문제가 발생 할 수 있음.

N + 1 예시 코드

아래와 같은 Entity가 있다고 가정할 때,

@Entity(name = "post")
data class Post (
    @field:Id
    @field:GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long? = null,
    val title: String,
    val content: String,
    val author: String,
    @field:OneToMany(mappedBy = "post", fetch = FetchType.LAZY)
    val comments: List<Comment> = listOf()
)

/api/posts/{id} API를 호출하면 PostResponse를 1개 반환하고 이 API를 사용할 때 발생한 쿼리는 아래와 같음.

위 쿼리를 보면 Post를 조회하기 위한 쿼리가 1번 Comment를 조회하기 위한 쿼리가 1번 발생할 수 있음. 이를 N + 1 문제라고 함.

왜 문제가 될까?

예를 들어서 Post가 20개 정도있고 Comment가 2000개 정도 있다고 가정할 때, Post를 조회하는 API를 호출하면 20개의 Post를 조회하는 쿼리가 발생하고, 각 Post마다 Comment를 조회하는 쿼리가 20번 발생하게 됨. 이렇게 되면 총 40번의 쿼리가 발생하게 되어 DB에 부하가 걸리게 됨. 이러한 경우, DB의 성능이 충분하다면 단 한 번의 조인으로 모든 데이터를 조회하는 것이 효율적임.

해결 방법

🚨 fetchType.EAGER을 사용하면 안됨!!

    fun findAll(): List<PostResponse> {
        return postRepository.findAll().map{
            logger.info("Comment 조회 전")
            val result = PostConverter.toResponseUseRepository(it)
            logger.info("Comment 조회 후")
            result
        }
    }

현재 Comment가 객체탐색되는 코드는 위 코드에서 toResponseUseRepository 부분임. 그래서 해당 라인 앞 뒤로 log를 찍어보면 Lazy Loading에서는 아래와 같은 결과가 나옴.

하지만 EAGER로 설정하면 아래와 같은 결과가 나옴.

위와 같은 결과가 나오는 이유는 Post N개를 조회하는 상황에서는 Lazy와 Eager의 차이는 Comment가 조회되는 시점에 Comment를 조회하는지(Lazy), Post를 조회하는 시점에 Comment를 조회하는지(Eager)의 차이이기 때문임.

Post 1개를 조회하는 상황에선 FetchType.EAGER을 사용하면 N + 1문제가 발생하지 않고 Left Join이 걸림.

FetchJoin 사용

@Query Annotation을 사용해서 FetchJoin을 사용하는 방법.

    @Query("SELECT p FROM post p JOIN FETCH p.comments")
    fun findAllPostFetchJoinComments(): List<Post>
  • 👇 실제 발생 쿼리

Hibernate: select p1_0.id,p1_0.author,c1_0.post_id,c1_0.id,c1_0.author,c1_0.content,p1_0.content,p1_0.title from example.post p1_0 join example.comment c1_0 on p1_0.id=c1_0.post_id

BatchSize

  • Lazy Loading시 프록시 객체를 조회할 때 where in절로 묶어서 한번에 조회 할 수 있게 해주는 옵션.

  • yml에 전역 옵션으로 적용할 수 있고 @BatchSize를 통해 연관관계 BatchSize를 다르게 적용가능.

  • Batch Size 100~1000정도로 적용하고 DBMS에 따라서 where in 절은 1000까지 제한하는 경우가 있으므로 1000이상은 설정을 잘 하지않음.

EntityGraph 사용

  • @EntityGraph Annotation을 사용하면 EntityGraph의 로딩을 기본 Lazy에서 Eager로 변경할 수 있음.

  • EntityGraph를 사용하는 경우 outer left join을 수행하여 데이터를 가져옴.

    @EntityGraph(attributePaths = ["comments"])
    override fun findAll(): List<Post>

그 외 방법들

MyBatis나 QueryDSL 같은 QueryBuilder를 사용해서도 N + 1 문제를 해결할 수 있음.

결론

  • DB 성능을 고려하고 쿼리를 잘게 쪼갤 수 있는 방식인 @BatchSize Annotation을 사용하는 방식이 적절하다고 느껴짐.

  • 1:1 관계의 경우에서는 FetchType.EAGER을 사용해도 큰 문제가 없음.

  • N + 1 문제가 과연 문제가 맞는지?도 고려해봐야 할 대상임.

Referenec

PreviousJPA 연관관계 맵핑NextORM

Last updated 9 months ago

참조 PR : Post.comments fetchType Eager Lazy 비교
참조 PR
참조 PR
N + 1 Problem, Incheol's Tech Blog
JPA N+1 문제와 해결법 총정리, teahee kim's velog
JPA Batch Size에 대한 고찰