집계 API 설계: 작은 응답으로 DB 작업량까지 줄이려면

집계 API를 가볍게 만들려면 응답에 담을 숫자의 개수와 그 숫자를 계산하는 데 읽을 데이터를 따로 제한해야 한다. 그보다 먼저 정할 것은 숫자의 뜻이다. 현재 남아 있는 기록을 생성일로 묶은 결과와 과거에 발생한 모든 사건의 이력은 다르다. 지표의 의미, 집계 입력 범위, 클라이언트의 시간 규칙을 함께 검토해야 작은 응답이 쓸 만한 API가 된다.

GitHub는 2026년 10월 14일 공개한 star history 설계 글에서 React 예제의 전체 이력 조회가 약 2,511회 요청에서 24회로 줄었다고 설명했다. 핵심은 개인별 목록을 집계로 바꾸는 것과 함께, 집계할 시간 구간을 먼저 좁힌 데 있다. 이 수치는 GitHub가 보고한 요청 횟수이며 처리 속도의 배수가 아니다. 아래는 이 사례를 출발점으로 만든 작은 서비스의 설계 리뷰 예제다. GitHub 구현을 재현하거나 성능을 측정한 결과는 아니다.

1. 먼저 “어떤 수를 세는가”를 한 문장으로 정한다

팀용 콘텐츠 서비스에 즐겨찾기 추이 화면을 만든다고 하자. 기획서에는 “최근 12주 동안 얼마나 관심을 받았는지 보여준다”라고 적혀 있다. 개발자는 현재 즐겨찾기 테이블의 생성 시각을 날짜별로 묶으려 한다. 이때 “관심”이라는 표현부터 더 구체적으로 바꿔야 한다.

가상의 월요일에 5명이 즐겨찾기를 추가했고, 그중 2명이 금요일에 해제했다고 해보자. 해제 시 행을 삭제하는 테이블이라면 다음 주에 다시 계산한 월요일 값은 3이다. 월요일에 추가 행위가 3번 일어났다는 뜻도, 금요일에 2명이 줄었다는 뜻도 아니다. 조회 시점에 유지되고 있는 즐겨찾기 가운데 월요일에 생성된 관계가 3개라는 뜻이다.

이 서비스가 현재 남은 관심의 유입 시기를 보여주려는 것이라면 이 정의로 진행할 수 있다. 화면에는 “현재 유지 중인 즐겨찾기의 생성일별 분포”처럼 범위를 드러낸다. 반대로 캠페인 당일 추가 횟수나 날짜별 순증감을 알아야 한다면, 추가·해제 사건을 별도로 보존하는 모델이 필요하다. 이미 삭제된 행의 과거를 집계 쿼리만으로 되살릴 수는 없다.

GitHub의 설계 글도 별을 해제하면 원본 기록이 사라진다고 설명한다. 따라서 star history라는 이름만으로 모든 별 추가·해제 사건을 보존한 자료라고 해석하면 안 된다.

이벤트 이력을 선택해도 계약은 남는다. 같은 요청의 재전송을 한 사건으로 볼지, 해제 후 다시 추가한 것을 새 사건으로 볼지, 늦게 들어온 기록을 어느 날짜에 반영할지 정해야 한다. 누적 총량을 재구성하려면 시작 시점의 기준값도 필요하다. 이벤트 테이블을 만들었다는 사실만으로 정확한 역사적 총량이 보장되지는 않는다.

2. 응답 84개와 입력 84개는 전혀 다른 조건이다

첫 설계안은 “전체 즐겨찾기를 날짜별로 집계한 뒤 최근 84개 날짜만 반환한다”이다. 응답은 작지만, 요청할 때마다 오래된 데이터까지 집계하는 계획이 선택될 수 있다. 게다가 기록이 있는 날짜만 나온다면 최근 84개 날짜가 최근 12주를 뜻하지도 않는다. 빈 날짜가 많을수록 더 먼 과거까지 포함된다.

수정안에서는 먼저 팀과 시작·종료 시각으로 집계 대상의 자격을 정한다. 그 범위에 들어온 현재 즐겨찾기만 날짜별로 묶고, 정해진 달력의 빈 날짜를 채운다. “최근 12주”라는 제품 요구를 데이터 선택 조건으로 옮기는 것이다. 전체 기간의 인기 순위를 구하던 API에 이 제한을 그대로 넣으면 질문 자체가 바뀌므로, 같은 최적화로 취급하면 안 된다.

PostgreSQL의 LIMIT 문서도 결과 행의 제한과 서버 내부 작업을 구분한다. 다만 SQL은 선언형이므로 문장에 적힌 순서가 모든 실행 계획의 물리적 순서를 뜻하지는 않는다. LIMIT가 실행을 일찍 멈추는 데 도움이 되는 쿼리도 있다. 여기서 검토할 것은 “LIMIT는 효과가 없다”가 아니라 “이 집계가 반환할 구간 밖의 데이터를 얼마나 처리하는가”이다.

