--- id: db99cbc5-9123-4599-b368-39ff3170e81d kind: REFERENCE slug: fetch-strategy-selection title: Fetch Join · Batch · Projection 선택 기준 topic: jpa-feed-query-performance topicName: JPA 피드 조회 성능 project: Liner N + 1문제 status: 게시 전 studio: "https://hyeonworks.com/studio/documents/db99cbc5-9123-4599-b368-39ff3170e81d/edit" --- # Fetch Join · Batch · Projection 선택 기준 세 전략은 서로 다른 비용을 줄인다. fetch join은 왕복을 접지만 행을 곱하고, batch는 왕복을 묶지만 엔티티를 그대로 만들고, 프로젝션은 적재를 없애지만 행수를 줄이지 않는다. ## 관계 - **Fetch Join으로 N+1을 해결하다 만난 MultiBag과 행 폭증** fetch join의 한계를 확인한 기록이다. - **Collection Fetch Join Pagination의 In-memory Paging** fetch join과 페이징이 함께 서지 못하는 것을 확인한 기록이다. - **Projection 이후에도 1,509행을 읽은 Row Over-fetch** 프로젝션이 남기는 비용을 확인한 기록이다. ## 목적 쿼리 수만 보고 전략을 고르면 비용이 다른 축으로 옮겨 간 것을 놓친다. 컬렉션 fetch join은 쿼리 수를 크게 줄이면서 전송 행수와 메모리를 키운다. 무엇을 줄이려는지 먼저 정하고 그 축을 재는 지표로 전후를 비교한다. ## 규칙 ### 1. 컬렉션 fetch join은 두 개 이상 쓰지 않는다 순서 컬럼이 없는 List 두 개를 동시에 fetch join하면 곱집합을 원래 컬렉션으로 되돌릴 수 없어 쿼리 생성 시점에 거부된다. 데이터가 0건이어도 발생하는 매핑 단계의 거부다. ### 2. 컬렉션 fetch join은 행을 곱한다 컬렉션 하나만 fetch join해도 부모 한 행이 자식 수만큼 반복된다. 전송 행수는 자식 총합이 된다. Hibernate 6 이상은 루트 엔티티를 자동으로 중복 제거하므로 결과 리스트 크기로는 이 증가가 보이지 않는다. 조인 카디널리티나 실행계획의 actual rows로 확인한다. ### 3. 컬렉션 fetch join과 페이징을 같이 쓰지 않는다 부모 기준 LIMIT을 걸면 조인 행에서 잘려 일부 부모의 자식이 누락된다. Hibernate는 이를 피하려고 SQL에서 LIMIT을 빼고 전체를 읽은 뒤 메모리에서 자른다. 응답은 한 페이지지만 로드한 부모는 전체다. 발행 SQL에 Limit 노드가 없는 것이 이 동작의 증거다. ### 4. fetch join은 ToOne에 쓴다 ToOne은 행을 곱하지 않는다. 루트 SQL에 합쳐도 카테시안이 생기지 않으므로 fetch join이 적합하다. ### 5. 컬렉션에는 batch fetch를 쓴다 엔티티만 페이징해 DB LIMIT이 정상 작동하게 한 뒤, 지연 연관은 부모 키를 모아 IN으로 채운다. 배치 크기가 B면 왕복은 부모 수를 B로 나눈 올림값이 된다. 배치는 부모와 자식을 곱하지 않는다. 실행계획에서 semi-join으로 나타난다. ### 6. 화면 조회에는 프로젝션을 쓴다 필요한 스칼라 값만 조회하면 영속 엔티티를 만들지 않는다. 1차 캐시, 더티체킹, 지연 프록시도 생기지 않는다. join이 있어도 컬럼을 읽기 위한 경로일 뿐 엔티티를 만들지 않는다. 이 동작은 배치 설정 여부와 관계없이 성립한다. ### 7. 프로젝션의 효과는 실행계획이 아니라 ORM 층에서 확인한다 필요한 컬럼만 골라도 EXPLAIN의 width가 줄지 않을 수 있다. 조인 대상의 행폭이 반영되고 width가 실제 전송 바이트가 아니라 타입의 평균폭 추정치이기 때문이다. 프로젝션의 이득은 엔티티 로드 수로 확인한다. ### 8. 세 전략이 남기는 비용을 적는다 fetch join은 행 폭증과 페이징 불가를 남긴다. batch는 엔티티 과적재를 남긴다. 프로젝션은 부모당 자식 전량 조회를 남긴다. 남은 비용을 적어야 다음 단계가 무엇을 풀어야 하는지 이어진다. ## 적용 조건 - 연관을 포함한 목록 조회를 설계할 때 - N+1을 확인하고 fetch 전략을 고를 때 - 전략을 바꾼 뒤 무엇이 줄고 무엇이 남았는지 정리할 때 ## 예외 - 컬렉션이 하나이고 페이징이 없으며 자식 수가 작다면 컬렉션 fetch join이 단순하다. 자식 수가 커질 수 있는 구조에는 쓰지 않는다. - 배치 크기 설정은 세션 전체에 영향을 준다. 기존 측정을 유지하려면 별도 설정 범위로 격리한다. ## 예시 - 컬렉션 두 개 fetch join : 쿼리 생성 시점 거부 - 컬렉션 한 개 fetch join : 전송 행수 = 자식 총합 - 컬렉션 fetch join + 페이징 : DB LIMIT 없음, 부모 전체 로드 - ToOne fetch join : 행 곱하지 않음, 적합 - batch fetch : 왕복 = 부모 수 / 배치 크기 올림 - 프로젝션 : 엔티티 로드 0, 쿼리 상수, 자식 행수는 그대로