Spring

[Spring] OSIV, N+1문제, LAZY와 EAGER

montmer27 2026. 1. 23. 22:20

OSIV (Open Session In View)

  • JPA/Hibernate에서 영속성 컨텍스트(Session)를 View 렌더링 시점까지 열어두는 기능
  • Spring Boot에서는 기본값이 true

OSIV ON -> DB 커넥션을 API 응답이 완전히 끝날 때까지 유지

OSIV OFF -> 트랜잭션이 끝나면 즉시 커넥션 반환

 

끄는 것이 좋은 이유

  • 커넥션 점유 시간 단축 : 커넥션 풀 효율 증가
  • 동시 처리량 향상 : 병목 발생 예방
  • 명확한 트랜잭션 경계 : 지연 로딩이 어디서든 발생할 수 있는 불명확함 제거

끌 때의 단점(TRADEOFF)

  • 지연 로딩 사용 시 LazyInitializationException 발생 가능
  • Fetch Join이나 DTO 변환을 명시적으로 처리해야 함

KEY TAKEAWAY

  • 트래픽이 많은 서비스에서는 OSIV를 끄고(false), 필요한 데이터는 트랜잭션 안에서 미리 로딩하는 것이 성능상 유리

N+1 문제

정의 : 연관된 데이터를 조회할 때 쿼리가 N+1번 실행되는 성능 문제

예) 1번의 메인 쿼리 + N개 데이터마다 추가 쿼리 발생

 

// 게시글 10개 조회
List<Post> posts = postRepository.findAll(); //1번 쿼리

// 각 게시글의 작성자 조회
for(Post post:posts) {
	String author = post.getUser().getName();
}
// 총 11번(1 + 10번)의 쿼리가 실행

 

발생 원인

  • 지연 로딩 (Lazy Loading) 설정된 연관 엔티티에 접근할 때
  • 반복문 안에서 연관 데이터를 조회할 때

해결 방법

핵심 : 필요한 데이터를 미리 한 번에 가져와서 쿼리 횟수를 줄이는 것

1. Fetch Join

@Query("SELECT p FROM Post p JOIN FETCH p.user"); // Post들을 가져올 때 거기 딸린 user도 같이 가져와라
List<Post> findAllWithUser();
// 1번의 JOIN 쿼리로 해결 가능

2. @EntityGraph

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

// 필드명만 {""}안에 잘 적어주면 됨

3. Batch Size

@BatchSize(size = 100)
@OneToMany
// N개를 100개씩 묶어서 조회
// N:1 관계에서 1쪽에 있음

 


Lazy vs Eager

Lazy를 더 선호하는 이유

1. 불필요한 데이터 로딩 방지

- Eager : 연관 데이터를 항상 조회

- Lazy : 실제 사용할 때만 조회

2.  초기 쿼리 성능

- Eager : 처음부터 JOIN이 많아져 느려질 수 있다.

- Lazy : 필요한 것만 가져와서 가볍다.

3. 유연한 최적화

- Eager : 항상 JOIN 강제

- Lazy : 상황에 따라 Fetch Join 선택 가능

 

Lazy의 단점

1. 페이징 불가

2. 중복 데이터

- OneToMany 관계에서 부모 엔티티 중복 발생

- DISTINCT 필요

3. 여러 컬렉션 Fetch Join 불가

- 동시 Fetch Join 시도 시 MultipleBagFetchException 발생 가능

 

Lazy의 적정 사용시기

- ManyToTone, OneToOne : 안전하게 사용 가능

- OneToMany : 페이징 없을 때만, DISTINCT를 추가하여 가능