1. 즉시 로딩의 두 종료
우선 즉시 로딩이란?
A 엔티티를 로딩할 때, A와 연결된 연관 엔티티 B도 함께 가져오는 것을 의미한다.
즉시 로딩을 구현하는 방법은 JPA에서 2가지로 구분할 수 있다.
| 방법 | 설명 | 예시 | 제어 가능성 |
| EAGER | 어노테이션으로 고정된 즉시 로딩 | @ManyToOne(fetch = EAGER) | 매우 제한적 |
| fetch join | JPQL 쿼리로 지정된 즉시 로딩 | JOIN FETCH | 동적으로 선택 가능 |
2. EAGER는 고정, fetch join은 선택 가능
EAGER는 선언한 시점부터 항상 즉시 로딩된다.
즉 우리가 어떤 쿼리를 쓰든 상관없이 해당 연관 엔티티는 무조건 같이 가져오게 된다.
@ManyToOne(fetch = FetchType.EAGER)
private User user;
위처럼 설정해두면 commentRepository.findById(1L)이든 .findBySchedule(...)이든 항상 user가 join되어 함께 로딩 된다.
문제는 이걸 제어할 수 없다는 점이다.
join이 필요하지 않은 쿼리에도 무조건 붙기 때문에 불필요한 데이터 로딩과 성능 저하, 예측 불가능한 SQL로 이어질 수 있다.
하지만 반대로 fetch join은 JPQL에서 명시적으로 join할 때만 즉시 로딩을 수행한다.
@Query("SELECT c FROM Comment c JOIN FETCH c.user WHERE c.schedule.id = :id")
List<Comment> findByScheduleWithUser(@Param("id") Long scheduleId);
이 경우에는 Comment와 User가 함께 로딩된다.
하지만 다른 쿼리에서는 명시하지 않는 한 로딩되지 않는다.
즉 fetch join은 "내가 원하는 시점에만 즉시 로딩을 명령할 수 있다."
3. 실제 예제로 살펴보기
엔티티 구성
@Entity
public class Comment {
@ManyToOne(fetch = FetchType.LAZY)
private User user;
@ManyToOne(fetch = FetchType.LAZY)
private Schedule schedule;
}
이 경우 user와 schedule은 모두 지연 로딩이다.
쿼리 1: 댓글만 필요할 때
@Query("SELECT c FROM Comment c WHERE c.schedule.id = :id")
List<Comment> findCommentsOnly(@Param("id") Long scheduleId);
➡️ 이 쿼리는 Comment만 로딩하고 User는 전혀 로딩하지 않는다.
➡️ comment.getUser()를 호출하는 순간에야 지연 로딩을 위해 SELECT 쿼리가 발생한다.
쿼리 2: 댓글과 작성자 이름도 필요할 때
@Query("SELECT c FROM Comment c JOIN FETCH c.user WHERE c.schedule.id = :id")
List<Comment> findCommentWithUSer(@Param("id") Long scheduleId);
➡️ JOIN FETCH가 명시돼 있으므로 Comment와 User가 같은 쿼리 안에서 즉시 로딩된다.
4. LAZY의 진짜 작동 방식
그렇다면 LAZY는 정확히 어떻게 동작할까?
@ManyToOne(fetch = FetchType.LAZY)
private USer user;
Comment comment = commentRepository.findById(1L).orElseThrow();
User user = comment.getUser(); // 아직 로딩 안 됨.
log.info(user.getUsername()); // 이 시점에 SELECT 발생
- 처음 comment.getUser()를 호출할 때는 프록시 객체가 반환된다.
- 진짜 DB 접근은 getUsername() 같은 구체적 필드에 접근할 때 발생한다.
- 이게 바로 프록시 기반 지연 로딩이다.
5. 즉시 로딩 판단 기준 - 언제 fetch join을 쓰고 안 쓰는가?
| 상황 | fetch join 사용 | 이유 |
| 댓글 목록 + 작성자 이름 | O | N+1 방지. 루프에서 연관 엔티티 접근 |
| 상세 페이지(게시글 + 작성자 + 댓글 + 댓글 작성자) | 일부만 | 여러 조인을 동시에 하면 중복 row, 성능 저하 발생 |
| REST API 응답 DTO | X | fetch join 대신 Projection 사용이 더 적절 |
| 페이징 처리(1:N) | X | 컬렉션 fetch join은 JPA 페이징과 충돌함 |
| 단건 조회 + 연관 필드 꼭 필요 | O | 그 시점에만 명확히 로딩 |
1) 댓글 목록 + 작성자 이름
상황
- 댓글 여러 개를 리스트로 불러와서 화면에 "댓글 내용 + 작성자 이름"을 보여줘야 함.
List<Comment> comments = commentRepository.findByScheduleId(id);
for (Comment c : comments) {
log.info(c.getUser().getUsername());
}
무슨 일이 일어나나? (LAZY라면)
- comments는 1번 쿼리로 다 가져온다.
- 근데 각 댓글의 .getUser()를 호출할 때마다 추가 쿼리 발생
-> 댓글 10개 -> 유저 쿼리 10번 -> 총 11번 쿼리 발생 = N + 1 문제
해결 : fetch join 사용
@Query("SELECT c FROM Comment c JOIN FETCH c.user WHERE c.schedule.id = :id")
-> 댓글 + 유저를 한 번의 쿼리로 로딩
-> .getUser() 호출해도 추가 쿼리 없음
결론
리스트에서 연관 엔티티에 접근할 예정이면, fetch join을 쓰자!
2) 상세 페이지( 게시글 + 작성자 + 댓글 + 댓글 작성자) -> 부분만 fetch join
상황
- 게시글 상세 페이지에서 게시글 내용, 게시글 작성자, 댓글 리스트, 각 댓글의 작성자 정보도 필요
문제
@Query("""
SELECT p FROM Post p
JOIN FETCH p.author
JOIN FETCH p.comments c
JOIN FETCH c.user
WHERE p.id = :id
""")
- 이렇게 여러 fetch join을 한 번에 쓰면, Post x Comment 수 만큼 row가 중복돼서 나온다.
- 댓글이 3개면 같은 Post가 3줄로 복제되어 메모리에 올라감
- 컬렉션에 중복 삽입되거나, 페이징이 이상하게 작동할 수도 있다.
해결
- Post와 Author만 fetch join 하고, 댓글은 @OneToMany 그대로 LAZY로 두고 따로 조회하거나
- 댓글만 별도 쿼리로 뽑아서 DTO로 합쳐주는 방식
결론
1:N 관계에서 동시에 여러 개를 fetch join하면 성능과 중복 문제 생김
-> 부분적으로만 사용하자!
3. REST API 응답 DTO -> Fetch join 사용 X
상황
- 백엔드에서 필요한 필드만 담은 DTO를 응답으로 내려주려함.
{
"commentId": 1,
"content": "좋아요",
"username": "유저1"
}
문제
- 굳이 엔티티 전체를 로딩할 필요 없다.
- fetch join으로 불러와도 결국 DTO로 변환하는데 낭비가 발생
- 연관 엔티티가 필요 없을 수도 있고, 단일 필드만 필요한 경우 많음
더 좋은 방법
@Query("""
SELECT new com.example.CommentResponse(c.id, c.content, u.username)
FROM Comment c JOIN c.user u
""")
List<CommentResponse> findCommentDtos();
-> 이렇게 DTO로 바로 가져오면 필요한 필드만 SELECT 하므로 성능, 용량 모두 최적화 가능
결론
API 응답을 위해 DTO만 필요할 때는 fetch join 말고 Projection(JPQL new) 쓰는게 낫다.
6. 결론 정리
- EAGER는 항상 즉시 로딩되며, 제어가 어렵다.
- fetch join은 JPQL에서만 명시적으로 작동하는 즉시 로딩이다.
- LAZY는 실제 코드에서 접근할 때까지 로딩하지 않으며, 성능 최적화에 유리하다.
- 실무에서는 대부분의 연관관계를 LAZY로 설정하고, 필요한 시점에 fetch join이나 @EntityGraph로 로딩을 제어한다.
'Back-End > JPA' 카테고리의 다른 글
| 트러블 슈팅 : 게시글 조회 시 조회수가 증가하지 않는 문제 (0) | 2025.02.04 |
|---|---|
| Spring Data JPA : Auditing (1) | 2024.12.18 |
| Spring Data JPA : 벌크성 수정 쿼리와 엔티티 그래프 (2) | 2024.12.18 |
| Spring Data Jpa : 페이징과 정렬 (1) | 2024.12.18 |
| Spring DataJpa : 쿼리 메서드 기능 (0) | 2024.12.18 |