데이터파이프라인

상용 배포를 막던 1GB 데이터베이스: 원본 보관에서 필요한 값의 투영으로

2026. 08. 27

조달 공고 6개월치를 확보하자 DB가 1,058MB로 불어나 저장소에 포함할 수 없게 되었고, 영업담당자가 쓰는 공고 이력 조회 기능 전체가 상용 배포에서 막혔습니다. 용량의 85%를 차지하던 원본 응답을 전수 조사해 실제 참조 경로를 특정하고, 그 값만 남긴 과정입니다.

프로젝트 배경

소속
휴트리온 · AX·DX사업부 AI개발팀 (Client: 통신사 B2B 영업조직)
프로젝트
B2B AI Agent · Account Plan 자동 생성
기간
2026.07 ~ 진행중 (2026.09 베타 · 12 상용 목표)
담당
Account Plan 에이전트 1인 전담 · 수집~대시보드 전 계층

영업 대상 기업의 공시·뉴스·공공조달 데이터를 자동 수집·분석해 Account Plan 초안을 생성하는 백엔드 파이프라인입니다. 조달 공고는 공공 API로 매일 수집하며, 기업의 사업 방향을 판단하려면 과거 6개월에서 1년치 이력이 필요합니다.

데이터파이프라인저장소설계SQLite스키마상용서비스

1. 배경: 상용 배포를 막은 1GB

영업담당자가 Account Plan 초안에서 가장 먼저 확인하는 항목은 대상 기업의 공공사업 참여 이력입니다. 어떤 기관의 어떤 사업에 참여했고 어느 분야에서 낙찰받았는지가 다음 제안의 근거가 되기 때문입니다.

이 근거를 만들기 위해 조달 공고 6개월치를 백필해 공고 81,661건과 수요기관 603곳을 확보했습니다. 수집 자체는 성공했으나, SQLite 파일이 1,058MB로 불어나면서 상용 배포 경로가 막혔습니다.

조달 DB는 애플리케이션 코드와 함께 배포되는 구조입니다. 서비스 기동 직후부터 조회가 가능해야 하고, 배포할 때마다 6개월치를 다시 수집하려면 수천 회의 API 호출과 수십 분이 소요되기 때문입니다. 1GB 바이너리는 GitHub 파일 크기 제한을 넘으며, 제한을 넘지 않더라도 저장소 이력에 한 번 포함되면 이후 모든 클론 비용이 영구히 증가합니다.

용량 문제를 해결하지 못하는 한 영업담당자가 쓰는 공고 이력 조회 기능 전체가 상용에 올라가지 못합니다.

2. 문제 정의

2.1 용량의 85%가 한 테이블에 집중되어 있었습니다

테이블별 크기를 집계한 결과 분포가 극단적이었습니다.

테이블크기비중
g2b_raw_payloads898MB85%
나머지 전체160MB15%

g2b_raw_payloads는 조달청 API의 응답을 가공 없이 저장한 테이블입니다. 한 행이 공고 100건 묶음에 해당하며, 하루치를 수집할 때마다 수십 행이 누적됩니다.

원본을 보관한 이유는 재처리 가능성 확보였습니다. 파싱 규칙을 변경하거나 필드를 잘못 읽은 경우 외부 API를 다시 호출하지 않고 저장된 원본에서 재생성하려는 의도였고, 조달청 API가 일일 호출 한도를 가진 자원이라는 점에서 당시로서는 타당한 판단이었습니다.

2.2 원본을 단순 삭제하면 기능이 소실됩니다

용량 문제만 보면 g2b_raw_payloads를 비우는 것으로 해결됩니다. 그러나 삭제 영향을 검토하는 과정에서 공고문 첨부 주소가 원본 응답 안에만 존재한다는 사실이 드러났습니다.

ntceSpecDocUrl1 ~ ntceSpecDocUrlN

이 필드는 공고 테이블에 정규화되지 않았고, 첨부 문서를 읽을 때마다 원본 응답을 JSON 파싱해 꺼내 쓰는 구조였습니다. 공고 81,661건을 집계한 결과 77,179건(94%)에 첨부가 존재했습니다.

원본 898MB를 그대로 지우면 공고문 본문을 읽는 경로가 함께 사라지며, 이는 Account Plan의 근거 품질을 직접 떨어뜨립니다. 사업 참여 이력만 나열되고 그 사업이 무엇이었는지는 확인할 수 없게 되기 때문입니다.

