"조회는 무조건 DTO로 해야 할까?"
"fetchJoin을 언제 써야 하지?"실무에서 JPA를 쓰다 보면 조회 API를 어떻게 설계할지 고민이 많아집니다.
이 글에서는 **JPA 엔티티 연관관계 설정(LAZY vs EAGER)**과 함께
DTO select vs fetchJoin을 어떤 기준으로 선택해야 할지 정리해보겠습니다.
⚙️ 1. 엔티티 연관관계 매핑 기본 설정
JPA에서 엔티티 간 관계를 설정할 때 기본 전략은 다음과 같습니다.
✅ OneToMany / ManyToOne
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "category_id")
private Category category;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "emotion_id")
private Emotion emotion;
- 기본적으로 모든 연관관계는 LAZY로 설정하는 것이 권장
- 이유:
- 필요할 때만 쿼리 날리므로 성능 최적화
- EAGER는 항상 즉시 로딩 → 원치 않는 쿼리 발생 / N+1 문제 / 무한 루프 발생 가능
📌 참고: 실무에서는 EAGER는 거의 쓰지 않고, 모든 연관관계를 LAZY로 선언합니다.
🧩 2. LAZY일 때 발생하는 문제 – LazyInitializationException
Spring Boot에서 spring.jpa.open-in-view=false (OSIV 비활성화) 설정 시,
트랜잭션이 끝난 이후 Controller나 DTO 변환 시점에 record.getCategory().getName() 같은 LAZY 필드에 접근하면 예외가 발생합니다.
LazyInitializationException: could not initialize proxy - no Session
🛠️ 3. 이 문제를 해결하는 두 가지 전략
전략 설명
✅ DTO 직접 select | QueryDSL / JPQL에서 select new Dto(...) 방식으로 필요한 필드만 뽑음 |
✅ fetchJoin | 연관 객체를 쿼리에서 JOIN FETCH로 미리 로딩하여 세션 내에서 모두 로딩시킴 |
🔍 4. 상황별 판단 기준
상황 추천 방식 이유
단순 목록 조회 (홈 화면, 통계 등) | DTO 직접 select | 필요한 필드만 조회, 빠름 |
상세 화면 (카테고리 이름, 감정 아이콘 등 필요) | fetchJoin 후 DTO 변환 | 연관 객체 필드 접근 시 LazyException 방지 |
연관 객체 조작 (ex: setCategory, user.addRecord) | fetchJoin | 연관 객체가 실제 엔티티로 존재해야 함 |
출력용으로 연관 필드 하나만 필요 | DTO or fetchJoin → 상황 따라 | 출력 전용이면 DTO select, 도메인 사용이면 fetchJoin |
🧠 실전 예제 1: DTO select가 적합한 경우
List<RecordPreviewDto> records = queryFactory
.select(new QRecordPreviewDto(
record.id,
record.amount,
category.name,
record.date
))
.from(record)
.join(record.category, category)
.where(record.user.eq(user))
.fetch();
✔️ 필요한 필드만 가져오므로 빠르고 가벼움
❌ category 조작은 불가 (엔티티가 아님)
🧠 실전 예제 2: fetchJoin이 필요한 경우
Record record = queryFactory
.selectFrom(record)
.join(record.category, category).fetchJoin()
.join(record.emotion, emotion).fetchJoin()
.where(record.id.eq(recordId))
.fetchOne();
RecordResponseDto.from(record); // DTO 변환 중 category/emotion 필드 접근 안전
✔️ category.name, emotion.iconName 등 연관 필드 접근 가능
✔️ LazyException 없이 안정적
❌ 불필요한 조인 주의 (1:N은 중복 발생 가능)
📌 fetchJoin을 사용하면 생기는 이점
장점 설명
✅ LazyException 방지 | 트랜잭션 밖에서도 안전하게 접근 가능 |
✅ 도메인 조작 가능 | category.setSomething() 등 연관 객체 사용 가능 |
✅ 코드 가독성 ↑ | DTO 변환 시 별도 쿼리 호출 없이 필드 접근 가능 |
⚠️ fetchJoin의 주의점
- 1:N 관계에서는 중복 데이터 발생 → distinct, Set 처리 필요
- fetchJoin + 페이징 불가 → QueryDSL, Hibernate에서 경고
- 연관 관계가 많아질수록 쿼리 복잡도 ↑, 성능 악화 가능성 있음
✅ 결론: 실무 기준 요약
목적 추천 방식
단순 응답 (리스트, 요약) | DTO 직접 select |
상세 조회 (연관 객체 정보 필요) | fetchJoin 후 DTO 변환 |
연관 객체 조작이 필요한 경우 | fetchJoin |
API 응답에 연관 필드 접근만 필요한 경우 | 상황에 따라 선택 (DTO or fetchJoin) |
🪄 마무리 요약
- 💡 모든 연관관계는 LAZY로 매핑한다
- 💡 조회 시 DTO select와 fetchJoin은 목적에 맞게 구분한다
- 💡 fetchJoin은 성능에 유리하지만, 오용 시 쿼리 부하를 초래한다
- 💡 LazyException 방지를 위한 fetchJoin은 트랜잭션 내에서 DTO 변환까지 마치는 것이 핵심이다
🧾 참고 예시
// 월별 레코드 조회 시
List<Record> records = recordRepository.findWithCategoryAndEmotionByUserAndDateBetween(...);
// → fetchJoin 사용해 연관 정보 포함 → RecordResponseDto로 변환
// 카테고리 목록 출력 시
List<CategoryResponseDto> categories = categoryRepository.findCustomCategoryDtos(user);
// → DTO select로 필드만 추출
'Back-End > JPA' 카테고리의 다른 글
JPA EAGER vs LAZY 정리 (0) | 2025.08.02 |
---|---|
JPA Cascade Type 정리 (1) | 2025.07.28 |
JPA OSIV(Open Session In View) - 현업에서는 어떻게 사용할까? 🤔 (0) | 2025.07.13 |