01 Measure the actual path
DB가 빠른데 HTTP는 느린 이유부터.
초기 staging 시험에서 응답의 꼬리 지연이 길게 나왔지만, DB 함수 실행 시간은 훨씬 짧았습니다. 직접 Data API를 호출한 시험과 실제 React Router 앱 경로도 달랐습니다. 그래서 SQL만 바꾸기 전에, 무엇을 측정한 결과인지부터 구분했습니다.
앱에는 Server-Timing을 넣어 인증과 RPC 시간을 기록했습니다. 하네스에서는 DM 전송과 읽음을 별도 phase로 나누고, 전체 HTML 대신 실제 내부 이동에 사용하는 .data 요청을 측정했습니다. 부하 발생기의 시작·요청 간격에 jitter를 주고 CPU·event-loop·오류·429도 함께 기록했습니다.
Vercel Function 리전은 DB와 가까운 서울 icn1으로 맞췄습니다. 리전 변경 전후에는 실행 조건도 달라졌으므로 이 기록으로 지역 설정 하나의 개선율을 계산하지는 않습니다.
02 Query plan
인덱스의 존재보다, 검색식과의 일치.
50,000개 합성 프로필의 특정 문자열 검색에서, nickname·handle·bio의 OR 조건은 순차 스캔으로 실행됐습니다. 세 컬럼에 각각 인덱스를 두는 시도만으로는 planner가 원하는 경로를 선택하지 않았습니다.
검색 대상 문자열을 결합한 하나의 식으로 조회 조건과 GIN trigram 인덱스를 맞췄습니다. 당시 저장된 실행 출력에서 순차 스캔 계획의 최상위 실행 시간은 약 83.1ms, 변경 후 EXPLAIN 실행 시간은 2.546ms였고 profiles_directory_search_trgm_idx 사용을 확인했습니다.
03 Authentication round trip
Navigation 조회마다 필요했던 외부 왕복을 줄이기.
알림·메뉴 조회는 최신 사용자 레코드를 사용하지 않는데도 매 요청에서 getUser()로 Auth 서버를 확인했습니다. 이 읽기 경로를 getClaims(jwt) 검증으로 바꾸고, verifier 클라이언트를 프로세스당 하나만 유지해 JWKS 캐시를 재사용하도록 했습니다.
비대칭 서명키 환경에서 정상 요청마다 발생하던 Auth HTTP 왕복을 줄이는 선택입니다. 쿠키의 user 객체를 그대로 신뢰하는 방식은 아니며, 토큰 검증 이후 데이터 권한은 authenticated RPC·RLS가 제한합니다. 다만 로컬 claims 검증은 서버의 최신 세션 철회를 매번 확인하는 것과 같지 않아 적용 범위를 구분했습니다.
| 부하 | 변경 전 p99 | 후속 p99 |
|---|---|---|
| 20 VU | 729ms | 181ms |
| 50 VU | 1,253ms | 492.1ms |
후속 50 VU 실행은 1,065요청에서 오류·429가 없었습니다. 이는 인증 왕복 제거 후 관측된 변화입니다. 20초의 짧은 Preview 실행이고 프로세스 warm 상태 등도 달라질 수 있어, 엄밀한 통제 A/B나 모든 사용자에 대한 개선율로 일반화하지 않습니다.
04 Reduce repeated work
왕복 수와 매번 다시 세는 비용을 따로 줄이기.
Navigation은 여러 조회를 get_navigation_state RPC로 모아 fan-out을 줄였습니다. 이어서 전체 DM 이력을 반복 집계하는 대신 사용자·대화별 unread counter를 저장했습니다. 메시지 삽입과 읽음·차단·대화 상태 변경이 이 집계를 갱신하고, 메뉴는 저장된 카운터를 읽습니다.
이는 읽기 비용을 줄이는 대신 집계 정합성을 유지할 책임이 생기는 선택입니다. 원본 메시지와 읽음 기록을 기준으로 어긋난 카운터를 확인·보정하는 경로도 두었습니다. 브라우저는 실시간 신호를 받을 때 전체 페이지를 재실행하지 않고 필요한 알림·DM resource를 다시 조회합니다.
후속 검색 gate는 입력·쿼리 비용 제한에 더해 프로세스별 실행 수와 대기열을 제한합니다. 별도 Discord 경로에서는 프로필 렌더링을 인스턴스당 1개 실행·4개 대기로 묶고 동일 interaction의 진행 중 작업을 공유했습니다. 이 제한들은 서비스 전체의 분산 동시성 보장과 구분해야 합니다.
05 Distributed measurement
짧은 공유 세션 시험에서, 고유 세션의 30분 시험으로.
후속 Navigation 시험은 GitHub runner를 나누고 가상 사용자마다 고유 인증 세션을 사용했습니다. 아래는 Navigation HTTP 경로의 당시 기록입니다. 신규 OAuth 가입이나 전체 서비스 E2E 시험은 아닙니다.
| 고유 세션 | 요청 | 오류 | worst shard p95 | worst shard p99 |
|---|---|---|---|---|
| 100 | 180,170 | 1 | 439.66ms | 556.86ms |
| 250 | 451,390 | 17 | 457.71ms | 638.61ms |
| 500 | 900,092 | 0 | 459.73ms | 765.21ms |
각 단계의 동시 실행 구간은 약 30분이며 429는 모두 0회였습니다. 표의 percentile은 전체 요청을 합친 값이 아니라 shard별 percentile 중 최댓값입니다. 100·250 세션의 오류는 기록상 네트워크 오류이며, DB 원인을 입증한 값은 아닙니다.
오류가 없다는 것과 응답 시간 목표를 통과했다는 것은 다릅니다. 문서의 Navigation 공개 목표는 p95 300ms·p99 750ms입니다. 표의 worst shard p95는 모두 300ms를 넘고, 500 세션은 p99도 750ms를 넘었습니다. 일부 진단 단계의 진행 허용을 공개 SLO 전체 통과로 바꾸어 적지 않습니다.
100 세션 시험에서도 최대 응답은 5.865초였고 5초 초과가 46회 남았습니다. DB 실행과 HTTP 지연이 다르다는 점은 추가 계측의 근거지만, 서로 다른 요청들의 percentile을 빼서 특정 계층의 정확한 지연이나 원인을 확정할 수는 없습니다.
이 과정에서 남긴 설계 기준
먼저 요청 경로를 계측하고, 실제로 줄일 수 있는 SQL 작업량·외부 왕복·반복 집계를 구분합니다. 이후 실행 수와 대기열을 제한하고, 시험 환경과 사용자 모델을 현실에 가깝게 바꾸어 남은 지연을 확인합니다. 인덱스 하나나 오류율 0%만으로 용량 검증을 끝내지 않습니다.
기록의 출처와 현재 검증 범위
시스템 설계 감사, Discord Bot 시스템 디자인 점검, Grofile 전체 시스템 디자인 점검의 기록을 함께 확인했습니다. 위 수치는 2026년 8월 세션에 보존된 원래 도구 출력과 당시 결과에서 추렸고, 구현은 고정 커밋과 대조했습니다. 임시 시험 자원과 artifacts는 당시 정리돼 현재 재다운로드를 확인한 것은 아닙니다.
현재 운영 성능이나 후속 9월 버전의 재측정 결과로 외삽하지 않습니다. 이 글을 작성하면서 운영 DB 조회·새 부하 시험·배포는 수행하지 않았습니다.