조회 결과를 다른 시스템에 넘기는 API 를 만들 때, 요청마다 결과 크기가 크게 달라지는 경우가 있다. 모든 요청을 한 방식으로 처리하면 소량 요청은 불필요하게 느리고 대량 요청은 메모리를 터뜨린다. 그래서 예상 규모를 먼저 재고 경로를 가르는 구성을 쓴다.
요청
│
├─ EXPLAIN 으로 예상 행 수·비용 측정
│
├─ 임계치 이하 → 동기 처리 (+ 서킷 브레이커로 오판 대비)
│
└─ 임계치 초과 → 커서 스트리밍 (NDJSON 또는 Arrow)
쿼리를 실제로 돌리기 전에 EXPLAIN(또는 EXPLAIN ANALYZE)으로 실행 계획만 조회해 예상 행 수와 비용을 본다. 이 값이 임계치보다 작으면 동기 경로로, 크면 스트리밍 경로로 보낸다.
요점은 쿼리를 다 돌려 보기 전에 무게를 재는 것이다. 결과를 다 받고 나서 "크네" 하고 판단하면 이미 메모리에 올라간 뒤다.
다만 실행 계획의 추정치는 통계 기반이라 틀릴 수 있다. 통계가 오래됐거나 편향된 데이터면 크게 어긋난다. 그래서 다음 항목이 따라붙는다.
장애 전파를 막는 안정성 패턴이다. 특정 호출이 실패나 타임아웃을 반복해 임계치를 넘으면 회로를 Open 으로 바꿔 이후 요청을 즉시 차단하고 폴백시킨다. 일정 시간이 지나면 Half-Open 으로 소수 요청만 흘려 복구 여부를 확인하고, 성공하면 Closed 로 되돌린다.
Closed ──(연속 실패 임계 초과)──▶ Open ──(대기 시간 경과)──▶ Half-Open
▲ │
└──────────────────(시험 요청 성공)───────────────────────────┘
여기서 서킷 브레이커의 역할은 분명하다 — EXPLAIN 이 "가볍다" 고 판단해 동기 경로로 보냈는데 실제로는 무거웠거나 DB 가 느려진 경우에, 그 요청들이 쌓여 전체 시스템을 마비시키지 않게 막는 안전장치다. 추정이 틀릴 수 있다는 전제를 설계에 반영한 것이다.
서버 측 커서를 열어 결과를 한 번에 메모리에 올리지 않고 일정 배치 단위로 순차 fetch 하며 클라이언트로 흘려보낸다.
한 줄에 JSON 객체 하나씩 나열한다. 줄바꿈이 레코드 구분자라서 전체를 다 받지 않아도 한 줄씩 즉시 파싱된다.
{"id":1,"name":"a"}
{"id":2,"name":"b"}
일반 JSON 배열([...])은 닫는 괄호까지 받아야 파싱이 끝나므로 스트리밍에 맞지 않는다. NDJSON 은 사람이 읽을 수 있고 도구 지원도 넓어 중간 규모까지 무난하다.
컬럼 기반 인메모리 데이터 포맷 표준이다. 언어·프로세스 사이에 직렬화 비용을 거의 들이지 않고 데이터를 주고받을 수 있어, Arrow IPC 스트리밍이나 Arrow Flight 같은 프로토콜로 대량 표 형식 데이터를 빠르게 전송한다.
NDJSON 보다 파싱 비용이 훨씬 낮은 대신, 양쪽 모두 Arrow 를 다룰 수 있어야 하고 스키마가 고정돼 있어야 한다. 진짜 대량 구간에서 선택한다.
| 구간 | 경로 | 포맷 | 보호 장치 |
|---|---|---|---|
| 소량 | 동기 응답 | JSON | 서킷 브레이커 |
| 중량 | 커서 스트리밍 | NDJSON | 타임아웃, 배치 크기 |
| 대량 | 커서 스트리밍 | Arrow | 타임아웃, 배치 크기, 백프레셔 |
임계치는 행 수 하나로 잡기보다 예상 행 수와 비용 두 가지로 잡는 편이 안전하다. 행 수는 적어도 컬럼이 넓거나 정렬·조인이 무거우면 비용이 크기 때문이다.