docs(n+1liner): adopt the decomposition contract and strip evaluative prose
- 계약 채택 — schema 2 → 4. 독자 질문, 후보 36건(PROMOTE 24 · MERGE_INTO 8 · KEEP_IN_SSOT 4), 종류별 칸, kind:slug 관계. 저장소 github-project/ca-tmpl @ 761384d (문서가 인용한 IT 7종이 그 커밋에 있다) - 재선별 결과 기존 24건이 전부 살아남았다. 측정이 Reference 안에 들어 있었지만 독립성 검사를 통과하지 못해 Case 로 빼지 않았다 - 평문 칸의 백틱 제거, 「기준선」을 실제 이름으로, frontmatter 에 source·sourceRevision - 리뷰 39건 반영 — 설명 뒤에 붙은 평가·예고·되풀이·독자 오해 가정을 지웠다. 사실을 담은 절반이 있는 문장은 평가만 뺐다 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
b25357c48a
commit
62520a4dce
+33
-31
@@ -4,84 +4,86 @@ kind: QUESTION
|
||||
slug: cardinality-estimate-after-analyze
|
||||
title: ANALYZE 이후 Cardinality Estimate는 어떻게 달라지는가
|
||||
topic: jpa-feed-query-performance
|
||||
topicName: JPA 피드 조회 성능 — N+1 진단과 조회 전략의 진화
|
||||
topicName: JPA 피드 조회 성능
|
||||
project: Liner N + 1문제
|
||||
status: 게시 전
|
||||
studio: "https://hyeonworks.com/studio/documents/e1e0e2a0-c6b6-45bf-be42-f697ba5e2fff/edit"
|
||||
questionStatus: OPEN
|
||||
sourceRevision: n+1liner-lab@2026-08
|
||||
source:
|
||||
- final/document.md#6-4
|
||||
- final/document.md#4-6
|
||||
---
|
||||
|
||||
# ANALYZE 이후 Cardinality Estimate는 어떻게 달라지는가
|
||||
|
||||
반복되는 하이라이트 조회의 실행계획에서 추정 행수는 1이고 실제 행수는 500이었다. 대량 데이터를 넣은 직후 통계를 갱신하지 않아 편중을 담지 못했다는 가설을 세웠지만 아직 검증하지 않았다.
|
||||
플래너가 계획을 고르기 전에 조건에 걸리는 행이 몇 개일지 어림한 값이 추정 행수(cardinality estimate)이고, PostgreSQL은 `ANALYZE`로 컬럼 분포 통계를 모아 이 값을 낸다. 반복되는 하이라이트 조회의 실행계획에서는 추정 행수가 1이었는데 실제로는 500행이 나왔다. 대량 시드 직후 `ANALYZE`를 실행하지 않아 통계가 `feed_item_id`별 편중을 담지 못했다는 가설을 세웠지만 아직 검증하지 않았다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **PostgreSQL Query Plan 측정 기준**
|
||||
추정과 실제의 차이를 기록하는 기준이다.
|
||||
추정 행수와 실제 행수가 벌어지면 통계를 갱신한 뒤 다시 재고 전후를 비교하라고 적어 두었다.
|
||||
- **Fetch 타입이 아니라 조회 방식이 만든 ToOne N+1**
|
||||
이 실행계획이 나온 기록이다.
|
||||
이 실행계획을 `EXPLAIN`으로 확인하고 500배 차이를 처음 적었다.
|
||||
|
||||
## 사실
|
||||
|
||||
- 대량 시드 직후 측정한 계획에서 추정 rows는 1, 실제 rows는 500이었다. 500배 차이다.
|
||||
- 이 계획은 feed_item_id 조건을 인덱스로 처리했고 실행시간은 0.173 ms였다.
|
||||
- 읽은 블록은 모두 캐시에서 왔다. 디스크 읽기는 0이었다.
|
||||
- 하이라이트 개수는 순위 기반 편중 분포라 feed_item_id별 자식 수가 크게 다르다. 상한 500, 하한 1이다.
|
||||
- fetch join 조인 계획에서도 추정 4,202와 실제 1,961의 차이가 있었다.
|
||||
- 시드 직후 통계 갱신 명령을 실행하지 않았다.
|
||||
- 대량 시드 직후 잰 계획에서 추정 행수는 1인데 실제 행수는 500이었다. 500배 차이다.
|
||||
- 이 계획은 feed_item_id 조건을 ix_highlights_feed_items_created 인덱스로 처리했고(Index Scan) 실행시간은 0.173 ms였다.
|
||||
- 읽은 블록 14개가 모두 캐시에 있었고 디스크 읽기는 0이었다(shared hit=14, read=0).
|
||||
- 하이라이트 개수는 순위 기반 편중 분포라 feed_item_id마다 자식 행이 500개에서 1개까지 벌어진다.
|
||||
- fetch join 계획의 Hash Join 노드에서도 추정 rows=4202 에 실제 1,961로 어긋났다.
|
||||
- 대량 시드 직후 ANALYZE를 실행하지 않았다.
|
||||
|
||||
## 가정
|
||||
|
||||
- 통계를 갱신하면 feed_item_id별 분포가 반영되어 추정이 실제에 가까워진다.
|
||||
- 추정이 달라지면 플래너가 다른 계획을 고를 수 있다.
|
||||
- 통계를 갱신하면 feed_item_id별 분포가 반영되어 추정 행수가 실제에 가까워진다.
|
||||
- 추정 행수가 달라지면 플래너가 다른 계획을 고를 수 있다.
|
||||
- 편중이 큰 컬럼은 기본 통계 대상 수로 부족할 수 있다.
|
||||
|
||||
## 미지수
|
||||
|
||||
- 통계를 갱신한 뒤 추정 행수가 실제에 얼마나 가까워지는가.
|
||||
- 추정이 바뀌면 스캔 방식이 바뀌는가. 인덱스에서 순차 스캔으로, 또는 그 반대로 뒤집히는가.
|
||||
- 편중이 큰 컬럼에 통계 대상 수를 늘리면 추정이 더 좋아지는가.
|
||||
- 실행시간과 읽은 블록 수가 달라지는가.
|
||||
- 이 차이가 지금까지의 결론을 바꾸는가. 왕복 수와 전송 행수에 관한 판단은 통계와 무관하다.
|
||||
- 운영에서 대량 적재 후 통계 갱신을 절차에 넣을 것인가.
|
||||
- ANALYZE highlights를 실행하고 같은 조회를 다시 EXPLAIN하면 추정 행수가 1에서 얼마로 바뀌는가.
|
||||
- 추정 행수가 바뀌면 Index Scan이 Seq Scan으로, 또는 그 반대로 뒤집히는가.
|
||||
- highlights.feed_item_id의 통계 대상 수를 늘리면 추정 행수가 실제 500에 더 가까워지는가.
|
||||
- 실행시간 0.173 ms와 읽은 블록 14개가 달라지는가.
|
||||
- 통계를 갱신하면 지금까지의 결론 중 무엇이 바뀌는가. 왕복 수와 전송 행수에 관한 판단은 통계와 무관하다.
|
||||
- 운영에서 대량 적재 뒤 ANALYZE를 절차에 넣을 것인가.
|
||||
|
||||
## 제약
|
||||
|
||||
- 통계 갱신 전후를 비교하려면 같은 데이터에서 연속으로 재야 한다. 캐시 상태가 섞이면 비교가 흐려진다.
|
||||
- 지금까지 기록한 실행계획은 모두 갱신 전 값이다. 갱신 후 값과 섞어 읽지 않도록 표기를 구분해야 한다.
|
||||
- 실행하기 전에는 수치를 채우지 않는다.
|
||||
- 통계 갱신 전후를 비교하려면 같은 데이터에서 연속으로 재야 한다. 캐시 상태가 섞이면 읽은 블록 수를 나란히 놓을 수 없다.
|
||||
- 지금까지 기록한 실행계획은 모두 ANALYZE 전 값이다. 갱신 후 값과 섞이지 않도록 ANALYZE 전을 Plan A, 후를 Plan B로 나눠 적는다.
|
||||
|
||||
## 선택지
|
||||
|
||||
### 1. 통계를 갱신하고 전후를 비교한다
|
||||
|
||||
같은 데이터에서 갱신 전후 계획을 나란히 기록한다. 추정 행수, 스캔 방식, 읽은 블록, 실행시간을 대조한다.
|
||||
|
||||
측정이 한 번 더 필요하지만 가설을 닫을 수 있다.
|
||||
같은 데이터에서 ANALYZE highlights 전후 계획을 나란히 기록한다. 추정 행수, 스캔 방식, 읽은 블록, 실행시간을 대조한다.
|
||||
|
||||
### 2. 통계 대상 수까지 조절해 본다
|
||||
|
||||
편중이 큰 컬럼의 통계 대상 수를 늘린 뒤 다시 잰다. 기본값으로 부족한지 확인한다.
|
||||
highlights.feed_item_id의 통계 대상 수를 늘린 뒤 다시 잰다. 기본값으로 부족한지 확인한다.
|
||||
|
||||
변수가 하나 더 늘어 비교가 복잡해진다.
|
||||
변수가 하나 더 늘어 갱신 전후 비교와 통계 대상 수 비교가 섞인다.
|
||||
|
||||
### 3. 갱신 전 값만 두고 넘어간다
|
||||
|
||||
왕복 수와 전송 행수에 관한 결론은 통계와 무관하다. 추정 차이를 한계로만 적고 진행한다.
|
||||
|
||||
플래너가 다른 계획을 고를 가능성을 확인하지 못한 채 남는다.
|
||||
플래너가 다른 계획을 고르는지는 확인하지 못한다.
|
||||
|
||||
## 다음 검증
|
||||
|
||||
1. 대량 시드 직후 현재 계획을 다시 기록한다. 갱신 전 값임을 명시한다.
|
||||
1. 대량 시드 직후 계획을 다시 기록한다. ANALYZE 전 값이므로 Plan A로 적는다.
|
||||
|
||||
2. 통계를 갱신한다.
|
||||
2. ANALYZE highlights를 실행한다.
|
||||
|
||||
3. 같은 쿼리를 같은 실행 안에서 다시 EXPLAIN한다. 추정 행수, 스캔 방식, 읽은 블록, 실행시간을 기록한다.
|
||||
3. 같은 쿼리를 같은 테스트 실행 안에서 EXPLAIN (ANALYZE, BUFFERS)로 다시 잰다. 추정 행수, 스캔 방식, 읽은 블록, 실행시간을 기록한다.
|
||||
|
||||
4. 두 계획을 나란히 두고 무엇이 달라졌는지 적는다.
|
||||
4. Plan A와 Plan B를 나란히 두고 무엇이 달라졌는지 적는다.
|
||||
|
||||
5. 계획이 바뀌었다면 지금까지의 결론 중 영향을 받는 항목이 있는지 확인한다.
|
||||
|
||||
6. 운영 절차에 대량 적재 후 통계 갱신을 넣을지 판단한다.
|
||||
6. 운영 절차에 대량 적재 뒤 ANALYZE를 넣을지 판단한다.
|
||||
|
||||
+37
-34
@@ -4,85 +4,88 @@ kind: QUESTION
|
||||
slug: concurrency-stability
|
||||
title: 실제 동시 트래픽에서도 이 구조가 안정적인가
|
||||
topic: jpa-feed-query-performance
|
||||
topicName: JPA 피드 조회 성능 — N+1 진단과 조회 전략의 진화
|
||||
topicName: JPA 피드 조회 성능
|
||||
project: Liner N + 1문제
|
||||
status: 게시 전
|
||||
studio: "https://hyeonworks.com/studio/documents/6cbe963f-86f8-4df6-be5a-900712970d01/edit"
|
||||
questionStatus: OPEN
|
||||
sourceRevision: n+1liner-lab@2026-08
|
||||
source:
|
||||
- final/document.md#4-5
|
||||
- final/document.md#18
|
||||
---
|
||||
|
||||
# 실제 동시 트래픽에서도 이 구조가 안정적인가
|
||||
|
||||
지금까지의 측정은 단일 스레드 퍼시스턴스 통합 테스트에서 조회 횟수의 증가 형태를 확인한 것이다. 처리량, 커넥션 풀 안정성, 동시성은 이 측정의 범위 밖이라 조회 구조가 실제 부하를 견디는지 아직 모른다.
|
||||
지금까지의 측정은 단일 스레드 퍼시스턴스 통합 테스트에서 아이템 수 N을 키울 때 쿼리 수와 읽는 행수가 얼마나 늘어나는지만 확인했다. 여러 요청이 겹칠 때의 처리량과 커넥션 풀 안정성은 이 테스트가 재지 않았으므로, 조회 구조가 실제 부하를 견디는지는 아직 모른다.
|
||||
|
||||
한 요청만 놓고 보면 왕복 수는 페이지 크기에 묶여 있다. 그 요청당 왕복이 트래픽에 곱해진 값은 요청을 동시에 보내야 나오고, 그 측정은 하지 않았다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **PostgreSQL Query Plan 측정 기준**
|
||||
측정 범위와 도구 선택을 정한 기준이다.
|
||||
무엇을 어디까지 재고 어떤 도구로 잴지 그 기록에서 정했다.
|
||||
- **feed_visible을 Production CQRS로 승격할 것인가**
|
||||
부하 결과가 필요한 다른 판단이다.
|
||||
고트래픽 읽기에서 사전계산이 실제로 필요한지는 부하 결과를 봐야 정할 수 있다.
|
||||
|
||||
## 사실
|
||||
|
||||
- 측정은 단일 스레드에서 이미 열린 테스트 트랜잭션 안의 어댑터 호출만 쟀다. HTTP 종단을 거치지 않았다.
|
||||
- 지연 값은 warm cache 상태의 로컬 비교값이다. 표본은 7회 중 앞 2회를 버린 5개다.
|
||||
- 표본이 적어 백분위수 대신 중앙값과 최댓값으로 기록했다.
|
||||
- 왕복 수는 2,022개에서 23개로, 엔티티 로드는 1,569개에서 0개로, 자식 전송은 1,509행에서 최대 60행으로 줄었다.
|
||||
- 깊은 페이지 조회는 2,000행 대신 20행을 읽었다.
|
||||
- 문서 첫머리에 고트래픽 처리량, 커넥션 풀 안정성, 동시성이 범위 밖임을 명시했다.
|
||||
- 측정은 단일 스레드에서 이미 열린 테스트 트랜잭션 안의 어댑터 호출만 쟀고, HTTP 종단은 거치지 않았다.
|
||||
- 지연 값은 DB 캐시가 이미 채워진 상태에서 잰 로컬 비교값이고, 7회 반복 중 앞 2회를 워밍업으로 버린 5개가 표본이다.
|
||||
- 표본이 5개뿐이어서 p50·p99 대신 중앙값과 최댓값으로 적었다.
|
||||
- N=1,000에서 왕복 수는 2,022개에서 23개로, 엔티티 로드는 1,569개에서 0개로, 자식 전송은 1,509행에서 최대 60행으로 줄었다.
|
||||
- 100번째 페이지에서 OFFSET은 20행을 만들려고 2,000행을 읽었고 keyset은 20행만 읽었다.
|
||||
- 고트래픽 처리량과 커넥션 풀 안정성, 동시성은 이 측정의 범위 밖이라고 문서 첫머리에 적어 두었다.
|
||||
- 커넥션 풀과 Little's Law는 이번 조회 문제를 해결한 뒤 별도 주제로 검증할 수 있다고 다음 단계에 함께 적어 두었다.
|
||||
|
||||
## 가정
|
||||
|
||||
- 왕복 수가 줄면 같은 트래픽에서 DB 부하도 줄어든다.
|
||||
- 요청당 왕복이 처리량에 곱해지므로 왕복 감소는 처리량 한계를 올린다.
|
||||
- 단일 스레드에서 확인한 조회 형태는 동시 실행에서도 유지된다.
|
||||
- 요청마다 반복되는 왕복이 처리량에 곱해지므로, 왕복이 줄면 처리량 한계도 올라간다.
|
||||
- 단일 스레드에서 잰 요청당 쿼리 수와 읽는 행수는 여러 요청이 동시에 돌아도 달라지지 않는다.
|
||||
|
||||
## 미지수
|
||||
|
||||
- 목표 처리량에서 커넥션 풀이 포화되는가. 풀 크기와 대기 시간은 어떻게 되는가.
|
||||
- 동시 실행에서 지연 분포가 어떻게 되는가. 꼬리 지연이 어디까지 늘어나는가.
|
||||
- 배치 크기 설정이 동시 실행에서 어떻게 작동하는가. 세션마다 독립인가.
|
||||
- 깊은 페이지 요청이 섞이면 전체 지연에 어떤 영향을 주는가.
|
||||
- 캐시가 차갑거나 통계가 갱신되지 않은 상태에서 계획이 달라지는가.
|
||||
- 어느 지표를 운영 알람 기준으로 삼을 것인가.
|
||||
- 목표 처리량에서 커넥션 풀이 포화되는가. 포화되지 않으려면 풀을 몇 개로 잡아야 하고 요청은 얼마나 기다리는가.
|
||||
- 요청이 동시에 몰릴 때 지연이 어느 구간에 몰리는가. 꼬리 지연은 어디까지 벌어지는가.
|
||||
- hibernate.default_batch_fetch_size=100은 세션 전체에 걸리는 설정인데, 여러 세션이 동시에 도는 동안에도 세션마다 따로 적용되는가.
|
||||
- 깊은 페이지 요청이 섞이면 전체 지연이 얼마나 밀리는가.
|
||||
- DB 캐시가 비어 있거나 ANALYZE를 돌리지 않아 통계가 낡은 상태에서 실행계획이 달라지는가.
|
||||
- 운영 알람의 기준으로 어느 지표를 삼을 것인가.
|
||||
|
||||
## 제약
|
||||
|
||||
- 단일 스레드 값으로는 동시성 질문에 답할 수 없다. 도구를 바꿔야 한다.
|
||||
- 안정적인 꼬리 지연을 말하려면 워밍업 후 반복 횟수를 크게 늘린 독립 세트가 여러 개 필요하다.
|
||||
- 부하 테스트 환경이 운영과 다르면 결과를 그대로 옮길 수 없다. 데이터 규모와 하드웨어를 맞춰야 한다.
|
||||
- 로컬에서 확인한 것을 운영에서 확인한 것으로 승격하지 않는다.
|
||||
- 단일 스레드에서 잰 값으로는 동시 실행을 말할 수 없어서, 부하 테스트와 APM(Application Performance Monitoring, 애플리케이션 성능 모니터링)으로 도구를 바꿔야 한다.
|
||||
- 안정적인 꼬리 지연(p99)을 말하려면 워밍업 뒤 100회 넘게 반복한 독립 세트가 여러 개 있어야 한다.
|
||||
- 부하 테스트 환경의 데이터 규모와 하드웨어를 운영에 맞추지 않으면 거기서 나온 결과를 운영으로 그대로 옮길 수 없다.
|
||||
|
||||
## 선택지
|
||||
|
||||
### 1. 부하 테스트와 APM으로 확인한다
|
||||
|
||||
목표 처리량을 정하고 종단 지연, 처리량, 커넥션 풀 지표를 함께 잰다. 조회 구조의 개선이 부하에서도 나타나는지 본다.
|
||||
|
||||
환경 구성과 데이터 준비에 시간이 든다.
|
||||
목표 처리량을 먼저 정하고 HTTP 종단 지연과 처리량, 커넥션 풀 사용률을 함께 잰다. 왕복이 줄어든 만큼 처리량이 올라가는지 부하에서 확인한다.
|
||||
|
||||
### 2. 반복 횟수를 늘린 지연 측정부터 한다
|
||||
|
||||
같은 단일 스레드 조건에서 표본을 크게 늘려 꼬리 지연을 먼저 안정화한다. 동시성은 아직 다루지 않는다.
|
||||
|
||||
동시성 질문에는 여전히 답하지 못한다.
|
||||
같은 단일 스레드 조건에서 반복을 100회 넘게 늘려 꼬리 지연부터 안정시킨다. 동시성은 아직 다루지 않는다.
|
||||
|
||||
### 3. 현재 범위를 명시하고 운영 판단은 미룬다
|
||||
|
||||
조회 형태 개선까지만 주장하고 처리량은 다루지 않는다. 필요해질 때 부하 테스트를 연다.
|
||||
쿼리 수와 읽는 행수가 줄었다는 데까지만 주장하고 처리량은 다루지 않는다. 필요해지면 그때 부하 테스트를 연다.
|
||||
|
||||
운영에 올린 뒤 문제를 발견할 위험이 남는다.
|
||||
운영에 올린 뒤에야 문제를 발견할 수 있다.
|
||||
|
||||
## 다음 검증
|
||||
|
||||
1. 목표 처리량과 허용 지연을 먼저 정한다. 기준이 없으면 결과를 판정할 수 없다.
|
||||
1. 목표 처리량과 허용 지연을 먼저 정한다. 이 두 값이 없으면 부하 결과가 통과인지 실패인지 판정할 수 없다.
|
||||
|
||||
2. 운영과 비슷한 데이터 규모를 준비한다. 편중 분포를 유지한다.
|
||||
2. 운영과 비슷한 데이터 규모를 준비하되 하이라이트 개수의 Zipf 형태 편중은 그대로 둔다.
|
||||
|
||||
3. 부하 테스트로 종단 지연과 처리량을 측정한다. 커넥션 풀 사용률과 대기 시간을 함께 본다.
|
||||
3. 부하 테스트로 HTTP 종단 지연과 처리량을 측정하고, 커넥션 풀 사용률과 대기 시간을 함께 본다.
|
||||
|
||||
4. 개선 전후 구조를 같은 조건에서 비교한다. 왕복 감소가 처리량으로 이어지는지 확인한다.
|
||||
4. 개선 전후 구조를 같은 조건에서 비교해 왕복이 줄어든 것이 처리량으로 이어지는지 확인한다.
|
||||
|
||||
5. 깊은 페이지와 얕은 페이지 요청을 섞어 지연 분포를 본다.
|
||||
5. 깊은 페이지 요청과 얕은 페이지 요청을 섞어 지연이 어디까지 벌어지는지 본다.
|
||||
|
||||
6. 운영 알람으로 쓸 지표를 정한다.
|
||||
|
||||
+29
-24
@@ -4,81 +4,86 @@ kind: QUESTION
|
||||
slug: isolate-round-trip-and-row-volume
|
||||
title: Round Trip과 Row Volume을 독립 측정할 것인가
|
||||
topic: jpa-feed-query-performance
|
||||
topicName: JPA 피드 조회 성능 — N+1 진단과 조회 전략의 진화
|
||||
topicName: JPA 피드 조회 성능
|
||||
project: Liner N + 1문제
|
||||
status: 게시 전
|
||||
studio: "https://hyeonworks.com/studio/documents/5159c415-232d-424a-970a-b0db52746767/edit"
|
||||
questionStatus: OPEN
|
||||
sourceRevision: n+1liner-lab@2026-08
|
||||
source:
|
||||
- final/document.md#4-5
|
||||
- final/document.md#6-2
|
||||
---
|
||||
|
||||
# Round Trip과 Row Volume을 독립 측정할 것인가
|
||||
|
||||
현재 데이터셋은 N을 키우면 반환 부모 수, 자식 총 행수, 엔티티 생성량, DB 왕복이 동시에 늘어난다. 지연이 늘어난 원인을 어느 하나에 돌릴 수 없다. 변수를 하나씩 격리한 데이터셋을 만들지 결정하지 않았다.
|
||||
현재 데이터셋은 N을 키우면 반환 FeedItem 수, Highlight 총 행수, 엔티티 생성량, DB 왕복이 한꺼번에 늘어나서 지연이 늘어난 원인을 넷 중 어느 하나에 돌릴 수 없다. 변수를 하나씩 격리한 데이터셋을 만들지는 아직 정하지 않았다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **JPA N+1 정량 진단 기준**
|
||||
왕복과 행수를 다른 축으로 세는 기준이다.
|
||||
왕복 수와 전송 행수를 서로 다른 축으로 나눠 센다.
|
||||
- **Fetch 타입이 아니라 조회 방식이 만든 ToOne N+1**
|
||||
왕복 수와 한 번에 읽는 행 수를 같은 조회에서 확인한 기록이다.
|
||||
같은 조회에서 왕복 수와 한 번에 읽어 오는 행수를 함께 확인한다.
|
||||
|
||||
## 사실
|
||||
|
||||
- 하이라이트 개수는 순위 기반 편중 분포로 생성된다. 상한 500, 하한 1이다.
|
||||
- 하이라이트 총량은 N에 정비례하지 않는다. N=10에서 1,285개인데 N=1,000에서 2,917개다.
|
||||
- 조회 수는 하이라이트 총량이 아니라 부모 수 N에 정비례한다.
|
||||
- N을 키우면 반환 부모 수, 자식 총 행수, 엔티티 생성량, 왕복이 함께 늘어난다.
|
||||
- 지연은 단일 스레드에서 7회 반복하고 앞 2회를 버린 뒤 5개 표본의 중앙값과 최댓값으로 기록했다.
|
||||
- 하이라이트 총량은 N에 정비례하지 않는다. N=10에서 이미 1,285개이고 그중 500개가 첫 아이템 하나에서 나오며, N을 100배인 1,000으로 키워도 2,917개에 그친다.
|
||||
- 컬렉션 초기화 수(collectionFetches)는 하이라이트 총량이 아니라 FeedItem 수 N에 정비례한다.
|
||||
- 왕복은 FeedItem 수에 비례해 늘고, 전송 행수와 엔티티 생성은 한 FeedItem이 가진 Highlight 수에 비례해 늘어난다.
|
||||
- 지연은 단일 스레드에서 7회 반복하고 앞 2회를 워밍업으로 버린 뒤 남은 5개 표본의 중앙값과 최댓값으로 기록했다.
|
||||
- 격리 데이터셋 세 종류를 계획했지만 아직 실행하지 않았다.
|
||||
|
||||
## 가정
|
||||
|
||||
- 왕복 수와 전송 행수는 지연에 서로 다른 방식으로 기여한다.
|
||||
- 부모 수만 바꾸고 자식 수를 고정하면 왕복의 기여를 분리할 수 있다.
|
||||
- 부모 수를 고정하고 자식 수만 바꾸면 과조회의 기여를 분리할 수 있다.
|
||||
- 왕복 수와 전송 행수는 지연을 서로 다른 방식으로 늘린다.
|
||||
- FeedItem 수만 바꾸고 FeedItem마다 Highlight를 1개로 고정하면 왕복이 지연에 얼마나 붙는지 따로 잴 수 있다.
|
||||
- FeedItem 수를 고정하고 Highlight 수만 바꾸면 한 번에 읽어 오는 행수가 지연을 얼마나 늘리는지 따로 볼 수 있다.
|
||||
|
||||
## 미지수
|
||||
|
||||
- 격리 데이터셋을 추가로 유지할 가치가 있는가. 시더와 테스트가 늘어난다.
|
||||
- 부모 수만 바꾼 데이터셋에서 지연이 왕복 수에 선형으로 붙는가.
|
||||
- 자식 수만 바꾼 데이터셋에서 지연이 전송 행수에 어떻게 붙는가.
|
||||
- 두 기여를 분리해도 전략 선택이 달라지는가. 이미 배치와 프로젝션으로 둘 다 줄였다.
|
||||
- FeedItem 수만 바꾼 데이터셋에서 지연이 왕복 수에 선형으로 붙는가.
|
||||
- Highlight 수만 바꾼 데이터셋에서 지연이 전송 행수에 비례하는가, 아니면 어느 행수부터 더 가파르게 늘어나는가.
|
||||
- 왕복과 행수를 갈라 재도 전략 선택이 달라지는가. 이미 배치로 왕복을 2,022개에서 23개로, 프로젝션으로 엔티티 로드를 1,569개에서 0개로 줄였다.
|
||||
- 편중 분포를 유지한 데이터셋과 격리 데이터셋을 모두 유지할 것인가, 격리 데이터셋으로 대체할 것인가.
|
||||
|
||||
## 제약
|
||||
|
||||
- 현재 지연 값은 단일 스레드·warm cache 상대값이라 절대값 비교에 쓸 수 없다. 격리 데이터셋을 만들어도 이 한계는 그대로다.
|
||||
- 실행하기 전에는 수치를 채우지 않는다. 예상값으로 표를 메우지 않는다.
|
||||
- 시더가 복잡해지면 기존 측정의 재현성에 영향을 줄 수 있다. 기존 데이터셋은 유지한 채 추가해야 한다.
|
||||
- 현재 지연 값은 단일 스레드에서 같은 JVM으로, DB 캐시가 데워진 상태에서 잰 상대값이라 절대값 비교에 쓸 수 없다. 격리 데이터셋을 만들어도 측정 조건이 같아서 이 값으로는 여전히 N에 따른 증가 방향만 말할 수 있다.
|
||||
- 시더가 복잡해지면 지금까지 잰 값을 다시 재현하지 못할 수 있다. 기존 데이터셋은 그대로 두고 새 데이터셋을 더한다.
|
||||
- 편중 분포는 실제 라이너 데이터와 같다고 주장하려고 만든 것이 아니라 일부 페이지에 하이라이트가 매우 많을 수 있다는 조건을 통제된 방식으로 재현하려고 만든 합성 분포다. 하이라이트 개수를 균일하게 두면 한 아이템에 수백 개가 몰려서 생기는 전송량과 메모리 압박도, 아이템마다 최신 3개를 뽑아야 하는 필요도 데이터셋에서 사라진다.
|
||||
|
||||
## 선택지
|
||||
|
||||
### 1. 세 데이터셋을 모두 만든다
|
||||
|
||||
부모 수만 바꾼 것, 자식 수만 바꾼 것, 편중을 유지한 것 세 가지를 유지한다. 각 변수의 기여를 따로 볼 수 있다.
|
||||
FeedItem 수만 바꾼 것, Highlight 수만 바꾼 것, 편중 분포를 유지한 것 세 가지를 함께 둔다. 왕복과 전송 행수가 지연을 각각 얼마나 늘리는지 따로 잴 수 있다.
|
||||
|
||||
시더와 테스트가 늘어나고 실행 시간도 길어진다.
|
||||
|
||||
### 2. 편중 데이터셋만 유지하고 격리는 하지 않는다
|
||||
|
||||
전략 선택이 이미 정해졌다면 원인 분해가 결정을 바꾸지 않는다. 현재 데이터셋으로 회귀만 지킨다.
|
||||
전략 선택이 이미 정해졌다면 원인을 갈라도 결정이 바뀌지 않는다. 현재 데이터셋으로 회귀만 지킨다.
|
||||
|
||||
나중에 지연 원인을 따져야 할 때 다시 만들어야 한다.
|
||||
|
||||
### 3. 필요할 때만 한시적으로 만든다
|
||||
|
||||
특정 판단이 필요해지는 시점에 격리 데이터셋을 만들고 측정한 뒤 남기지 않는다.
|
||||
왕복과 행수를 갈라 봐야 하는 판단이 생겼을 때 격리 데이터셋을 만들어 측정하고 남기지 않는다.
|
||||
|
||||
측정 시점마다 시더를 다시 맞춰야 해서 재현성이 떨어진다.
|
||||
측정할 때마다 시더를 다시 맞춰야 해서 같은 값을 다시 얻기 어렵다.
|
||||
|
||||
## 다음 검증
|
||||
|
||||
1. 부모 수만 바꾼 데이터셋을 만든다. 부모마다 자식을 정확히 1개씩 둔다.
|
||||
1. FeedItem을 10 / 100 / 1,000으로 바꾸고 FeedItem마다 Highlight를 정확히 1개씩 두는 데이터셋을 만든다.
|
||||
|
||||
2. 부모 수를 고정하고 자식 수만 바꾼 데이터셋을 만든다.
|
||||
2. FeedItem을 20개로 고정하고 Highlight를 1 / 10 / 100 / 500으로 바꾸는 데이터셋을 만든다.
|
||||
|
||||
3. 두 데이터셋에서 왕복 수, 전송 행수, 지연을 각각 측정한다.
|
||||
|
||||
4. 지연이 어느 변수에 어떻게 붙는지 확인한다.
|
||||
4. 왕복 수가 늘 때와 전송 행수가 늘 때 지연이 각각 얼마나 늘어나는지 비교한다.
|
||||
|
||||
5. 분해 결과가 이미 내린 전략 선택을 바꾸는지 본다. 바꾸지 않는다면 격리 데이터셋을 상시 유지할 필요가 있는지 다시 판단한다.
|
||||
5. 갈라 잰 결과가 이미 내린 전략 선택을 바꾸는지 본다. 바꾸지 않는다면 격리 데이터셋을 상시 유지할지 다시 판단한다.
|
||||
|
||||
+31
-26
@@ -4,72 +4,77 @@ kind: QUESTION
|
||||
slug: nullable-first-highlighted-at
|
||||
title: Highlight 없는 FeedItem을 허용할 것인가
|
||||
topic: jpa-feed-query-performance
|
||||
topicName: JPA 피드 조회 성능 — N+1 진단과 조회 전략의 진화
|
||||
topicName: JPA 피드 조회 성능
|
||||
project: Liner N + 1문제
|
||||
status: 게시 전
|
||||
studio: "https://hyeonworks.com/studio/documents/b099ca65-bf9f-4d61-814c-74722453fa3c/edit"
|
||||
questionStatus: OPEN
|
||||
sourceRevision: n+1liner-lab@2026-08
|
||||
source:
|
||||
- final/document.md#3-1
|
||||
- final/document.md#14-4
|
||||
---
|
||||
|
||||
# Highlight 없는 FeedItem을 허용할 것인가
|
||||
|
||||
정렬키 first_highlighted_at이 nullable이라 하이라이트 없는 FeedItem이 존재할 수 있는 스키마다. 시더는 하이라이트가 만든 FeedItem만 넣어 항상 값이 차지만, 허용 여부를 정하지 않으면 정렬과 커서 비교식의 경계 동작이 정의되지 않는다.
|
||||
정렬키 first_highlighted_at은 지금 null을 허용한다. 하이라이트가 하나도 없는 FeedItem이 스키마 위에서는 존재할 수 있다는 뜻이다. FeedSeedFixture.seed(N)이 하이라이트가 만든 FeedItem만 넣다 보니 지금까지 쓴 데이터셋에는 이 컬럼이 빈 행이 없었다. 허용할지 말지를 정하기 전에는 정렬에서 그 행을 어디에 둘지도, 커서 비교식에 무엇을 담을지도 정할 수 없다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **Keyset Pagination 설계 기준**
|
||||
정렬키의 null 처리가 커서 설계에 걸리는 지점이다.
|
||||
정렬키에 null이 섞이면 여기서 세운 커서 설계가 그대로 성립하지 않는다.
|
||||
- **Visibility OR이 Keyset Index를 깨뜨린 문제**
|
||||
같은 정렬키 인덱스를 다루는 기록이다.
|
||||
거기서 플래너가 쓰지 못한 인덱스가 이 질문의 정렬키 인덱스와 같은 ix_feed_items_keyset이다.
|
||||
|
||||
## 사실
|
||||
|
||||
- 현재 스키마의 first_highlighted_at은 timestamptz nullable이다. NOT NULL이 아니다.
|
||||
- 시더는 하이라이트가 만든 FeedItem만 생성하므로 이 값을 항상 채운다. 그래서 지금까지의 측정에서는 null이 나타나지 않았다.
|
||||
- first_highlighted_at은 timestamptz이고 null을 허용한다. NOT NULL 제약이 붙어 있지 않다.
|
||||
- FeedSeedFixture.seed(N)은 하이라이트가 만든 FeedItem만 생성하므로 이 컬럼을 항상 채운다. 그래서 지금까지의 측정에서는 null이 나타나지 않았다.
|
||||
- FeedItem은 (user, page) 조합당 하나이고 UNIQUE(user_id, page_id) 제약이 있다.
|
||||
- keyset 커서는 (first_highlighted_at, id)를 비교식으로 사용한다.
|
||||
- 정렬키 전용 인덱스는 (first_highlighted_at DESC, id DESC)로 만들었다.
|
||||
- keyset 커서는 (first_highlighted_at, id)를 비교식에 담는다. 시각이 같은 행도 안정적으로 넘기려고 id까지 함께 넣었다.
|
||||
- 정렬키 전용 인덱스 ix_feed_items_keyset은 (first_highlighted_at DESC, id DESC)로 만들었다.
|
||||
|
||||
## 가정
|
||||
|
||||
- 하이라이트가 하나도 없는 FeedItem이 생기는 경로가 실제로 있을 수 있다.
|
||||
- 하이라이트가 하나도 없는 FeedItem이 실제로 만들어지는 경로가 도메인에 있을 수 있다.
|
||||
- null이 섞이면 커서 비교식이 경계에서 행을 빠뜨리거나 중복시킬 수 있다.
|
||||
- 정렬 위치를 정의하지 않으면 페이지를 넘길 때 순서가 흔들릴 수 있다.
|
||||
- 정렬 위치를 정하지 않으면 페이지를 넘길 때 순서가 흔들릴 수 있다.
|
||||
|
||||
## 미지수
|
||||
|
||||
- 하이라이트 없는 FeedItem을 만드는 경로가 도메인에 존재하는가. 존재한다면 어떤 상황인가.
|
||||
- NOT NULL로 좁힐 것인가, null을 허용하고 정렬 위치를 정의할 것인가.
|
||||
- null을 허용한다면 정렬에서 어디에 두는가. 그 위치를 인덱스가 지원하는가.
|
||||
- 커서가 null을 만났을 때 비교식을 어떻게 표현하는가.
|
||||
- 부분 인덱스 조건을 쓴다면 null 행이 인덱스에서 빠지는데 그 행은 어떻게 조회되는가.
|
||||
- FeedItem 생성 시점과 첫 하이라이트 시점이 다를 수 있는가.
|
||||
- 하이라이트 없는 FeedItem을 만드는 경로가 도메인에 있는가. 있다면 FeedItem을 만드는 코드 중 어느 것인가.
|
||||
- 스키마를 NOT NULL로 좁히는가, null을 허용하고 정렬 위치를 정하는가.
|
||||
- null을 허용한다면 정렬에서 맨 앞과 맨 뒤 중 어디에 두는가. 그 순서를 (first_highlighted_at DESC, id DESC) 인덱스가 그대로 주는가.
|
||||
- 커서에 담긴 시각이 null인 페이지를 넘길 때 (first_highlighted_at, id) 비교식에 무엇을 넣는가.
|
||||
- 부분 인덱스 조건을 쓰면 null 행이 인덱스에서 빠지는데, 그 행은 어느 쿼리로 읽는가.
|
||||
- FeedItem을 만든 시각과 첫 하이라이트가 생긴 시각이 다를 수 있는가.
|
||||
|
||||
## 제약
|
||||
|
||||
- 결정 시점은 keyset 페이징을 프로덕션에 반영하기 전이다. 커서 비교식과 인덱스 정의가 이 결정에 달려 있다.
|
||||
- 스키마를 NOT NULL로 좁히려면 기존 데이터에 null이 없어야 한다. 마이그레이션 전에 확인이 필요하다.
|
||||
- 현재 시더로는 이 경계가 재현되지 않는다. null이 섞인 데이터셋을 따로 만들어야 검증할 수 있다.
|
||||
- 커서 비교식과 정렬키 인덱스 정의가 이 결정에 달려 있어서, keyset 페이징을 프로덕션에 반영하기 전에 정해야 한다.
|
||||
- 스키마를 NOT NULL로 좁히려면 기존 데이터에 null이 하나도 없어야 하므로, 마이그레이션을 돌리기 전에 세어 봐야 한다.
|
||||
- 지금 데이터셋에는 null 행이 없어 이 경계가 재현되지 않는다. null이 섞인 데이터셋을 따로 만들어야 확인할 수 있다. 시더가 이 컬럼의 값을 시간에 흩어 놓은 것은 시간순 페이징과 정렬 인덱스를 실험할 바탕을 마련하려는 구성이다. 값이 비는 행을 만드는 것은 그 구성에 들어 있지 않다.
|
||||
|
||||
## 선택지
|
||||
|
||||
### 1. NOT NULL로 좁힌다
|
||||
|
||||
FeedItem이 항상 하이라이트와 함께 만들어진다면 정렬키를 NOT NULL로 정의한다. 커서 비교식이 단순해지고 인덱스도 그대로 쓸 수 있다.
|
||||
FeedItem이 항상 하이라이트와 함께 만들어진다면 정렬키에 NOT NULL을 건다. 커서는 지금처럼 (first_highlighted_at, id) 두 값만 비교하면 되고 인덱스도 정의를 바꾸지 않는다.
|
||||
|
||||
대신 하이라이트 없는 FeedItem을 만드는 경로가 나중에 필요해지면 스키마와 생성 흐름을 다시 바꿔야 한다.
|
||||
|
||||
### 2. null을 허용하고 정렬 위치를 정의한다
|
||||
|
||||
정렬에서 null을 어디에 둘지 명시하고 인덱스도 같은 위치로 만든다. 커서 비교식은 null 구간을 따로 다룬다.
|
||||
정렬에서 null을 맨 앞에 둘지 맨 뒤에 둘지 쿼리에 적고, 인덱스도 같은 순서로 만든다. 커서 비교식은 null 구간을 따로 다룬다.
|
||||
|
||||
생성 흐름은 자유로워지지만 커서 표현과 인덱스 정의가 복잡해진다.
|
||||
FeedItem을 만드는 흐름은 하이라이트 없이도 되지만, 커서에 무엇을 담을지와 인덱스를 어느 순서로 만들지를 둘 다 따로 정해야 한다.
|
||||
|
||||
### 3. 부분 인덱스로 null 행을 제외한다
|
||||
|
||||
정렬키가 있는 행만 인덱스에 담는다. 피드 목록에는 하이라이트가 있는 항목만 노출한다는 정책이 된다.
|
||||
정렬키가 채워진 행만 인덱스에 담는다. 피드 목록에 하이라이트가 있는 항목만 내보낸다고 정하는 것과 같다.
|
||||
|
||||
인덱스는 작아지지만 null 행을 조회하는 별도 경로가 필요하다.
|
||||
인덱스는 작아지지만 null 행을 읽는 쿼리를 따로 둬야 한다.
|
||||
|
||||
## 다음 검증
|
||||
|
||||
@@ -77,8 +82,8 @@ FeedItem이 항상 하이라이트와 함께 만들어진다면 정렬키를 NOT
|
||||
|
||||
2. 기존 데이터에 first_highlighted_at이 null인 행이 있는지 센다.
|
||||
|
||||
3. null이 섞인 데이터셋을 만들어 현재 커서 비교식이 경계에서 어떻게 동작하는지 재현한다.
|
||||
3. null이 섞인 데이터셋을 만들어 지금 커서 비교식이 경계에서 행을 빠뜨리는지 중복시키는지 재현한다.
|
||||
|
||||
4. 세 선택지 각각에서 커서로 넘긴 페이지가 OFFSET 페이지와 같은 행·같은 순서인지 대조한다.
|
||||
4. 세 선택지 각각에서 커서로 넘긴 페이지가 OFFSET 페이지와 같은 20행·같은 순서인지 FeedKeysetIT.l15KeysetWalkMatchesOffsetPages와 같은 방식으로 대조한다.
|
||||
|
||||
5. 정렬 위치를 정의한 경우 인덱스가 그 순서를 그대로 주는지 실행계획으로 확인한다.
|
||||
5. null 정렬 위치를 정한 경우 ix_feed_items_keyset이 그 순서를 그대로 주는지, 실행계획에 Sort가 다시 나오는지로 확인한다.
|
||||
|
||||
+40
-34
@@ -4,87 +4,93 @@ kind: QUESTION
|
||||
slug: promote-feed-visible-to-cqrs
|
||||
title: feed_visible을 Production CQRS로 승격할 것인가
|
||||
topic: jpa-feed-query-performance
|
||||
topicName: JPA 피드 조회 성능 — N+1 진단과 조회 전략의 진화
|
||||
topicName: JPA 피드 조회 성능
|
||||
project: Liner N + 1문제
|
||||
status: 게시 전
|
||||
studio: "https://hyeonworks.com/studio/documents/5088ce14-b096-41d3-abba-64b7afb48bb9/edit"
|
||||
questionStatus: OPEN
|
||||
sourceRevision: n+1liner-lab@2026-08
|
||||
source:
|
||||
- final/document.md#15-5
|
||||
- final/document.md#17-3
|
||||
- final/document.md#18
|
||||
---
|
||||
|
||||
# feed_visible을 Production CQRS로 승격할 것인가
|
||||
|
||||
사용자별 가시성을 미리 계산한 테이블은 조회를 커버링 인덱스 하나로 만들었다. 상시 유지하려면 원본 변경을 투영에 동기화해야 하고, 이는 별도 물리 읽기 저장소를 두는 결정이 된다.
|
||||
사용자별 가시성을 미리 펼친 feed_visible 테이블을 두자 피드 조회가 커버링 인덱스 하나만 읽는 단일 스캔으로 바뀌었다. 이 테이블을 상시 유지하려면 원본이 바뀔 때마다 투영을 갱신해야 하고, 그러면 쓰기와 분리된 물리 읽기 저장소를 두는 풀 CQRS(Command Query Responsibility Segregation, 명령과 조회를 다른 모델로 나누는 설계)로 넘어간다. 그 비용을 이번 범위에서 바로 받아들일지는 따로 판단해야 할 일로 두었다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **Feed Visibility Query Pattern**
|
||||
세 방식을 비교한 기준이다.
|
||||
단일 OR, UNION 분해, 사전계산 세 방식을 같은 결과 집합으로 비교했다.
|
||||
- **현재 Read Model은 CQRS-lite로 유지한다**
|
||||
지금 유지하기로 한 범위다.
|
||||
이번 범위에서는 저장소를 나누지 않고 읽기 모델만 분리하기로 했다.
|
||||
- **Visibility OR이 Keyset Index를 깨뜨린 문제**
|
||||
사전계산이 풀려던 문제다.
|
||||
keyset으로 푼 페이지 조회에 가시성 세 분기를 얹자 BitmapOr와 재정렬이 다시 나타났고, 사전계산은 이것을 없애려던 방식이다.
|
||||
|
||||
## 사실
|
||||
|
||||
- 사전계산 조회는 커버링 인덱스의 단일 스캔이었다. OR도 조인도 정렬도 없었다.
|
||||
- 단일 OR은 후보 1,500을 훑고 상위 20을 정렬로 만들었다. UNION 분해는 분기별로 스캔했다.
|
||||
- 세 방식은 같은 조회 사용자에게 같은 항목 집합을 반환했다.
|
||||
- 통합 쿼리에서 부모 선택을 사전계산으로 두면 깊은 페이지에서 인덱스 범위로 19행만 읽었다. 단일 OR로 두면 가시성 분기와 멘션 조건을 다시 계산하며 200행을 읽었다.
|
||||
- 현재 구현은 사전계산 테이블을 테스트 안에서 만들고 지운다. 상시 유지하지 않는다.
|
||||
- 현재 읽기 경로는 쓰기와 같은 저장소 위에 읽기 전용 포트·DTO·쿼리만 분리한 형태다.
|
||||
- 사전계산 조회는 feed_visible 커버링 인덱스 하나만 읽었다. OR도 조인도 정렬도 없이 20행에 접근했고 buffers는 1이었다.
|
||||
- 단일 OR은 세 분기를 BitmapOr로 합쳐 후보 1,500을 훑은 뒤 정렬로 상위 20을 냈고 buffers는 122였다. UNION 분해는 요청할 때마다 세 분기를 각각 스캔했고 buffers는 200으로 단일 OR보다 컸다.
|
||||
- 세 방식은 seed 2,000에서 뷰어 user008에게 같은 20개 항목을 반환했다.
|
||||
- 통합 쿼리에서 부모 선택을 사전계산으로 두면 가장 깊은 페이지에서 ix_feed_visible 인덱스 범위로 19행만 읽었다. 단일 OR로 두면 가시성 세 분기와 멘션 조건을 다시 계산하며 200행을 훑었다.
|
||||
- feed_visible은 FeedVisibilityIT와 FeedCrownIT 안에서 만들고 지우는 실험용 테이블이라 상시 유지하지 않는다.
|
||||
- 지금 읽기 경로는 쓰기와 같은 저장소를 쓰면서 읽기 전용 포트·DTO·쿼리만 분리한 CQRS-lite다.
|
||||
|
||||
## 가정
|
||||
|
||||
- 고트래픽 읽기에서는 조회 비용 차이가 실제 부하로 나타난다.
|
||||
- 상시 유지하면 원본 변경마다 투영 갱신이 필요하다.
|
||||
- 투영이 어긋나면 사용자가 볼 수 없는 항목을 보거나 볼 수 있는 항목을 놓친다.
|
||||
- 고트래픽 읽기에서는 세 방식의 조회 비용 차이가 실제 부하로 나타난다.
|
||||
- 상시 유지하면 피드·멘션·가시성이 바뀔 때마다 투영을 다시 써야 한다.
|
||||
- 투영이 어긋나면 사용자가 볼 수 없어야 할 항목이 화면에 나오거나 볼 수 있는 항목이 빠진다.
|
||||
|
||||
## 미지수
|
||||
|
||||
- 현재 트래픽에서 단일 OR이나 UNION 분해로 충분한가. 사전계산이 필요한 임계가 어디인가.
|
||||
- 동기화를 어떤 방식으로 하는가. 도메인 이벤트인가 아웃박스인가.
|
||||
- 투영 갱신이 늦어졌을 때 허용 가능한 지연은 얼마인가.
|
||||
- 지금 트래픽을 단일 OR이나 UNION 분해로 감당할 수 있는가. 사전계산이 필요해지는 읽기 요청량이 어디부터인가.
|
||||
- 쓰기 변경을 투영으로 어떻게 옮기는가. 도메인 이벤트로 보내는가, 아웃박스 테이블을 거치는가.
|
||||
- 투영 갱신이 늦어졌을 때 지연을 어디까지 허용하는가.
|
||||
- 가시성이 바뀌는 사건이 무엇인가. 아이템 공개 범위 변경, 멘션 추가·삭제, 사용자 삭제까지 포함하는가.
|
||||
- 사용자 수만큼 늘어나는 저장 공간이 감당 가능한가.
|
||||
- 투영이 어긋났을 때 어떻게 발견하고 복구하는가.
|
||||
- 이 변경이 현재 정한 계약의 범위를 넘는가. 넘는다면 계약과 가드레일을 어떻게 개정하는가.
|
||||
- 저장 공간은 사용자 수만큼 늘어나는데 그 크기를 감당할 수 있는가.
|
||||
- 투영이 원본과 어긋났을 때 무엇으로 발견하고 어떻게 되돌리는가.
|
||||
- 승격이 현재 계약의 범위를 넘는다는 것은 확인했고, 계약과 가드레일의 어느 조항을 어떻게 고칠지는 아직 정하지 않았다.
|
||||
|
||||
## 제약
|
||||
|
||||
- 현재 계약에서 별도 물리 읽기 저장소는 에스컬레이션 대상으로 남겨 두었다. 승격하려면 계약을 먼저 개정해야 한다.
|
||||
- 가시성은 보안에 걸린 조건이다. 투영이 어긋나면 노출 사고가 된다. 지연 허용치를 느슨하게 잡을 수 없다.
|
||||
- 지금까지의 측정은 단일 스레드 로컬 값이다. 고트래픽에서 어느 방식이 필요한지는 이 측정으로 답할 수 없다.
|
||||
- 현재 계약은 별도 물리 읽기 저장소를 에스컬레이션 대상으로 두었다. 승격하려면 계약을 먼저 개정해야 한다.
|
||||
- 가시성은 보안에 걸린 조건이라 투영이 어긋나면 볼 수 없어야 할 항목이 노출된다. 그래서 동기화 지연 허용치를 느슨하게 잡을 수 없다.
|
||||
- 지금까지 잰 값은 모두 단일 스레드 로컬 측정이라, 고트래픽에서 어느 방식이 필요한지는 이 값으로 답할 수 없다.
|
||||
|
||||
## 선택지
|
||||
|
||||
### 1. 현재 범위를 유지하고 요청 시 조회로 푼다
|
||||
|
||||
단일 OR이나 UNION 분해로 조회한다. 동기화 비용이 없고 정합성 문제도 없다.
|
||||
단일 OR이나 UNION 분해로 요청할 때마다 조회한다. 투영을 두지 않으니 동기화 비용도, 원본과 어긋날 위험도 없다.
|
||||
|
||||
고트래픽에서 조회 비용이 그대로 남는다.
|
||||
요청마다 가시성 세 분기를 다시 풀기 때문에 조회 비용은 줄지 않는다. 로컬에서 잰 buffers는 단일 OR 122, UNION 분해 200이었고 고트래픽에서 이 값이 어떤 부하가 되는지는 재지 않았다.
|
||||
|
||||
### 2. 사전계산을 상시 유지하는 읽기 저장소로 승격한다
|
||||
|
||||
쓰기 변경을 투영에 반영하고 조회는 투영만 읽는다. 조회 비용이 가장 낮다.
|
||||
쓰기 변경을 투영에 반영하고 조회는 투영만 읽는다. 세 방식 가운데 이것만 buffers 1로 20행을 읽었다.
|
||||
|
||||
동기화 경로, 지연 허용치, 정합성 검증, 복구 절차를 모두 설계해야 한다. 계약 개정도 필요하다.
|
||||
대신 동기화 경로, 지연 허용치, 정합성 검증, 복구 절차를 모두 설계해야 하고 계약도 함께 개정해야 한다.
|
||||
|
||||
### 3. 일부만 사전계산한다
|
||||
|
||||
접근이 잦은 구간만 투영으로 유지하고 나머지는 요청 시 조회한다.
|
||||
접근이 잦은 구간만 투영으로 유지하고 나머지는 요청할 때 조회한다.
|
||||
|
||||
두 경로를 함께 운영해야 하고 어느 구간을 투영에 둘지 정하는 기준이 필요하다.
|
||||
두 조회 경로를 함께 운영해야 하고, 어느 구간을 투영에 둘지 무엇을 보고 가를지 먼저 정해야 한다.
|
||||
|
||||
## 다음 검증
|
||||
|
||||
1. 부하 테스트로 현재 조회 방식이 목표 트래픽을 견디는지 확인한다. 이 판단은 단일 스레드 측정으로 대신할 수 없다.
|
||||
1. 부하 테스트로 지금의 조회 방식이 목표 트래픽을 견디는지 확인한다.
|
||||
|
||||
2. 가시성이 바뀌는 사건을 모두 열거하고 각각이 투영의 어느 행에 영향을 주는지 정리한다.
|
||||
2. 가시성이 바뀌는 사건을 모두 열거하고 각각이 투영의 어느 행을 건드리는지 정리한다.
|
||||
|
||||
3. 사용자 수와 아이템 수를 곱한 투영 크기를 계산한다.
|
||||
3. 사용자 수와 아이템 수를 곱해 투영이 몇 행까지 커지는지 계산한다.
|
||||
|
||||
4. 동기화 지연의 허용치를 정한다. 가시성은 보안 조건이므로 이 값이 설계를 좌우한다.
|
||||
4. 동기화 지연을 어디까지 허용할지 정한다. 가시성이 보안 조건이라 이 값을 정해야 동기화 방식을 고를 수 있다.
|
||||
|
||||
5. 투영과 원본이 어긋났는지 확인하는 방법과 복구 절차를 정의한다.
|
||||
5. 투영과 원본이 어긋났는지 확인하는 방법과 되돌리는 절차를 정의한다.
|
||||
|
||||
6. 위 결과를 보고 계약을 개정할지 판단한다.
|
||||
6. 위 다섯 가지 결과를 보고 계약을 개정할지 판단한다.
|
||||
|
||||
Reference in New Issue
Block a user