기간 조건을 넣었다고 읽기 비용이 자동으로 줄어드는 것도 아니다. 팀과 원본 생성 시각을 조건으로 사용할 수 있는 인덱스 후보를 검토하되, 실제 계획에서 범위를 좁히는지 확인해야 한다. 전체를 읽고 조건에 맞지 않는 행을 버릴 수도 있다. 집계에 전달된 행 수, 필터에서 제외한 행, 반복 실행 횟수와 읽은 버퍼를 함께 살펴야 한다. 계획의 최상단 결과 행 수만 보고 스캔량으로 해석하지 않는다. PostgreSQL EXPLAIN 문서는 노드별 출력 행 수와 실제 읽은 행 수가 다를 수 있음을 설명한다.

리뷰용 가정으로, 같은 12주에 원본 행이 1만 개인 팀과 100만 개인 팀을 놓고 보자. 두 팀 모두 달력상 응답은 84일이다. 그러나 집계 입력의 밀도는 크게 다르다. 이는 측정 결과가 아니라 시험 데이터를 나눌 기준이다. 기간 상한은 원본 행 수나 실행 시간의 상한이 아니다. 출시 직후 특정 주에 데이터가 몰리는 사례를 별도로 검증해야 한다.

3. 달력과 빈칸도 응답 계약에 넣는다

숫자 배열만 넘기면 프런트엔드가 주 시작일, 시간대, 정렬 방향을 추측하게 된다. 외부 API를 가져오는 경우에는 공급자의 규칙부터 확인한다. GitHub 공식 star history 문서의 주 단위 결과는 최신 순이며 페이지는 과거 방향으로 이동한다. 별이 없는 주도 0으로 제공하고, 일별 배열은 일요일부터 시작한다. 주·일 경계가 UTC와 일치한다는 보장은 없다. 따라서 이 값을 그대로 출시 전후 특정 UTC 날짜의 변화에 대응시키기는 어렵다.

우리 예제의 API에는 다음 규칙을 제안한다. 이는 GitHub의 동작 설명이 아니라, 직접 소유한 데이터와 API에 적용할 선택이다.

  • 기간: UTC 월요일 00시를 주 경계로 삼고, 완료된 최근 12주를 반환한다. 진행 중인 주는 제외한다. 시작 시각은 포함하고 종료 시각은 제외해 인접 구간이 겹치지 않게 한다.
  • 배열: 날짜를 명시한 84개 일별 값을 오래된 순서로 반환한다. 데이터가 없는 날은 0이다. 조회 실패나 아직 집계하지 못한 날을 0으로 바꾸지는 않는다.
  • 기준: 응답에 대상 팀, 지표 정의 버전, 기간, 시간대와 데이터 갱신 시각을 싣는다. 요청의 기간 경계와 데이터가 최신인 시점은 서로 다른 정보다.
  • 페이지: 과거 구간을 추가로 읽게 한다면 첫 요청에서 정한 기간 기준을 이어받는다. 월요일이 바뀌어도 페이지 사이에 같은 주가 중복되거나 빠지지 않도록 한다.
  • 변경 가능성: 즐겨찾기 해제로 과거 날짜의 값이 바뀔 수 있음을 명시한다. 기간 기준을 고정하는 것만으로 여러 요청이 같은 데이터 스냅샷을 읽게 되지는 않는다.

최소 응답에는 개인 식별자가 필요하지 않다. 그렇다고 합계라서 접근 제어까지 생략해도 된다는 뜻은 아니다. 인증된 사용자가 볼 수 있는 팀의 집계인지 확인하고, 캐시 키에도 팀과 지표 버전, 기간을 포함해야 한다. 다른 팀의 숫자가 섞이는 오류는 결과가 몇 바이트인지와 무관하다.

4. 설계 회의에서는 이렇게 승인 조건을 잡는다

이 예제의 결론은 “현재 유지 중인 즐겨찾기 분포를 보여주는 내부 대시보드”로 범위를 좁혀 진행하는 것이다. 캠페인별 전체 추가·해제 분석은 별도 지표로 남긴다. 아래 표는 실행 결과가 아닌 구현 후 통과해야 할 수용 기준이다. 날짜 경계 예시는 UTC 기준 2026년 9월 28일 00시부터 10월 12일 00시 직전까지의 2주로 축소했다.

