--- id: 4e3200c8-eff5-4442-ae84-ae7b7fa92c8b kind: PROJECT_DECISION slug: query-strategy-behind-port title: Query Strategy는 FeedQueryPort 뒤에서 소유한다 topic: jpa-feed-query-performance topicName: JPA 피드 조회 성능 — N+1 진단과 조회 전략의 진화 topicName: JPA 피드 조회 성능 project: Liner N + 1문제 status: 게시 전 studio: "https://hyeonworks.com/studio/documents/4e3200c8-eff5-4442-ae84-ae7b7fa92c8b/edit" decisionStatus: PROPOSED sourceRevision: n+1liner-lab@2026-08 source: - final/document.md#5-2 --- # Query Strategy는 FeedQueryPort 뒤에서 소유한다 조회 전략은 퍼시스턴스 어댑터 `FeedQueryAdapter`의 책임으로 둔다. 상위 계층에 드러내는 것은 조회 사용자·페이지 크기와 반환할 `FeedSummary`뿐이고, fetch join·배치 페치·DTO 프로젝션·윈도우 함수 중 무엇을 쓰는지는 `FeedQueryPort` 뒤에 감춘다. ## 근거 - **Fetch Join · Batch · Projection 선택 기준** 여기서 비교한 세 전략을 모두 `FeedQueryAdapter` 안에서 갈아 끼웠다. - **Collection Fetch Join Pagination의 In-memory Paging** 컬렉션 fetch join에 페이징을 붙이자 Hibernate가 전체 조인 결과를 읽고 메모리에서 페이지를 잘랐는데, 이 실패도 어댑터 안에서 끝났다. - **화면 조회는 Read Projection을 사용한다** 화면 조회를 DTO 프로젝션으로 바꿀 때도 포트 뒤의 구현만 갈아 끼웠다. ## 결정문 조회 경로는 FeedController에서 GetFeedUseCase를 거쳐 FeedQueryPort로 이어지고, 퍼시스턴스 어댑터 FeedQueryAdapter가 그 포트를 구현한다. 조회 전략을 바꾸는 일은 이 어댑터 안에서 끝낸다. 쿼리 포트는 엔티티 타입을 노출하지 않는다. 컨트롤러는 엔티티를 의존하거나 반환하지 않는다. ## 판단 이유 이 프로젝트에서 조회 전략을 여섯 번 바꿨다. 엔티티 매핑, fetch join, 배치 페치, DTO 프로젝션, 윈도우 함수와 LATERAL, 커서 페이징이고, 바꿀 때마다 발행되는 SQL 형태와 반환 구조가 달라졌다. 전략이 상위 계층에 드러나 있었다면 여섯 번 모두 GetFeedUseCase와 웹 계층까지 함께 고쳐야 했다. 포트 뒤에 두었기 때문에 상위 계층은 그대로 두고 어댑터만 갈아 끼우며 비교할 수 있었다. FeedItemJpaEntity의 연관 게터를 public 대신 package-private로 좁힌 것도 같은 이유에서다. 연관 게터가 열려 있으면 상위 계층이 엔티티 객체 그래프를 타고 다니며 지연 로딩을 아무 데서나 촉발하거나 영속성 컨텍스트에 의존한다. 좁혀 두면 같은 패키지의 어댑터와 매퍼만 그래프를 순회할 수 있다. 빌드가 강제하는 규칙은 아니고 방어적 캡슐화 관례다. 쿼리 포트가 엔티티 타입을 노출하지 않으면 어댑터 안에서 조회 방식을 바꿔도 상위 계층이 보는 계약은 바뀌지 않는다. ## 영향 - 윈도우 함수와 LATERAL은 표준 JPQL(Java Persistence Query Language)로 표현되지 않아서 어댑터 안에 네이티브 SQL이 들어간다. 이 SQL은 포트 밖으로 나가지 않는다. - 화면 요구가 바뀌어 반환 형태가 달라지면 FeedQueryPort 계약까지 함께 바꿔야 한다. - 전략마다 나온 실패를 프로덕션 코드에 섞지 않고 통합 테스트 안에서만 재현했다. loadFeed를 건드리지 않고 loadFeedProjection을 따로 둔 덕분에 앞 단계를 다시 측정해 전후를 같은 조건으로 비교할 수 있었다. - 조회 성능의 책임은 FeedQueryAdapter가 모두 진다. 조회가 느려지면 이 어댑터 안부터 본다. - 쿼리 포트가 엔티티 타입을 노출하지 않는지와 의존 방향은 ArchUnit 검사(query_ports_do_not_leak…)가 확인한다. 이번 구현에서는 이 검사와 ./gradlew check가 모두 통과했다.