이 글의 배경이 되는 이야기는 5화: 식약처 API 11종과의 첫 만남에서 읽을 수 있습니다.
이 글을 읽으면 알 수 있는 것
- 식품안전나라 API와 공공데이터포털 API의 응답 구조가 어떻게 다른지
- 같은 정부 포털에서 제공하는 API인데 왜 형식이 제각각인지
- 이 차이를 어떤 방식으로 흡수했는지
문제: 같은 질문, 다른 대답 형식
CheckEat은 식품 데이터를 수집하기 위해 두 곳의 정부 API를 사용합니다.
| 출처 | API 수 | 데이터 | 건수 |
|---|---|---|---|
| 식품안전나라 (foodsafetykorea.go.kr) | 9종 | 제품, 원재료, 표준사전, 공전 규격 | ~104만 건 |
| 공공데이터포털 (data.go.kr) | 3종 | 영양성분, 원재료 정보, HACCP | ~150만 건 |
두 곳 모두 정부 기관이 운영하는 공공데이터 포털입니다. 같은 목적(식품 정보 제공)의 API인데, URL 구조, 응답 포맷, 에러 코드, 페이지네이션 방식 — 모든 것이 달랐습니다.
처음 API를 연결할 때는 “하나 해봤으니 나머지도 비슷하겠지”라고 생각했습니다. 틀렸습니다.
응답 구조 4가지 유형
11개 API의 응답을 분류하면 4가지 유형으로 나뉩니다.
유형 A: 식품안전나라 — 서비스명 래핑
9개 API가 이 구조를 따릅니다. 응답의 최상위 키가 서비스 코드명(I1250, C002 등)입니다.
{
"I1250": {
"RESULT": { "CODE": "INFO-000", "MSG": "정상 처리되었습니다" },
"total_count": "1043709",
"row": [{ "PRDLST_REPORT_NO": "197810010037", "PRDLST_NM": "..." }]
}
}
여기서 이미 주의할 점이 여럿 있습니다:
total_count가 문자열입니다. 숫자가 아니라"1043709"로 옵니다.- 데이터 배열의 키가
row입니다. 복수형rows가 아닙니다. - API를 바꿀 때마다 최상위 키가 바뀝니다.
"I1250"→"C002"→"I2520".
유형 B: 공공데이터포털 — response 래핑
영양성분 API 등이 이 구조를 따릅니다. 유형 A와는 완전히 다른 형태입니다.
{
"response": {
"header": { "resultCode": "00", "resultMsg": "NORMAL SERVICE." },
"body": {
"totalCount": 284523,
"items": [{ "FOOD_CD": "D018-001", "FOOD_NM_KR": "..." }]
}
}
}
유형 A와 비교하면:
- 최상위 키가 서비스명이 아니라
response로 고정 totalCount가 정수입니다. (유형 A는 문자열)- 데이터 배열 키가
row가 아니라items - 에러 코드가
INFO-000이 아니라"00"(어떤 API는"OK")
유형 C: response 래핑이 빠진 변형
같은 공공데이터포털인데 response 래핑 없이 바로 header와 body가 나오는 API도 있습니다.
유형 D: 이중 중첩
HACCP API는 items 안에 item이 한 번 더 들어 있습니다.
같은 포털의 다른 API에서는 items가 바로 데이터 배열인데, 여기서는 한 겹 더 까야 합니다.
필드명 불일치: 같은 것의 다른 이름
응답 구조뿐 아니라, 같은 개념을 가리키는 필드명도 API마다 다릅니다.
“품목보고번호”는 한국에서 제조·수입 식품을 식별하는 유일한 키입니다. 이 하나의 개념이 API마다 이렇게 불립니다:
| 출처 | 필드명 | 표기법 |
|---|---|---|
| 식품안전나라 | PRDLST_REPORT_NO | 대문자_밑줄 |
| 공공데이터포털 | prdlstReportNo | 카멜케이스 |
| 영양성분 CSV | 품목제조보고번호 | 한글 |
같은 번호인데 세 가지 이름으로 불리니, 데이터를 합치려면 “이 필드가 저 필드와 같은 것”이라고 매번 수동으로 지정해줘야 합니다.
에러 코드도 마찬가지입니다:
| 출처 | ”성공" | "데이터 없음” |
|---|---|---|
| 식품안전나라 | INFO-000 | INFO-200 |
| data.go.kr A | "00" | items: null |
| data.go.kr B | "OK" | items: null |
“성공했다”를 표현하는 방식조차 통일되어 있지 않습니다.
갈림길: 이 차이를 어떻게 흡수할 것인가
API를 하나씩 연결하면서 패턴이 보이기 시작했습니다. 식품안전나라 쪽 9개 API는 서로 구조가 같고, 공공데이터포털 쪽 3개도 서로 같습니다. 차이가 나는 것은 출처 사이이지, 같은 출처 안에서는 아닙니다.
그래서 세 가지 방법을 놓고 고민했습니다.
| 방법 | 장점 | 단점 |
|---|---|---|
| API마다 개별 처리 | 단순 명확 | 재시도, 로깅, 중단점 복구 등 공통 로직이 11번 중복 |
| 하나의 범용 처리기 | 코드 하나로 통일 | 유형 A~D 분기가 뒤섞여서 오히려 복잡 |
| 출처별 처리기 2개 | 공통 흐름 재사용 + 차이점만 분리 | 초기 설계에 시간 필요 |
세 번째를 선택했습니다.
공통 흐름(재시도, 중단점 복구, 원본 저장, 작업 기록)은 한 곳에 구현하고, “API를 호출해서 응답을 파싱하는 부분”만 출처별로 나누는 구조입니다.
공통 처리기 (재시도, 중단점 복구, 원본 저장, 작업 기록)
├── 식품안전나라 전용 (유형 A 파싱) → 9개 API가 공유
└── 공공데이터포털 전용 (유형 B~D 파싱) → 3개 API가 공유
개별 API는 “어떤 API인지”와 “한 건의 데이터를 어떻게 저장하는지”만 정의하면 됩니다. 나머지 — 페이지를 넘기고, 실패하면 재시도하고, 어디까지 받았는지 기록하는 일 — 은 공통 처리기가 알아서 합니다.
이 구조 덕분에 새 API를 추가할 때마다 40~70줄 정도의 짧은 코드만 작성하면 되었습니다.
결과
| 항목 | 수치 |
|---|---|
| 연동한 데이터 소스 | 12종 (API 9 + CSV 1 + 고시문서 파싱 1 + HACCP 1) |
| 응답 구조 유형 | 4가지 |
| 필드명 케이싱 | 3가지 (대문자_밑줄, 카멜케이스, 한글) |
| 에러 코드 표현 | 3가지 (INFO-000, "00", "OK") |
| 총 적재 건수 | 제품 104만 + 원재료 98만 + 영양 28만 + … |
한계
공공 API의 일일 호출 한도에 도달하면, 현재는 일반 오류와 동일하게 처리됩니다. “한도를 넘었으니 내일 이어서 받으세요”라는 안내가 자동으로 나오지 않아서, 사용자가 에러 메시지를 직접 읽고 판단해야 합니다.
한 줄 교훈
“데이터가 공개되어 있다”와 “데이터를 쓸 수 있다” 사이에는 응답 구조 4종, 에러 코드 3종, 필드명 표기법 3종의 간극이 있습니다.