검토 상황통과 조건과 확인 근거
시작·종료 경계에 생성된 행9월 28일 00시 행은 포함, 10월 12일 00시 행은 제외한다. 인접 구간을 합쳐도 중복이 없어야 한다.
기록이 전혀 없는 날짜정상 조회라면 14일 모두 반환하고 빈 날은 0이다. 오류·미집계 상태는 정상적인 0과 구별한다.
과거에 추가한 즐겨찾기 해제캐시 갱신 정책에 따라 원래 생성일의 값이 감소한다. 과거 값이 고정된다는 화면 설명이 없어야 한다.
조용한 기간과 출시 주간같은 응답 크기에서도 입력 행·읽기량·시간을 별도로 기록한다. 밀집 구간이 팀의 처리 예산을 넘으면 실시간 집계를 승인하지 않는다.
다른 팀의 같은 날짜 요청쿼리 조건, 권한 검사, 캐시 키가 모두 팀을 구분한다. 합계와 식별자 어느 쪽도 다른 팀에 노출되지 않는다.
주가 바뀌는 동안 과거 페이지 조회날짜 구간은 연속이고 중복되지 않는다. 별도 스냅샷 기능이 없다면 값까지 고정된다고 약속하지 않는다.

처리 예산에는 팀이 감당할 요청 시간, DB 읽기량, 동시 요청 수를 실제 운영 조건에 맞춰 적는다. 이 글에서 보편적인 밀리초 기준을 정할 수는 없다. 초기 구현에서 안전한 범위의 실행 계획을 검토하고, 통제된 시험 환경에서 밀도별 부하를 확인한 뒤 값을 정한다. EXPLAIN ANALYZE는 쿼리를 실제로 실행한다. 운영 DB에서 가벼운 메타데이터 조회처럼 사용하지 않는다.

5. 밀집 구간을 못 버티면 무엇을 바꿀까

12주를 4주로 줄이면 보통 다룰 범위는 작아지지만, 한 주에 데이터가 집중되었다면 충분하지 않을 수 있다. 이때는 일별 사전 집계나 갱신 주기가 있는 캐시, 무거운 조회의 비동기 처리 등을 검토한다. 어느 방식을 택하든 해제로 바뀌는 과거 값의 수정과 갱신 지연을 같은 지표 계약에 포함해야 한다.

사전 집계로 넘어갈 때 “추가될 때 하루 합계를 1씩 올린다”만 구현하면 지표의 뜻이 바뀐다. 현재 유지 중인 관계를 세기로 했다면 해제 시 원래 생성일의 합계도 수정해야 한다. 반면 추가 사건 수를 세는 지표라면 해제를 이유로 과거 추가 횟수를 빼지 않는다. 계산 위치를 옮기는 변경과 지표를 바꾸는 변경은 각각 검토해야 한다.

  • 현재 남은 관심의 유입 분포: 현재 상태 테이블의 기간 집계가 맞을 수 있다. 과거 값의 수정 가능성을 받아들인다.
  • 추가·해제 횟수와 순증감: 사건 이력과 중복 처리 기준, 필요한 기준값을 먼저 확보한다.
  • 출시 전후 날짜별 변화: 시간대와 경계가 확인된 자료가 필요하다. 날짜 기준이 보장되지 않는 외부 집계를 임의로 재해석하지 않는다.

6. 전환과 롤백에서도 숫자의 뜻을 유지한다

  1. 비교 기준을 고정한다. 기존 목록 기반 집계와 새 집계에 같은 팀, 기간, 시간대, 삭제 규칙을 적용한다. 변경 가능한 원본이라면 통제된 스냅샷이나 시험 데이터로 먼저 대조한다. 서로 다른 시각에 읽은 값의 차이를 곧바로 새 API 버그로 판단하지 않는다.
  2. 새 읽기 경로를 분리한다. 내부 일부 요청부터 전환하고 버킷별 값, 누락 날짜, DB 부하와 캐시 지연을 확인한다. 비교용 이중 조회 자체도 부하를 늘리므로 범위를 제한한다.
  3. 되돌릴 조건을 미리 적는다. 설명할 수 없는 값 차이, 날짜 중복·누락, 처리 예산 초과가 나타나면 전환을 멈춘다. 롤백은 같은 지표 의미와 접근 권한을 유지하는 이전 읽기 경로로 한다.
  4. 안전한 이전 경로가 없으면 중단을 표시한다. 집계 장애를 숨기려고 개인별 목록을 노출하거나 실패 구간을 0으로 채우지 않는다. 오래된 캐시를 제공할 수 있다면 갱신 시각을 표시하고, 허용 범위를 넘으면 일시적인 조회 불가 상태를 보여준다.

첫 리뷰에서 물을 질문은 간단하다. “이 84개의 숫자는 무엇을 뜻하고, 만들기 위해 어디까지 읽으며, 언제 바뀔 수 있는가?” 세 답이 맞물려야 응답 크기를 줄인 개선이 운영 가능한 집계 API로 이어진다.

확인 범위와 참고 자료

자료 확인일은 2026년 10월 15일이다. 공식 설계 글과 문서를 바탕으로 작성했으며, GitHub API 호출이나 PostgreSQL 부하 시험은 수행하지 않았다. 예제의 12주·UTC 월요일·일별 84개 응답과 수용 기준은 독립적인 설계 제안이다. 실제 배포 전에는 서비스 데이터 분포, 실행 계획, 권한·캐시 동작을 검증해야 한다.

댓글