2025. 11. 7. 09:52ㆍDev./Spring
상황 요약
단건 조회시 1:N 양방향 관계로 설정된 A 엔티티와 연결된 B 엔티티 목록을 함께 내려주는 기능을 구현해야 했다.
@Entity
public class Todo {
@OneToMany(mappedBy = "todo", fetch = FetchType.LAZY)
private List<Comment> comments = new ArrayList<>();
}
@Entity
public class Comment {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "todo_id")
private Todo todo;
}
Todo를 불러와 그 안의 todo.getComments()를 호출했을 때 다음과 같이 쿼리 두번이상 실행되는 문제가 발생했다.
select * from todos where id = 1;
select * from comments where todo_id = 1;
한 건만 조회한건데, 쿼리는 각자 따로따로 날림
원인
fetch = FetchType.LAZY
JPAsms 1:N 관계에서 지연로딩(Lazy loading)을 기본으로 한다.
Lazy (지연로딩) - 필요할 때까지 연관된 데이터를 DB에서 조회하지 않는다
왜 Lazy가 기본일까?
단순하게 성능 보호 때문
EAGER (즉시 로딩): 항상 join → 쓸데없는 조인 폭발
LAZY (지연 로딩): 실제로 필요할 때만 쿼리 실행
안전하게 기본값은 LAZY로 두고 정말 필요할 때 조인하는 것을 원칙으로 둔다.
Lazy Loading 상황 요약
- 처음 todoRepository.findById(1L) 시점에는 Todo만 로딩
- todo.getComments()를 호출하는 순간 Comment를 추가로 가져옴
즉 필요할때만 쿼리를 날려 성능상 안전한 기본 값이지만 관계가 있는 엔티티를 탐색하면서 불필요한 쿼리가 추가되는 것이다.
해결
1. @Query로 직접 해결
JPA의 커스텀 쿼리 메서드처럼 JPQL을 직접 명시적으로 작성할 수 있다.
@Query("SELECT DISTINCT t FROM Todo t LEFT JOIN FETCH t.comments WHERE t.id = :id")
Optional<Todo> findByIdWithComments(@Param("id") Long id);
직접 작성해줌으로써 한번의 쿼리로 조인이 가능
select distinct t.*, c.*
from todos t
left join comments c on t.id = c.todo_id
where t.id = ?;
2. @EntityGraph
@EntityGraph는 JPQL을 직접 쓰지 않고도 fetch join 효과를 주는 선언적 방법이다.
@EntityGraph(attributePaths = "comments")
Optional<Todo> findById(Long id);
- attributePaths는 즉시 로딩할 연관 필드를 지정
- JPA가 내부적으로 LEFT JOIN FETCH를 붙여 SQL을 생성
- 비즈니스 로직의 findById메서드를 발견하면 JpaRepository에도 있는 메서드인데? @EntityGraph 어노테이션을 보고 쿼리 실행시 JOIN FETCH comments 힌트를 주어 기존 findById에서 확장된 메서드로 재정의 한다.
select t.*, c.*
from todos t
left join comments c on t.id = c.todo_id
where t.id = ?;
SQL문은 위와 동일하다. 하지만 코드 가독성 측면에서 훨씬 좋음.
근데 왜 distinct 는 안찍힐까?
Hibernate가 알아서 해준다. SQL 결과가 중복되도 엔티티 식별자(PK) 기준으로 중복을 자동제거한다. DISTINCT 키워드 없이 1차 캐시에서 중복없는 엔티티 하나만 유지해 문제 없다.
결론
N+1 문제는 JPA가 연관되어있는 엔티티를 언제 가져오느냐의 설계 문제이다. 지연 로딩은 안전하지만 지금처럼 필요할 때에 @EntityGraph나 FETCH JOIN으로 즉시 연관 엔티티를 참조해오도록 해야한다.
'Dev. > Spring' 카테고리의 다른 글
| [Spring] JPA 영속성 컨텍스트 (0) | 2025.11.10 |
|---|---|
| [Spring] JPA 관계 설정(1:N/N:1) (0) | 2025.11.05 |
| [Spring] 수정 사항 발생시 자동 리빌드 Auto Reload (0) | 2025.11.04 |
| [내배캠] 한달 회고 (0) | 2025.10.31 |