--- id: 5088ce14-b096-41d3-abba-64b7afb48bb9 kind: QUESTION slug: promote-feed-visible-to-cqrs title: feed_visible을 Production CQRS로 승격할 것인가 topic: jpa-feed-query-performance 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와 재정렬이 다시 나타났고, 사전계산은 이것을 없애려던 방식이다. ## 사실 - 사전계산 조회는 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 분해로 감당할 수 있는가. 사전계산이 필요해지는 읽기 요청량이 어디부터인가. - 쓰기 변경을 투영으로 어떻게 옮기는가. 도메인 이벤트로 보내는가, 아웃박스 테이블을 거치는가. - 투영 갱신이 늦어졌을 때 지연을 어디까지 허용하는가. - 가시성이 바뀌는 사건이 무엇인가. 아이템 공개 범위 변경, 멘션 추가·삭제, 사용자 삭제까지 포함하는가. - 저장 공간은 사용자 수만큼 늘어나는데 그 크기를 감당할 수 있는가. - 투영이 원본과 어긋났을 때 무엇으로 발견하고 어떻게 되돌리는가. - 승격이 현재 계약의 범위를 넘는다는 것은 확인했고, 계약과 가드레일의 어느 조항을 어떻게 고칠지는 아직 정하지 않았다. ## 제약 - 현재 계약은 별도 물리 읽기 저장소를 에스컬레이션 대상으로 두었다. 승격하려면 계약을 먼저 개정해야 한다. - 가시성은 보안에 걸린 조건이라 투영이 어긋나면 볼 수 없어야 할 항목이 노출된다. 그래서 동기화 지연 허용치를 느슨하게 잡을 수 없다. - 지금까지 잰 값은 모두 단일 스레드 로컬 측정이라, 고트래픽에서 어느 방식이 필요한지는 이 값으로 답할 수 없다. ## 선택지 ### 1. 현재 범위를 유지하고 요청 시 조회로 푼다 단일 OR이나 UNION 분해로 요청할 때마다 조회한다. 투영을 두지 않으니 동기화 비용도, 원본과 어긋날 위험도 없다. 요청마다 가시성 세 분기를 다시 풀기 때문에 조회 비용은 줄지 않는다. 로컬에서 잰 buffers는 단일 OR 122, UNION 분해 200이었고 고트래픽에서 이 값이 어떤 부하가 되는지는 재지 않았다. ### 2. 사전계산을 상시 유지하는 읽기 저장소로 승격한다 쓰기 변경을 투영에 반영하고 조회는 투영만 읽는다. 세 방식 가운데 이것만 buffers 1로 20행을 읽었다. 대신 동기화 경로, 지연 허용치, 정합성 검증, 복구 절차를 모두 설계해야 하고 계약도 함께 개정해야 한다. ### 3. 일부만 사전계산한다 접근이 잦은 구간만 투영으로 유지하고 나머지는 요청할 때 조회한다. 두 조회 경로를 함께 운영해야 하고, 어느 구간을 투영에 둘지 무엇을 보고 가를지 먼저 정해야 한다. ## 다음 검증 1. 부하 테스트로 지금의 조회 방식이 목표 트래픽을 견디는지 확인한다. 2. 가시성이 바뀌는 사건을 모두 열거하고 각각이 투영의 어느 행을 건드리는지 정리한다. 3. 사용자 수와 아이템 수를 곱해 투영이 몇 행까지 커지는지 계산한다. 4. 동기화 지연을 어디까지 허용할지 정한다. 가시성이 보안 조건이라 이 값을 정해야 동기화 방식을 고를 수 있다. 5. 투영과 원본이 어긋났는지 확인하는 방법과 되돌리는 절차를 정의한다. 6. 위 다섯 가지 결과를 보고 계약을 개정할지 판단한다.