이 글의 배경이 되는 이야기는 5화: 식약처 API 11종과의 첫 만남과 6화: 제품 100만 개를 담을 그릇에서 읽을 수 있습니다.
이 글을 읽으면 알 수 있는 것
- 12개 데이터 소스를 하나의 패턴으로 통합한 구조
- 공통 처리기가 담당하는 7가지 책임
- 새 API를 추가할 때 40줄이면 충분한 이유
문제: 12개 소스, 같은 문제의 반복
이 프로젝트에서 데이터를 가져오는 소스는 12개입니다.
| 소스 | 유형 | 건수 |
|---|---|---|
| 식품안전나라 API 9종 | REST API | ~104만 건 |
| 공공데이터포털 API 3종 | REST API | ~150만 건 |
| 식품영양성분 CSV | 파일 | ~6천 건 |
| 첨가물 고시 hwpx | 문서 파싱 | ~780 건 |
이 소스들은 데이터의 내용과 형식이 전부 다릅니다. 하지만 수집 과정에서 해결해야 하는 문제는 놀라울 정도로 비슷했습니다.
- 요청이 실패하면? → 재시도
- 중간에 끊기면? → 어디까지 했는지 기억하고 이어서
- API 원본 응답은? → 별도 보존
- 몇 건 받았고, 몇 건 저장했고, 실패한 건은? → 기록
- 이전에 받은 데이터와 겹치면? → 덮어쓰기 (upsert)
처음에는 API마다 이 로직을 각각 구현했습니다. 세 번째 API를 연결할 때쯤, 코드의 70% 이상이 복붙이라는 것을 깨달았습니다.
갈림길: 중복을 어떻게 없앨 것인가
| 방법 | 장점 | 단점 |
|---|---|---|
| API마다 개별 스크립트 | 단순 명확 | 공통 로직이 12번 중복 |
| 하나의 범용 스크립트 | 코드 하나 | 유형 A~D 분기가 뒤섞임 |
| 공통 처리기 + 개별 정의 | 공통 흐름 재사용, 차이만 분리 | 초기 설계 필요 |
세 번째를 선택했습니다.
Django의 management command 패턴을 활용했습니다. python manage.py import_i1250 같은 형태로 각 데이터 소스를 실행하되, 공통 로직은 부모 클래스에 모아두는 구조입니다.
공통 처리기의 7가지 책임
부모 클래스가 담당하는 것:
| # | 책임 | 동작 |
|---|---|---|
| 1 | 페이지네이션 | 시작/끝 번호를 계산하고, 반복 호출 |
| 2 | 재시도 | 실패 시 지수 백오프 (5→15→45초) |
| 3 | 쿨다운 | 3회 연속 실패 시 60초 대기 후 재시도 |
| 4 | 중단점 복구 | --resume 시 DB 건수 기반으로 시작점 계산 |
| 5 | 원본 보존 | API 응답 JSON을 해시 기반으로 별도 저장 |
| 6 | 작업 기록 | 시작 시간, 건수, 성공/실패 여부 기록 |
| 7 | 워터마크 | 각 행의 수정일 최대값 추적 |
개별 API가 정의하는 것:
| 항목 | 예시 |
|---|---|
| 서비스 이름 | I1250, C002 |
| 한 건을 어떻게 저장하는가 | upsert_row() 구현 |
| 응답을 어떻게 파싱하는가 | 출처별 부모 클래스가 담당 |
계층 구조
공통 처리기
├── 식품안전나라 전용 (유형 A 파싱)
│ ├── import_i1250 (제품)
│ ├── import_c002 (원재료)
│ ├── import_i2520 (표준 사전)
│ └── ... (6개 더)
└── 공공데이터포털 전용 (유형 B~D 파싱)
├── import_food_nutrition (영양성분)
├── import_haccp (HACCP)
└── import_rawmaterial (원재료 정보)
3단 구조입니다. 최상위 공통 처리기는 재시도, 중단점 복구, 원본 보존 등 모든 데이터 소스에 공통되는 로직을 담당합니다. 중간 계층은 출처별 응답 파싱 차이를 흡수합니다. 최하위의 개별 커맨드는 “이 데이터를 DB에 어떻게 저장하는가”만 정의합니다.
이 구조 덕분에 새 API를 추가할 때 작성해야 하는 코드는 40~70줄 정도입니다. 서비스 이름, 모델 매핑, upsert 로직 — 이 세 가지만 정의하면, 나머지는 부모 클래스가 알아서 합니다.
이 구조가 해결한 문제들
재시도 로직의 일관성
12개 소스에서 모두 같은 재시도 정책이 적용됩니다. 한 곳에서 재시도 간격을 수정하면 전체에 반영됩니다.
작업 기록의 표준화
모든 수집 작업이 같은 형식으로 기록됩니다. “언제 실행했고, 몇 건 받았고, 몇 건 저장했고, 성공인가 실패인가.” 이 기록을 비교하면 “어제와 오늘의 건수 차이”를 볼 수 있습니다.
중단점 복구의 자동화
--resume 플래그 하나로, 어떤 API든 중단된 지점에서 이어받습니다. DB에 실제로 저장된 건수를 세어서 시작점을 계산하는 로직이 공통 처리기에 있으므로, 개별 API가 이 로직을 신경 쓸 필요가 없습니다.
갈림길: 응답 파싱을 어디에 둘 것인가
공통 처리기가 응답 파싱까지 담당하면 어떨까? 유형 A~D를 분기 처리하면 하나의 클래스에서 모든 API를 처리할 수 있습니다.
| 방식 | 장점 | 단점 |
|---|---|---|
| 공통 처리기에서 분기 | 클래스 하나 | if/elif 체인이 길어짐, 새 유형 추가 시 위험 |
| 출처별 중간 계층 | 유형별 책임 분리 | 클래스가 하나 더 필요 |
중간 계층을 선택한 이유는 첫 번째 글에서 이야기한 것과 같습니다. 식품안전나라와 공공데이터포털의 응답 구조가 근본적으로 다르고, 이 차이를 하나의 분기문에 우겨넣으면 코드가 취약해집니다.
결과
| 항목 | 수치 |
|---|---|
| 공통 처리기 | 1개 (~300줄) |
| 출처별 중간 계층 | 2개 (식품안전나라, 공공데이터포털) |
| 개별 커맨드 | 12개 (각 40~70줄) |
| 새 API 추가 시 작성량 | 40~70줄 |
| 공통으로 제공되는 기능 | 재시도, 중단점 복구, 원본 보존, 작업 기록, 워터마크, 페이지네이션, 쿨다운 |
한계
공통 처리기가 하는 일이 너무 많습니다. 300줄 안에 7가지 책임이 들어 있습니다. 단일 책임 원칙에서 벗어나 있고, 수정 시 영향 범위가 넓습니다. 하지만 현재 규모에서는 클래스를 더 분리하는 것이 오히려 복잡도를 높인다고 판단했습니다.
CSV와 hwpx 소스는 이 패턴에 완벽하게 맞지 않습니다. API 기반 페이지네이션을 전제로 설계된 구조라, 파일 기반 소스에서는 일부 기능(페이지네이션, 재시도)이 불필요합니다. 공통 처리기의 일부를 건너뛰는 방식으로 대응하고 있습니다.
한 줄 교훈
12개 소스에서 같은 문제를 같은 방식으로 풀고 싶다면, “어떻게 풀 것인가”를 한 곳에 두고 “무엇을 풀 것인가”만 각자 정의하게 해야 합니다.