계좌가 많아질 경우, 어떻게 설계하면 좋을까?🤔
N+1은 왜 발생할까?🤔(Fetch Join 적용 전후 성능 비교)
N+1은 왜 JPA에서 발생할까?🤔(JPQL)
N+1은 메모리에 어떤 영향을 미칠까?🤔(JVM)
영속성 컨텍스트는 N+1과 어떤 관계가 있을까?🤔(Hibernate)
Fetch Join은 영속성 컨텍스트에서 어떻게 동작할까?🤔
Hibernate 내부에서 영속성 컨텍스트는 어떻게 동작할까?🤔
N+1은 GC를 왜 자주 발생시킬까?🤔
스프링은 직접 데이터베이스와 통신하지 않고, 다음 단계로 통신한다.
개발자 코드 → JPA(ORM을 위한 표준 인터페이스) → Hibernate(JPA 구현체) → JDBC(DB 연결 드라이버) → Database(H2 / MySQL 등)
Hibernate(ORM)가 자동으로 H2 데이터베이스에 테이블을 생성하고 쿼리를 실행해주는 관계이다.
N+1이 그저 ‘쿼리가 많이 나가서 느리다’로 알려져 있다. 하지만 실제 운영 환경에서는 다음과 같은 문제 상황이 있다.
즉, DB 문제와 JVM 메모리 문제가 동시에 발생한다.

메서드 영역은 JVM이 시작될 때 생성되는 공간으로 바이트 코드(.class)를 처음 메모리 공간에 올릴 때 초기화되는 대상을 저장하기 위한 메모리 공간이다. 모든 쓰레드가 공유하는 메모리 영역이다. JVM이 동작하고 클래스가 로드될 때 적재되서 프로그램이 종료될 때까지 저장 된다. 메소드 영역은 클래스, 인터페이스, 메소드, 필드, Static 변수 등의 바이트 코드를 보관한다. 메서드 영역(Method Area) 은 Class Area 나 Static Area 로도 불리운다.
힙 영역은 메서드 영역와 함께 모든 쓰레드가 공유하며, JVM이 관리하는 프로그램 상에서 데이터를 저장하기 위해 런타임 시 동적으로 할당하여 사용하는 영역이다. 모든 쓰레드가 공유하며, new 키워드로 생성된 객체와 배열이 생성된다. 또한, 메소드 영역에 로드된 클래스만 생성이 가능하고 Garbage Collector가 참조되지 않는 메모리를 확인하고 제거하는 영역이다.
메서드 호출 시마다 각각의 스택 프레임(그 메서드만을 위한 공간)이 생성된다. 스택 영역은 int, long, boolean 등 기본 자료형을 생성할 때 저장하는 공간으로, 임시적으로 사용되는 변수나 정보들이 저장되는 영역이다.
다음의 코드를 실행한다고 가정하자.
List<Order> orders = orderRepository.findAll();
for (Order order : orders) {
order.getMember().getName();
}
이미 여기서 객체 N개가 생성된다.
order.getMember() 호출 순간 Member도 N개 생성된다.
| 대상 | 개수 |
|---|---|
| Order 엔티티 | N |
| Member 엔티티 | N |
| 프록시 객체 | N |
| 내부 Hibernate 관리 객체 | N 이상 (추가 메모리 사용 발생) |
최소 2N ~ 4N개의 객체를 생성한다. 만약 N = 10,000이면이라면 20,000~40,000개의 객체가 생성된다.
JPA는 DB에서 가져온 데이터를 엔티티 객체로 변환해서 힙에 올린다. 예를 들어 Order 100개 조회하면 각 Order마다 Member lazy loading 발생한다. 그러면 Order 객체 100개와 Member 객체 100개(혹은 그 이상)가 전부 힙에 올라간다.
특히 문제는 다음과 같은 경우이다.
JPA의 영속성 컨텍스트는 다음과 같은 형태로 힙 메모리 안에 존재한다.
Map<엔티티ID, 엔티티객체>
N+1이 발생하면 다음과 같은 이유로 메모리 점유가 계속 유지된다.
객체가 많이 생성되면 다음과 같은 상황이 발생한다. 이는 결국 GC 압박 문제로 이어질 수 있다.
객체가 과도하게 생성되면 다음과 같은 GC 흐름이 발생하며, 이는 트랜잭션 처리 시스템에 치명적인 성능 저하를 유발할 수 있다.
select o from Order o
join fetch o.member
객체 생성 패턴이 예측 가능해지면서 GC 압박이 완화된다.
N+1은 단순히 “쿼리가 많이 나간다”가 아니라 “객체 그래프를 제어하지 못해서 JVM 메모리를 비효율적으로 사용한다”는 문제이다. N+1은 힙에 객체를 대량으로 생성하므로 Fetch Join은 단순히 DB 최적화가 아니라 힙 메모리 최적화이기도 하다.
Gemini
ChatGPT
Claude.ai
JVM의 메모리 구조
스프링 H2(+ Hibernate)
JVM 메모리 구조란? (JAVA)
JVM 내부 구조 & 메모리 영역 💯 총정리