3. 기술적 난점

이 작업의 난점은 원본 보관이라는 결정 자체가 틀리지 않았다는 데 있습니다.

재처리 가능성을 위해 원본을 남기는 것은 데이터 파이프라인의 일반적인 관행입니다. 문제는 그 관행에 보관 기간과 용량 상한을 두지 않은 점이었습니다. 개발 단계에서 하루치를 다룰 때는 898MB라는 규모가 발생할 수 없으므로 비용이 드러나지 않았습니다.

두 번째 난점은 비가역성입니다. 원본을 삭제한 뒤 다른 필드가 필요하다는 사실이 밝혀지면 6개월치를 다시 수집해야 하며, 여기에는 다시 수천 회의 API 호출과 며칠의 일정이 듭니다.

4. 대안 검토

4.1 원본을 유지하고 저장소 밖으로 옮기는 방식

1안. Git LFS

  • 장점: 스키마와 코드를 변경하지 않습니다.
  • 단점: LFS 대역폭 과금이 발생하고, CI가 빌드할 때마다 1GB를 내려받아 빌드 시간이 증가합니다. 파일 크기 자체는 그대로 남습니다.

2안. 외부 스토리지 저장 후 기동 시 다운로드

  • 장점: 저장소가 가벼워집니다.
  • 단점: 배포 절차에 외부 의존이 추가되고, 1GB 다운로드가 서비스 기동 시간에 더해집니다. 스토리지 장애가 곧 서비스 기동 실패가 됩니다.

두 안 모두 1GB라는 크기를 그대로 둔 채 위치만 옮깁니다. 크기가 제약의 원인이므로 위치 변경은 해결이 아니라고 판단해 기각했습니다.

4.2 보관 기간을 단축하는 방식

3안. 원본을 최근 N일치만 유지

  • 장점: 구현이 단순하고 즉시 적용 가능합니다.
  • 단점: N을 어떻게 잡든 경계 밖의 공고는 첨부를 잃습니다. 오래된 공고일수록 기업의 사업 방향을 보여주는 이력으로서 가치가 높으므로, 과거를 먼저 버리는 방식은 이 데이터를 수집한 목적과 정면으로 어긋납니다.

4.3 필요한 값만 투영하는 방식

4안. 원본에서 실제 참조하는 값만 공고 테이블로 옮기고 원본 삭제

  • 장점: 크기 문제를 근본적으로 제거하며 첨부 조회 기능을 유지합니다.
  • 단점: 재처리 가능성을 포기합니다.

4안을 채택했습니다. 판단 근거는 재처리 가능성의 실제 사용 빈도였습니다. 6개월간 파싱 규칙을 여러 차례 수정했으나 저장된 원본을 다시 훑어 재처리한 사례는 한 건도 없었습니다. 필드를 잘못 읽은 경우에도 수정 시점 이후의 데이터만 정확하면 초안 품질에 지장이 없었습니다. 한 번도 사용하지 않은 보험에 용량의 85%를 지불하고 있었던 셈입니다.

5. 설계 결정 및 근거

5.1 삭제 전에 참조 경로를 전수 확인했습니다

비가역 작업이므로 원본을 참조하는 코드 경로를 먼저 전수 조사했습니다. 결과는 첨부 주소를 읽는 한 곳뿐이었습니다.

파싱된 나머지 필드는 이미 공고 테이블에 정규화되어 있었고, 원본을 직접 조회하는 함수는 첨부 처리 경로 외에 존재하지 않았습니다. 첨부 주소만 JSON 컬럼으로 옮기면 39MB이며, 잔여 859MB는 삭제해도 동작이 동일합니다.

5.2 데이터 이전보다 읽는 코드를 먼저 배포했습니다

작업 순서를 데이터 이전이 아닌 코드 수정으로 잡았습니다. 새 컬럼을 우선 조회하고 값이 없으면 기존 경로로 넘어가는 함수를 먼저 넣었습니다.

pythondef _notice_doc_urls(notice: dict[str, Any]) -> dict[str, Any] | None:
    """공고 행의 `doc_urls_json` 을 첨부 주소 dict 로 돌려준다.

    원본 응답을 걷어낸 DB 에서 쓴다. 값이 없으면 None 을 돌려주고,
    부르는 쪽이 원본을 뒤지는 기존 경로로 넘어간다.
    """

