feat: 문서 구조 변경 및 tech-visual 스킬 추가
This commit is contained in:
+48
@@ -0,0 +1,48 @@
|
||||
---
|
||||
id: 08a74b35-10c3-4874-8fbc-209b0b6e942e
|
||||
kind: PROJECT_DECISION
|
||||
slug: batch-fetch-for-entity-graph
|
||||
title: Entity Graph 조회에는 Batch Fetch를 사용한다
|
||||
topic: JPA 피드 조회 성능
|
||||
project: Liner N + 1문제
|
||||
status: 게시 전
|
||||
studio: "https://hyeonworks.com/studio/documents/08a74b35-10c3-4874-8fbc-209b0b6e942e/edit"
|
||||
decisionStatus: PROPOSED
|
||||
---
|
||||
|
||||
# Entity Graph 조회에는 Batch Fetch를 사용한다
|
||||
|
||||
엔티티를 그래프로 조회해야 하는 경로에서는 컬렉션 fetch join 대신 배치 페치를 쓴다. 엔티티만 페이징해 DB LIMIT을 살리고 지연 연관은 부모 키를 모아 IN으로 채운다.
|
||||
|
||||
## 근거
|
||||
|
||||
- **Fetch Join · Batch · Projection 선택 기준**
|
||||
세 전략의 역할을 나눈 기준이다.
|
||||
- **Collection Fetch Join Pagination의 In-memory Paging**
|
||||
fetch join과 페이징이 함께 서지 못하는 것을 확인한 기록이다.
|
||||
- **Projection 이후에도 1,509행을 읽은 Row Over-fetch**
|
||||
배치가 남긴 엔티티 과적재를 확인한 기록이다.
|
||||
|
||||
## 결정문
|
||||
|
||||
엔티티 그래프가 필요한 조회에서는 컬렉션을 fetch join하지 않고 배치 페치 크기를 설정해 지연 연관을 IN으로 묶는다.
|
||||
|
||||
배치 설정의 적용 범위를 명시한다. 세션 전체에 거는 설정은 기존 측정에 영향을 주므로 격리된 범위에 둔다.
|
||||
|
||||
## 판단 이유
|
||||
|
||||
fetch join은 부모와 자식을 한 결과에 합쳐 행을 곱했고, 그 때문에 DB가 부모 기준 LIMIT을 적용할 수 없었다. 배치는 부모만 먼저 페이징하고 자식은 별도 쿼리로 가져오므로 두 문제가 함께 풀린다.
|
||||
|
||||
측정에서 총 획득 statement가 크게 줄었다. 왕복은 부모 수를 배치 크기로 나눈 올림값이 된다. 컬렉션뿐 아니라 즉시 로딩 연관도 같은 배치에 묶였다.
|
||||
|
||||
로드한 부모 엔티티도 데이터셋 전체가 아니라 페이지 크기에서 멈췄다. 인메모리가 아니라 DB에서 LIMIT으로 부모를 먼저 자른 결과다.
|
||||
|
||||
실행계획에서도 부모 페이징에 Limit 노드가 붙고 자식 IN은 부모와 자식을 곱하지 않는 준조인으로 나타났다. 앞 단계에서 본 카테시안과 인메모리 페이징이 모두 사라졌다.
|
||||
|
||||
## 영향
|
||||
|
||||
- 지표 해석이 달라진다. 배치를 적용하면 초기화 컬렉션 수가 SQL 수와 같지 않다. 획득 statement 수와 컬렉션 수를 함께 보고 판단해야 한다.
|
||||
- 배치 크기 설정은 세션 전체에 영향을 준다. 기존 기준선 측정을 유지하려면 설정 범위를 격리해야 한다.
|
||||
- 엔티티는 여전히 통째로 하이드레이트된다. 화면에 필요하지 않은 컬럼까지 영속 객체로 올라온다. 이 비용은 배치가 풀지 않는다.
|
||||
- 배치 크기를 정해야 한다. 크기가 크면 IN 목록이 길어지고 작으면 왕복이 늘어난다.
|
||||
- 특정 연관에만 배치를 걸 수도 있지만 그러면 매핑 자체가 바뀐다. 기준선과 비교하려면 설정으로 두는 편이 낫다.
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
---
|
||||
id: 7f248f68-ce2b-43ec-94ce-82324d0bd1a7
|
||||
kind: PROJECT_DECISION
|
||||
slug: keep-read-model-as-cqrs-lite
|
||||
title: 현재 Read Model은 CQRS-lite로 유지한다
|
||||
topic: JPA 피드 조회 성능
|
||||
project: Liner N + 1문제
|
||||
status: 게시 전
|
||||
studio: "https://hyeonworks.com/studio/documents/7f248f68-ce2b-43ec-94ce-82324d0bd1a7/edit"
|
||||
decisionStatus: PROPOSED
|
||||
---
|
||||
|
||||
# 현재 Read Model은 CQRS-lite로 유지한다
|
||||
|
||||
읽기 경로를 쓰기 애그리거트와 분리하되 저장소는 나누지 않는다. 전용 조회 포트와 읽기 DTO, 읽기 최적 쿼리를 같은 저장소 위에 두고, 별도 물리 읽기 저장소는 에스컬레이션 대상으로 남긴다.
|
||||
|
||||
## 근거
|
||||
|
||||
- **feed_visible을 Production CQRS로 승격할 것인가**
|
||||
저장소 분리를 열어 둔 판단이다.
|
||||
- **화면 조회는 Read Projection을 사용한다**
|
||||
읽기 모델을 모델 수준에서 분리한 결정이다.
|
||||
- **Feed Visibility Query Pattern**
|
||||
사전계산이 어느 지점에서 읽기 모델 설계가 되는지 정리한 기준이다.
|
||||
|
||||
## 결정문
|
||||
|
||||
읽기 경로는 전용 조회 포트와 유스케이스, 어댑터로 분리한다. 읽기 최적 쿼리는 요청 시점에 실행하고 별도 물리 저장소를 두지 않는다.
|
||||
|
||||
사전계산 테이블을 상시 유지하는 구조는 현재 계약의 범위를 넘는 것으로 보고 채택하지 않는다.
|
||||
|
||||
## 판단 이유
|
||||
|
||||
문제가 N+1을 줄이는 SQL에서 화면에 맞는 읽기 모델을 설계하는 일로 넓어졌다. 쓰기 애그리거트로 읽기를 하려는 데서 조회 비용이 나왔기 때문이다.
|
||||
|
||||
읽기 모델을 분리하는 방법은 두 층이 있다. 모델만 분리하는 것과 저장소까지 분리하는 것이다. 모델 분리는 같은 저장소 위에서 전용 포트와 쿼리로 끝나고 동기화 비용이 없다.
|
||||
|
||||
저장소 분리는 조회 계획을 가장 단순하게 만들지만 쓰기 변경을 투영에 반영해야 한다. 가시성은 보안에 걸린 조건이라 투영이 어긋나면 노출 사고가 된다. 동기화 경로, 지연 허용치, 정합성 검증, 복구 절차를 모두 설계해야 한다.
|
||||
|
||||
현재 트래픽에서 그 비용이 필요한지 아직 확인하지 않았다. 지금까지의 측정은 단일 스레드 로컬 값이라 이 판단의 근거가 되지 못한다.
|
||||
|
||||
모델 분리만으로 하이드레이트 엔티티를 0으로, 발행 쿼리를 데이터 규모와 무관한 상수로 만들었다. 이 범위에서 얻을 수 있는 개선을 먼저 취했다.
|
||||
|
||||
## 영향
|
||||
|
||||
- 조회 요청마다 읽기 최적 쿼리가 실행된다. 사전계산 방식보다 조회 비용이 크다.
|
||||
- 가시성 조건은 요청 시점에 계산한다. 분기가 여럿이면 그 비용이 매 요청에 붙는다.
|
||||
- 읽기 전용 계약이 하나 늘어난다. 화면 요구가 바뀌면 이 계약도 바뀐다.
|
||||
- 저장소가 하나라 정합성 문제가 없다. 투영 갱신, 지연, 복구를 설계하지 않아도 된다.
|
||||
- 고트래픽 읽기에서 사전계산이 실제로 필요해지면 계약과 가드레일을 함께 개정해야 한다. 이 변경은 현재 범위를 넘는다.
|
||||
- 경계를 지키는 검사가 필요하다. 조회 포트가 엔티티를 노출하지 않는지, 의존 방향이 맞는지 확인한다.
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
---
|
||||
id: 1dbce381-f0dc-4d49-ad68-bd31d205677e
|
||||
kind: PROJECT_DECISION
|
||||
slug: keyset-for-feed-pagination
|
||||
title: Feed Pagination은 Keyset을 사용한다
|
||||
topic: JPA 피드 조회 성능
|
||||
project: Liner N + 1문제
|
||||
status: 게시 전
|
||||
studio: "https://hyeonworks.com/studio/documents/1dbce381-f0dc-4d49-ad68-bd31d205677e/edit"
|
||||
decisionStatus: PROPOSED
|
||||
---
|
||||
|
||||
# Feed Pagination은 Keyset을 사용한다
|
||||
|
||||
피드 목록의 페이징은 OFFSET이 아니라 이전 페이지의 마지막 정렬키를 커서로 넘기는 방식을 쓴다. 정렬키와 같은 컬럼·같은 방향의 인덱스를 함께 둔다.
|
||||
|
||||
## 근거
|
||||
|
||||
- **Keyset Pagination 설계 기준**
|
||||
이 결정을 규칙으로 편 기준이다.
|
||||
- **Visibility OR이 Keyset Index를 깨뜨린 문제**
|
||||
깊이별 비용과 인덱스 전제를 확인한 기록이다.
|
||||
- **Highlight 없는 FeedItem을 허용할 것인가**
|
||||
커서 설계가 기다리는 판단이다.
|
||||
|
||||
## 결정문
|
||||
|
||||
피드 목록은 (정렬 시각, 식별자)를 커서로 사용해 그 지점 이후만 조회한다. 정렬키 전용 인덱스를 같은 컬럼과 방향으로 둔다.
|
||||
|
||||
전체 페이지 수를 요구하지 않는 화면에서는 전체 건수 count를 발행하지 않는다.
|
||||
|
||||
## 판단 이유
|
||||
|
||||
OFFSET은 정렬 순서에서 앞의 행을 만든 뒤 버린다. 깊은 페이지에서는 결과 20행을 만들려고 2,000행을 읽었다. 무한 스크롤에서는 뒤로 갈수록 이 비용이 계속 늘어난다.
|
||||
|
||||
커서 방식은 읽는 행이 페이지 깊이와 무관하게 페이지 크기로 유지됐다. 정렬키 인덱스가 있을 때 커서 이후만 인덱스에서 읽었고 읽은 블록도 최소였다.
|
||||
|
||||
인덱스를 제거하면 커서 방식도 전량을 스캔했다. 문법이 아니라 인덱스가 비용을 줄인다. 기존 인덱스는 선두 컬럼이 달라 이 쿼리에 쓰이지 않았고 정렬키 전용 인덱스가 따로 필요했다.
|
||||
|
||||
정렬 시각이 같은 행을 안정적으로 넘기려면 식별자까지 커서에 담아야 한다. 시각만 쓰면 경계에서 행이 빠지거나 중복된다.
|
||||
|
||||
## 영향
|
||||
|
||||
- 임의 페이지로 점프할 수 없다. 앞뒤로 이어서 넘기는 탐색만 가능하다.
|
||||
- 전체 페이지 수를 화면에 표시할 수 없다. 필요하면 count를 별도로 다뤄야 한다.
|
||||
- 정렬 기준마다 인덱스가 필요하다. 정렬 기준이 늘면 인덱스 수와 쓰기 비용이 함께 는다.
|
||||
- 정렬키가 null일 수 있으면 정렬 위치와 커서 표현을 먼저 정의해야 한다. 이 판단이 아직 열려 있다.
|
||||
- 필터가 붙으면 인덱스 전제가 깨질 수 있다. 가시성 조건을 얹었을 때 정렬키 인덱스가 쓰이지 않고 정렬이 다시 생겼다. 필터를 포함한 설계가 따로 필요하다.
|
||||
- 커서를 클라이언트에 노출하므로 인코딩과 위변조 처리를 정해야 한다.
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
---
|
||||
id: ae6c9bea-d3a3-46e1-bbd4-8d580d336394
|
||||
kind: PROJECT_DECISION
|
||||
slug: measure-plan-on-real-postgresql
|
||||
title: Query Plan은 실제 PostgreSQL에서 측정한다
|
||||
topic: JPA 피드 조회 성능
|
||||
project: Liner N + 1문제
|
||||
status: 게시 전
|
||||
studio: "https://hyeonworks.com/studio/documents/ae6c9bea-d3a3-46e1-bbd4-8d580d336394/edit"
|
||||
decisionStatus: PROPOSED
|
||||
---
|
||||
|
||||
# Query Plan은 실제 PostgreSQL에서 측정한다
|
||||
|
||||
조회 성능 측정은 인메모리 대체 DB가 아니라 운영과 같은 PostgreSQL에서 실행한다. 실행계획과 인덱스 동작이 측정 대상이므로 DB는 대체재가 아니라 측정 대상의 일부다.
|
||||
|
||||
## 근거
|
||||
|
||||
- **PostgreSQL Query Plan 측정 기준**
|
||||
이 결정을 규칙으로 편 기준이다.
|
||||
- **Fetch 타입이 아니라 조회 방식이 만든 ToOne N+1**
|
||||
실제 엔진에서 실행계획과 통계 차이를 관측한 기록이다.
|
||||
- **Visibility OR이 Keyset Index를 깨뜨린 문제**
|
||||
부분 인덱스와 정렬 인덱스 기능에 기댄 측정 기록이다.
|
||||
|
||||
## 결정문
|
||||
|
||||
퍼시스턴스 조회 측정은 Testcontainers로 띄운 실제 PostgreSQL에서 수행한다. 인메모리 대체 DB로 실행계획이나 인덱스 동작을 판단하지 않는다.
|
||||
|
||||
스키마는 운영 마이그레이션을 그대로 적용하고 엔티티와의 불일치를 조기에 잡는다.
|
||||
|
||||
## 판단 이유
|
||||
|
||||
비용 기반 옵티마이저는 가능한 계획의 비용을 추정해 고른다. 그 추정값도, 고를 수 있는 선택지도 엔진마다 다르다. 비용 상수, 수집하는 통계, 저장 구조와 가시성 처리, 사용할 수 있는 인덱스 종류가 모두 갈린다.
|
||||
|
||||
이 프로젝트의 측정은 이 축들에 직접 걸린다. 추정 행수와 실제 행수가 500배 차이 난 관측은 통계 수집 방식에 달렸고, 순차 스캔과 인덱스 스캔의 판정은 비용 모델과 선택도 추정의 산물이며, 가시성 조건과 정렬 페이징은 부분 인덱스와 정렬 인덱스 기능에 기댄다.
|
||||
|
||||
다른 엔진에서 재면 스캔 방식 선택이 뒤집히고, 한쪽에만 있는 접근 경로가 통째로 사라지며, 그 엔진 특유의 동작이 재현되지 않는다. 세 지점에서 체계적으로 틀린 결론에 이른다.
|
||||
|
||||
측정 대상이 계획과 인덱스 동작인 이상 DB를 바꾸면 측정 자체가 달라진다.
|
||||
|
||||
## 영향
|
||||
|
||||
- 측정 실행에 컨테이너 런타임이 필요하다. Docker가 없는 환경에서는 이 테스트가 비활성화된다.
|
||||
- 인메모리 DB보다 기동과 실행이 느리다. 컨테이너를 클래스당 하나로 공유해 비용을 줄였다.
|
||||
- 재현성을 위해 이미지 태그보다 patch 버전이나 digest를 고정하는 편이 낫다. 같은 태그가 시점에 따라 다른 patch를 가리킬 수 있다.
|
||||
- 스키마 검증만으로 모든 드리프트를 막지 못한다. 인덱스 구성, 부분 인덱스 조건, check 제약, 외래키 정책은 따로 확인해야 한다.
|
||||
- 측정값은 warm cache 상태의 로컬 값이다. 운영 지연으로 옮겨 읽을 수 없다.
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
---
|
||||
id: 5e4d033c-d6fe-4257-a4dc-1ade44473c72
|
||||
kind: PROJECT_DECISION
|
||||
slug: no-collection-fetch-join-with-pagination
|
||||
title: Collection Fetch Join과 Pagination을 같이 사용하지 않는다
|
||||
topic: JPA 피드 조회 성능
|
||||
project: Liner N + 1문제
|
||||
status: 게시 전
|
||||
studio: "https://hyeonworks.com/studio/documents/5e4d033c-d6fe-4257-a4dc-1ade44473c72/edit"
|
||||
decisionStatus: PROPOSED
|
||||
---
|
||||
|
||||
# Collection Fetch Join과 Pagination을 같이 사용하지 않는다
|
||||
|
||||
컬렉션을 fetch join한 쿼리에 페이징을 걸지 않는다. Hibernate가 DB LIMIT을 빼고 결과셋 전체를 메모리에 올린 뒤 부모 기준으로 자르기 때문에, 응답은 한 페이지지만 비용은 데이터셋 전체에 비례한다.
|
||||
|
||||
## 근거
|
||||
|
||||
- **Collection Fetch Join Pagination의 In-memory Paging**
|
||||
이 동작을 실행계획과 로드 수로 확인한 기록이다.
|
||||
- **Fetch Join으로 N+1을 해결하다 만난 MultiBag과 행 폭증**
|
||||
컬렉션 fetch join이 행을 곱하는 것을 확인한 기록이다.
|
||||
- **Fetch Join · Batch · Projection 선택 기준**
|
||||
대신 무엇을 쓸지 정한 기준이다.
|
||||
|
||||
## 결정문
|
||||
|
||||
컬렉션을 fetch join하는 쿼리에 firstResult나 maxResults를 적용하지 않는다.
|
||||
|
||||
페이징이 필요한 목록 조회에서는 엔티티만 페이징해 DB LIMIT이 정상 발행되게 하고, 지연 연관은 배치나 별도 쿼리로 채운다.
|
||||
|
||||
## 판단 이유
|
||||
|
||||
컬렉션 fetch join에서는 부모 한 행이 자식 수만큼 늘어난다. 여기에 부모 기준 LIMIT을 걸면 조인 행에서 잘려 일부 부모의 자식이 누락된다.
|
||||
|
||||
Hibernate는 이 손상을 피하려고 SQL에서 LIMIT을 빼고 전체 조인 결과를 읽은 뒤 메모리에서 부모 기준으로 자른다. 발행된 SQL에 Limit 노드가 없는 것이 이 동작의 증거다.
|
||||
|
||||
측정에서 반환 목록은 페이지 크기로 고정됐지만 로드한 부모 엔티티는 데이터셋 전체였다. 초과 적재 배수는 데이터가 커질수록 늘었다. 작은 데이터셋에서는 두 값이 같아 문제가 드러나지 않는다.
|
||||
|
||||
엔티티만 페이징하면 Limit 노드가 정렬 위에 얹혀 상위 몇 행만 취하는 정렬로 바뀐다. 전체 정렬과 상위 몇 행 정렬의 차이가 계획 수준에서 나타난다.
|
||||
|
||||
## 영향
|
||||
|
||||
- 컬렉션을 한 번에 가져오는 편의를 포기한다. 자식 조회를 위한 쿼리가 따로 필요하다.
|
||||
- 엔티티만 페이징하면 지연 연관의 N+1이 돌아온다. 배치나 프로젝션을 함께 적용해야 한다.
|
||||
- 이 실수를 조기에 발견하려면 컬렉션 fetch join에 페이징이 걸릴 때 실패시키는 설정을 켤 수 있다. 근본 해결은 아니지만 안전장치가 된다.
|
||||
- 회귀 가드는 경고 코드 번호만 비교하지 않는다. 버전에 따라 코드가 달라질 수 있어 문구도 함께 확인한다.
|
||||
- 작은 데이터셋으로만 검증하면 이 문제를 놓친다. 데이터 규모를 바꿔 가며 반환 크기와 로드 수를 함께 봐야 한다.
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
---
|
||||
id: 4e3200c8-eff5-4442-ae84-ae7b7fa92c8b
|
||||
kind: PROJECT_DECISION
|
||||
slug: query-strategy-behind-port
|
||||
title: Query Strategy는 FeedQueryPort 뒤에서 소유한다
|
||||
topic: JPA 피드 조회 성능
|
||||
project: Liner N + 1문제
|
||||
status: 게시 전
|
||||
studio: "https://hyeonworks.com/studio/documents/4e3200c8-eff5-4442-ae84-ae7b7fa92c8b/edit"
|
||||
decisionStatus: PROPOSED
|
||||
---
|
||||
|
||||
# Query Strategy는 FeedQueryPort 뒤에서 소유한다
|
||||
|
||||
조회 전략은 퍼시스턴스 어댑터의 책임으로 둔다. 상위 계층에는 조회 조건과 반환 형태만 드러내고 fetch join, 배치, 프로젝션, 윈도우 함수 중 무엇을 쓰는지는 포트 뒤에 감춘다.
|
||||
|
||||
## 근거
|
||||
|
||||
- **Fetch Join · Batch · Projection 선택 기준**
|
||||
포트 뒤에서 교체한 전략들의 선택 기준이다.
|
||||
- **Collection Fetch Join Pagination의 In-memory Paging**
|
||||
전략을 바꿔 가며 실패를 격리한 기록이다.
|
||||
- **화면 조회는 Read Projection을 사용한다**
|
||||
같은 포트 뒤에서 구현을 바꾼 결정이다.
|
||||
|
||||
## 결정문
|
||||
|
||||
조회 경로는 컨트롤러에서 유스케이스를 거쳐 조회 포트로 이어지고, 퍼시스턴스 어댑터가 그 포트를 구현한다. 조회 전략의 변경은 어댑터 안에서 끝낸다.
|
||||
|
||||
포트는 엔티티 타입을 노출하지 않는다. 컨트롤러는 엔티티를 의존하거나 반환하지 않는다.
|
||||
|
||||
## 판단 이유
|
||||
|
||||
이 프로젝트에서 조회 전략을 여섯 번 바꿨다. 엔티티 매핑, fetch join, 배치, 프로젝션, 윈도우와 LATERAL, 커서 페이징이다. 전략마다 SQL 형태와 반환 구조가 달랐다.
|
||||
|
||||
전략이 상위 계층에 드러나 있었다면 매번 유스케이스와 웹 계층까지 함께 고쳐야 했다. 포트 뒤에 두었기 때문에 상위 계층은 그대로 두고 어댑터만 바꿔 가며 비교할 수 있었다.
|
||||
|
||||
엔티티 연관 게터를 좁게 열어 둔 것도 같은 경계다. 연관 게터가 열려 있으면 상위 계층이 객체 그래프를 타고 다니며 지연 로딩을 아무 데서나 촉발하거나 영속성 컨텍스트에 의존하게 된다.
|
||||
|
||||
포트가 엔티티를 노출하지 않으면 조회 방식이 바뀌어도 계약이 유지된다.
|
||||
|
||||
## 영향
|
||||
|
||||
- 어댑터 안에 네이티브 SQL이 들어간다. 표준 JPQL로 표현되지 않는 윈도우 함수와 LATERAL을 써야 하기 때문이다. 이 코드는 포트 뒤에 머문다.
|
||||
- 반환 형태를 바꾸려면 포트 계약을 바꿔야 한다. 화면 요구가 바뀌면 계약도 함께 바뀐다.
|
||||
- 전략별 실패를 프로덕션 코드에 섞지 않고 통합 테스트에 격리할 수 있었다. 다음 단계와 전후를 같은 기준으로 비교하는 데 필요했다.
|
||||
- 어댑터가 조회 성능의 책임을 모두 가진다. 성능 문제의 원인을 찾을 때 이 경계 안을 먼저 본다.
|
||||
- 경계를 지키는 검사를 자동화해야 한다. 포트가 엔티티를 노출하지 않는지, 의존 방향이 맞는지 확인하는 검사가 필요하다.
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
---
|
||||
id: 30a37f34-b406-4061-b924-e22e0be0c3bf
|
||||
kind: PROJECT_DECISION
|
||||
slug: read-projection-for-screen-query
|
||||
title: 화면 조회는 Read Projection을 사용한다
|
||||
topic: JPA 피드 조회 성능
|
||||
project: Liner N + 1문제
|
||||
status: 게시 전
|
||||
studio: "https://hyeonworks.com/studio/documents/30a37f34-b406-4061-b924-e22e0be0c3bf/edit"
|
||||
decisionStatus: PROPOSED
|
||||
---
|
||||
|
||||
# 화면 조회는 Read Projection을 사용한다
|
||||
|
||||
화면에 내보내는 조회는 엔티티를 하이드레이트하지 않고 필요한 스칼라 값만 캐리어로 받는다. 엔티티 그래프 조회는 쓰기 경로에 남기고 읽기 경로는 프로젝션으로 분리한다.
|
||||
|
||||
## 근거
|
||||
|
||||
- **Projection 이후에도 1,509행을 읽은 Row Over-fetch**
|
||||
프로젝션의 효과와 남은 비용을 확인한 기록이다.
|
||||
- **Fetch Join · Batch · Projection 선택 기준**
|
||||
배치와 프로젝션이 서로 다른 비용을 줄인다는 기준이다.
|
||||
- **Query Strategy는 FeedQueryPort 뒤에서 소유한다**
|
||||
이 구현을 감춘 경계다.
|
||||
|
||||
## 결정문
|
||||
|
||||
화면 조회 경로에서는 필요한 컬럼만 선택해 캐리어 record로 받는다. 영속 엔티티를 만들지 않는다.
|
||||
|
||||
부모와 자식을 각각 스칼라로 조회하고 애플리케이션에서 조립한다. 이 구현은 조회 포트 뒤에 둔다.
|
||||
|
||||
## 판단 이유
|
||||
|
||||
배치를 적용한 뒤에도 엔티티는 통째로 하이드레이트됐다. 페이지 20건을 조회하는데 부모와 연관을 합해 천 개가 넘는 영속 객체가 올라왔다. 화면에는 일부 컬럼만 필요했다.
|
||||
|
||||
캐리어 생성자 표현식은 영속 엔티티 대신 스칼라 값으로 record를 만든다. 1차 캐시, 더티체킹, 지연 프록시가 생기지 않는다. 컬럼을 읽기 위한 조인이 있어도 그 대상 엔티티를 만들지 않는다.
|
||||
|
||||
측정에서 하이드레이트한 엔티티가 0이 됐고 발행 쿼리도 데이터 규모와 관계없이 두 개로 고정됐다. 부모 스칼라 쿼리와 자식 IN 쿼리다.
|
||||
|
||||
이 효과는 배치 설정 여부와 무관하게 성립한다. 배치는 왕복을 줄이고 프로젝션은 적재를 없앤다. 두 전략은 서로를 대신하지 않는다.
|
||||
|
||||
## 영향
|
||||
|
||||
- 반환 형태가 화면 요구에 묶인다. 화면이 바뀌면 캐리어와 쿼리도 바뀐다.
|
||||
- 여러 컬렉션을 담는 응답은 생성자 표현식 한 번으로 만들 수 없다. 부모와 자식을 따로 조회해 조립해야 한다.
|
||||
- 프로젝션의 이득은 실행계획에서 확인되지 않는다. 필요한 컬럼만 골라도 계획의 행폭 추정치는 오히려 넓어질 수 있다. 엔티티 로드 수로 확인해야 한다.
|
||||
- 자식 조회의 행수는 프로젝션이 줄이지 않는다. 부모당 상한이 필요하면 별도 SQL 형태로 풀어야 한다.
|
||||
- 읽기 경로와 쓰기 경로의 모델이 갈린다. 같은 저장소를 쓰더라도 조회 전용 계약이 하나 늘어난다.
|
||||
Reference in New Issue
Block a user