일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
31 |
- 네트워크
- Backtracking
- greedy
- Algorithm
- dynamic programming
- 그리디
- 프로그래머스
- BFS
- 해시
- 알고리즘
- Spring
- 깊이우선탐색
- DynamicProgramming
- Network
- 백트래킹
- 스프링
- switch
- programmers
- 구현
- 이분탐색
- 백준
- boj
- DP
- HashMap
- 해시맵
- 너비우선탐색
- 동적계획법
- 브루트포스
- broadcast
- DFS
- Today
- Total
옌의 로그
[Spring] JPA 연관관계 매핑과 성능 최적화 (Fetch Join vs. Open-in-view) 본문
1. 서론: 연관관계, 그리고 성능 고민
최근에 투입된 프로젝트는 다양한 도메인 간에 1:N 관계가 아주 많은 구조를 가지고 있었다.
상품과 카테고리, 유저와 구매내역, 상품과 메타데이터 등 대부분의 엔티티가 서로 깊게 연관돼 있었고,
그 연관관계들이 곧바로 화면 응답이나 API 출력으로 이어지다 보니,
JPA의 Lazy Loading, N+1 문제, fetch join, open-in-view 설정 같은 키워드들이 일상처럼 따라다녔다.
특히, 연관된 데이터를 DTO로 변환해야 하는 경우가 많았는데, 이때마다 트랜잭션이 이미 종료돼 있어서 Lazy 객체 접근 시 LazyInitializationException이 터지는 일이 많았다.
그 과정에서 자연스럽게 다음과 같은 질문이 생겼다:
- 지연 로딩은 어떻게 최적화할 수 있을까?
- 트랜잭션 범위를 어디까지 가져가야 할까?
- Open-in-view를 유지해도 괜찮을까?
- fetch join은 언제, 어떻게 쓰는 게 좋을까?
이번 글에서는 그 고민의 결과로 정리한 내용들을 바탕으로,
JPA 연관관계 매핑과 성능 최적화 전략: Fetch Join vs. Open-in-view를 비교해보려 한다.
2. Lazy Loading, 왜 문제인가?
기본적으로 JPA의 연관관계는 @OneToMany, @ManyToOne 등에서 지연 로딩(LAZY) 으로 설정하는 게 일반적이다.
이는 객체를 조회할 때, 연관된 엔티티를 즉시 불러오지 않고, 실제로 접근할 때 쿼리를 날리는 방식이다.
@Entity
public class Product {
@ManyToOne(fetch = FetchType.LAZY)
private Category category;
}
예시: 상품 리스트를 조회하는 경우
List<Product> products = productRepository.findAll();
for (Product product : products) {
System.out.println(product.getCategory().getName());
}
위 코드는 Product를 조회하는 쿼리 1번 + 각 Category를 조회하는 쿼리 N번 = 총 N+1번의 쿼리가 발생한다.
3. Fetch Join이란?
Fetch Join 은 연관 엔티티를 SQL 조인으로 한 번에 조회하도록 하는 JPA의 기능이다.
예시
@Query("SELECT p FROM Product p JOIN FETCH p.category")
List<Product> findAllWithCategory();
이렇게 하면 쿼리는 단 1번, Product와 Category를 동시에 조회하게 된다.
장점
- N+1 문제 해소
- 트랜잭션 종료 이후에도 Lazy 객체 사용 가능 (이미 로딩됨)
단점
- 복잡한 연관관계에서는 데이터 중복이 발생할 수 있다
- 카디널리티 곱 문제 발생 (Cartesian Product) < 이건 추후에 포스팅 하겠다 . .
- 여러 fetch join을 한 번에 사용할 수 없음 (JPA 제약사항)
4. Open-in-view란?
Open-in-view는 트랜잭션 범위를 컨트롤러까지 열어두는 방식이다.
즉, 서비스에서 트랜잭션이 끝났더라도, 컨트롤러에서 Lazy 객체를 접근할 수 있도록 영속성 컨텍스트를 유지시킨다.
Spring Boot 기본 설정
spring.jpa.open-in-view=true 로 기본 설정되어 있다.
장점
- fetch join 없이도 연관 객체 접근 가능
- 코드가 간결해짐 (DTO 변환 시도까지 고려 안해도 됨)
단점
- 컨트롤러, 뷰 레이어에서 DB 연결을 유지 → 커넥션 낭비
- Lazy 로딩 시점이 불명확 → 디버깅 어려움
- 비즈니스 로직과 프레젠테이션 로직이 뒤섞일 수 있음
5. 비교 정리: 언제 무엇을 써야 할까?
항목 | Fetch Join | Open-in-view |
트랜잭션 범위 | 서비스 계층 안에서 종료 | 컨트롤러까지 연장 |
Lazy 접근 가능 시점 | 서비스 계층 안 | 컨트롤러까지 가능 |
장점 | 성능 최적화, 명확한 제어 | 코드 간결, 초기 설정 쉬움 |
단점 | 복잡한 경우 조인 지옥 (카디널리티 곱 발생) | 커넥션 점유, 버그 유발 위험 |
추천 상황 | 명확한 DTO 응답, 성능 중요 | 간단한 API, 작은 규모에서 OK |
6. 실무에서 내가 겪은 상황
실제 내가 개발한 상품관리 시스템에서도 비슷한 고민이 있었다.
상품 리스트에 카테고리명, 등록자 정보 등 여러 연관 객체가 포함되어 있었고, 이를 JSON 응답으로 내려줘야 했다.
처음엔 Open-in-view를 열어두고 그대로 Entity → JSON 으로 넘겼지만, 다음과 같은 문제가 발생했다:
- 페이지가 많아질수록 커넥션 수가 급격히 증가
- 필드 하나만 접근해도 DB 쿼리가 발생해 예측이 어려움
- DTO로 바꾸려고 하자 Lazy 로딩 문제로 LazyInitializationException 발생
→ 결국 fetch join + DTO 변환 방식으로 변경했고, 그때부터 응답 속도와 안정성이 확연히 좋아졌다.
7. 결론: 내가 선택한 방식
실무에서는 다음과 같은 원칙을 정했다:
- API 응답은 무조건 DTO로 분리한다.
- 서비스 계층 내에서 필요한 모든 데이터를 명시적으로 가져온다.
(→ fetch join 또는 @EntityGraph 활용) - Open-in-view는 false로 명시적으로 꺼둔다. (개발할 때만 켜두기 ㅎㅎ)
(→ 커넥션 예측 가능성 확보)
결과적으로, 트랜잭션 경계가 명확해지고, 테스트와 유지보수가 쉬워졌다.
마무리하며..
- Lazy 로딩은 기본적으로 좋은 선택이지만, 언제 로딩될지 명확하지 않으면 위험하다.
- Open-in-view는 초기에 개발단계에선 편하지만, 운영으로 넘어가면서 꼭 꺼야한다.
- fetch join은 명시적으로 필요한 연관 데이터를 가져오고, 트랜잭션 안에서 DTO로 변환하는 구조가 가장 이상적이다.
'스터디 > 스프링' 카테고리의 다른 글
[Spring] Swagger에서 ErrorCode Enum 자동화하기 (1) | 2025.08.27 |
---|---|
[Spring] ApplicationEventPublisher에 대한 고찰 (feat. Fcm push) (1) | 2025.08.08 |
[Spring] 스프링 핵심 원리 기본편 | 1. 객체 지향 설계와 스프링 (0) | 2023.08.24 |
[Spring] 스프링 입문 | 5. 회원관리 예제 - 웹 MVC 개발 (0) | 2023.08.20 |
[Spring] 스프링 입문 | 4. 스프링 빈과 의존관계 (2) | 2023.07.06 |