이 구조를 먼저 배포하면 데이터 이전 도중에도 서비스가 정상 동작하고, 아직 이전하지 않은 개발자 로컬 환경에서도 동일한 코드가 그대로 작동합니다. 이전과 배포 중 어느 쪽이 먼저 수행되어도 기능이 유지되므로 순서를 맞출 필요가 없어집니다.

5.3 비가역 작업에 세 단계 안전장치를 두었습니다

장치목적
--dry-run삭제 전 감소량과 보존 건수만 출력해 결과를 예측
g2b.db.before-slim작업 후 원본 파일을 남겨 로컬 복구 경로 확보
복원 스크립트압축된 DB를 되돌리는 절차를 코드로 고정

5.4 VACUUM으로 파일 크기를 실제로 회수했습니다

SQLite는 행을 삭제해도 파일 크기가 줄지 않습니다. 삭제된 공간은 free page로 남아 재사용을 대기하며, 운영체제에 반환되지 않습니다.

VACUUM을 실행해야 파일이 축소됩니다. 이 단계를 누락하면 삭제가 정상 수행되었음에도 파일이 1GB 그대로 남아 작업이 실패한 것으로 오인됩니다.

6. 실험 결과

6.1 용량

항목이전이후
DB 파일1,058MB92MB (8.7%)
g2b_raw_payloads898MB0
첨부 주소 위치원본 응답 내부공고 테이블 JSON 컬럼 (39MB)

6.2 데이터 보존

항목
공고81,661건 (2026-02-22 ~ 08-23)
수요기관603곳
첨부 주소77,179건 이전 완료

첨부가 존재하는 공고 77,179건을 전량 이전했습니다. 나머지 4,482건(6%)은 원본 응답에 첨부 필드 자체가 없던 공고이므로 이전 대상에 해당하지 않습니다.

6.3 기능 및 배포

공고문 본문 조회가 이전과 동일하게 동작함을 확인했습니다. 읽는 경로가 새 컬럼을 우선 조회하고 없으면 기존 경로로 넘어가므로 호출부 코드는 변경되지 않았습니다.

DB가 92MB로 줄어 저장소 포함이 가능해졌고, 막혀 있던 공고 이력 조회 기능의 상용 배포 경로가 확보되었습니다.

7. 결론 및 시사점

7.1 보관 데이터의 비용은 기간이 늘어날 때 재검토해야 합니다

원본 응답은 재처리를 위한 보험이었습니다. 6개월간 그 보험을 한 번도 사용하지 않은 채 용량의 85%를 지불했고, 그 비용이 상용 배포를 막는 지점에 이르러서야 드러났습니다.

보험을 드는 판단 자체는 잘못되지 않았습니다. 다만 데이터가 누적되는 구조에서는 비율이 아니라 절대량이 제약이 되므로, 수집 기간을 늘릴 때 보관 정책을 함께 재검토하는 절차가 필요합니다. 개발 단계의 하루치 기준으로 내린 판단은 6개월치에서 그대로 유효하지 않습니다.

7.2 비가역 작업에서는 의존 관계 전수 확인이 유일한 방어입니다

이번 작업이 성립한 이유는 원본을 참조하는 경로가 한 곳뿐이었기 때문이며, 그 사실은 코드 전수 조사로 사전에 확인했습니다.

크기만 보고 삭제했다면 첨부 조회 기능을 잃었을 것이고, 그 사실은 영업담당자가 공고문을 열어보려는 시점에야 드러났을 것입니다. 삭제 후에는 6개월치 재수집 외에 복구 수단이 없으므로, 지우기 전에 세는 것 외에 다른 방어 수단이 존재하지 않습니다.

7.3 읽는 쪽을 먼저 고치면 배포 순서 의존이 사라집니다

새 경로를 우선 조회하고 없으면 기존 경로로 넘어가는 구조를 먼저 배포하면, 데이터 이전과 코드 배포가 서로를 기다리지 않습니다.

데이터를 먼저 옮기면 코드 배포 전까지 기능이 중단되고, 코드를 먼저 배포하면 이전 완료 전까지 새 경로가 항상 비어 있습니다. 어느 순서로 수행해도 동작하는 상태를 만드는 것이 운영 중 서비스에서 안전합니다.

7.4 SQLite에서 삭제는 축소가 아닙니다

행 삭제만으로는 파일 크기가 변하지 않으며 VACUUM까지 수행해야 반영됩니다. 작업 결과를 파일 크기로 검증하는 절차에서 이 단계를 누락하면 성공한 작업을 실패로 판단하게 됩니다.