BL 자동 발행 기능 만들기

외부 API 연동의 지옥

by RICE
"그냥 API 붙이면 되는 거 아니에요?"
"PM님, 이거 그냥 OCR API 붙이면 자동으로 되는 거 아닌가요?"
개발 미팅에서 나온 이 질문에 나는 쓴웃음을 지었다.
2024년 8월, 수출 서비스를 런칭하면서 BL(선하증권) 자동 발행 기능을 만들기로 했을 때의 일이다.
당시 운영팀은 일주일에 10건의 BL을 외부 서비스를 통해 수기로 발행하고 있었고, 한 건당 평균 40분이 걸렸다.
"API 연동이요? 그거 하루면 되죠!" 라는 개발자의 말에 나는 경험상 알고 있었다.
'하루'는 '일주일'을 의미하고, '간단해요'는 '생각보다 복잡할 거예요'의 완곡한 표현이라는 것을.
결과적으로 이 프로젝트는 3개월이 걸렸고, 내가 PM으로 일하면서 경험한 가장 복잡한 외부 API 연동 케이스가 되었다.
그리고 많은 것을 배웠다. 특히 "API 문서가 친절하다고 연동이 쉬운 건 아니다"라는 진리를.




3675683.jpg freepik 무료 이미지 사용


첫날: 순진했던 나


프로젝트 킥오프 미팅에서 나는 자신감에 차 있었다. 손에는 OCR API 문서가 있었고, 머릿속에는 깔끔한 플로우가 그려져 있었다.

화주가 CI(Commercial Invoice)랑 PL(Packing List) 업로드

OCR로 텍스트 추출

추출된 데이터를 BL 템플릿에 매핑

메일 발송

끝!


"이거 2주면 되겠는데요?" 내가 말했다. "진짜요? 그럼 8월 말까지 배포 가능하겠네요!" 운영팀장님이 기뻐했다.

지금 생각하면 정말 순진했다. 현실은 나의 예상을 처참하게 배신했거든.


첫 번째 충격: OCR은 마법이 아니다

API 연동 첫날, 테스트용 CI 파일을 업로드했다. 깔끔하게 작성된 엑셀 파일을 PDF로 변환한 것이었다. OCR API는 3초 만에 결과를 반환했고, 정확도도 95% 이상이었다.

"역시 요즘 AI는 대단해!" 나는 감탄했다.

그런데 실제 화주사가 보낸 CI를 테스트하는 순간, 모든 게 무너졌다. 문제는 명확했다. 화주사마다 서류 양식이 달랐고, 어떤 건 손글씨가 섞여 있었으며, 스캔 품질도 들쑥날쑥했다. 심지어 한 화주는 CI를 사진으로 찍어서 보냈는데, 사진이 기울어져 있었다.

"이거... API만으로는 안 되겠는데요." 개발자가 말했다. "네... 제가 생각이 짧았네요." 나는 인정할 수밖에 없었다.


지옥의 시작: 응답 시간이 10초?


OCR 정확도 문제를 해결하기 위해 Fallback UI를 만들기로 했다. 파싱이 실패하거나 정확도가 낮으면 수동 입력 화면으로 전환하는 거다. 그런데 새로운 문제가 나타났다.

어느 날 운영팀에서 전화가 왔다.

"PM님, 이거 버튼 눌러도 아무 반응이 없는데요?" "네? 지금 테스트해보니까 잘 되는데요?" "아니 진짜 안 돼요. 한참 기다렸는데도 화면이 그대로예요."

현장에 가서 확인해보니, 운영자가 업로드한 PDF는 20MB가 넘는 고해상도 스캔 파일이었다. OCR API 응답 시간이 15초나 걸렸고, 그 사이 프론트엔드는 로딩 표시도 없이 멈춰 있었다.

"이거 에러 난 거 같은데요?" 운영자가 버튼을 다시 눌렀다. "아 잠깐만요!" 내가 소리쳤지만 이미 늦었다.

중복 요청이 두 번 더 발생했고, 같은 파일을 세 번 파싱하느라 API 비용만 날렸다.

해결책: 비동기 처리의 필요성

이 문제를 해결하려면 근본적으로 구조를 바꿔야 했다. 프론트엔드가 API 응답을 동기적으로 기다리는 대신, 작업을 백그라운드 큐에 등록하고 폴링으로 상태를 확인하는 방식으로.

개발자와 머리를 맞대고 설계했다:


사용자가 파싱 버튼을 누르면 즉시 작업 ID를 받음

프론트엔드는 3초마다 작업 상태를 확인

파싱이 완료되면 결과를 보여주고, 실패하면 Fallback UI로 전환

타임아웃은 30초로 설정


스크린샷 2025-11-15 오후 5.00.43.png notion mermaid code 사용

"이게 맞는 것 같은데, 구현하려면 시간이 좀 걸릴 것 같아요." 개발자가 말했다."얼마나요?" "일주일... 아니 열흘?"

예상 일정은 또 늘어났다. 하지만 이게 맞는 방향이었다.


두 번째 지옥: 데이터 형식이 왜 이래?


비동기 처리를 구현하고 나니 응답 시간 문제는 해결됐다. 그런데 이번엔 파싱 결과를 활용하는 단계에서 막혔다.

OCR API가 반환하는 데이터를 보고 나는 당황했다. 테이블의 각 셀 위치와 텍스트는 정확하게 추출됐지만, 그게 '품목명'인지 '수량'인지는 API가 알 수 없었다. 단지 "문서의 세 번째 테이블, 두 번째 행, 첫 번째 열에 이런 텍스트가 있어요"라고만 알려줄 뿐이었다.


내가 필요한 건

품목명: Widget A
수량: 100
단가: 12.5


같은 구조화된 데이터였다. 그런데 API는 위치 정보와 텍스트만 던져주는 거다.

"이거... 우리가 매핑 로직을 만들어야 하는 거 아니에요?" 개발자가 물었다. "그런 것 같네요..." 나는 한숨을 쉬었다.


화주마다 다른 서류 양식

더 큰 문제는 화주마다 CI 양식이 완전히 다르다는 거였다.

A사: '품목명'이 항상 첫 번째 테이블의 두 번째 열


B사: 'Description of Goods'라고 쓰고 세 번째 열에 위치


C사: 테이블이 아니라 자유 형식 텍스트로 작성


"이걸 어떻게 자동화해요?" 개발자가 절망적인 표정으로 물었다.

나는 잠시 고민하다가 아이디어를 냈다.

"화주별 매핑 룰을 데이터베이스에 저장하면 어떨까요? 첫 번째 파싱 때는 운영자가 수동으로 매핑하고, 그 정보를 저장해서 다음부터는 자동으로 적용하는 거죠."

"오, 그럼 사용할수록 정확도가 올라가겠네요?"

"맞아요! 일종의 학습하는 시스템인 셈이죠."

이 아이디어는 나중에 정말 유용하게 작동했다. 신규 화주의 첫 BL 발행은 시간이 걸리지만, 두 번째부터는 자동으로 처리되는 구조가 만들어진 거다.


세 번째 지옥: 에러가 너무 많아요


드디어 파싱도 되고, 매핑도 되고, 이제 BL을 발행하기만 하면 되는 단계에 왔다. 그런데 실제 테스트를 시작하자 예상치 못한 에러들이 쏟아졌다.

어느 날 운영자가 황당한 표정으로 말했다:

"PM님, 파싱은 성공했는데요, 품목명만 빈칸이에요. 다른 건 다 잘 나왔는데 왜 이것만 안 나오죠?"

로그를 확인해보니 OCR API는 정상 응답을 반환했지만, 품목명 필드의 신뢰도 점수가 20%였다. 너무 낮아서 시스템이 자동으로 제외시킨 거다.

"이런 경우엔 어떻게 해야 해요?" 운영자가 물었다.

나는 이런 '부분 성공' 케이스를 어떻게 처리할지 고민해야 했다.


에러 레벨 정의

팀과 논의 끝에 에러를 세 가지 레벨로 분류했다:

Critical: 파싱 자체가 불가능. 즉시 수동 입력 모드로 전환.
예) API 응답 없음, 파일 형식 오류, 타임아웃

Warning: 파싱은 됐지만 신뢰도 낮음. 결과를 보여주되 확인 필요 표시.
예) confidence score 60% 이하 필드, 필수 필드 누락

Info: 파싱 성공했지만 추가 정보 필요. 선택적 보완 가능.
예) 선택 필드 누락, 형식 불일치

운영자 입장에서 각 에러가 무엇을 의미하는지, 어떻게 대응해야 하는지 명확히 알려주는 게 중요했다.

"품목명 정확도가 낮습니다. 확인 후 수정해주세요." 같은 구체적인 메시지를 넣었더니, 운영자들의 혼란이 크게 줄었다.


네 번째 지옥: API 비용 폭탄


기능이 어느 정도 안정화되고 실제 운영에 들어갔을 때, 예상치 못한 문제가 발생했다. OCR API 비용이 예상보다 3배나 높게 나온 거다.

"이거 버그 아니에요?" 운영팀장님이 청구서를 보여주며 물었다.

로그를 분석해보니 문제가 보였다:


운영자가 실수로 같은 파일을 여러 번 업로드

파싱 실패 시 자동 재시도가 과도하게 발생

테스트 환경에서의 반복 호출


"이거 좀 막을 방법 없어요?" 개발팀장님이 물었다.


캐싱으로 비용 절감

해결책은 의외로 간단했다. 파일 해시값을 계산해서, 같은 파일에 대한 파싱 요청이 들어오면 저장된 결과를 재사용하는 거다.

스크린샷 2025-11-15 오후 5.01.45.png notion mermaid code 사용


이 간단한 로직만으로도 API 호출이 30% 줄었다. 실제로 화주들이 같은 CI를 여러 부킹에 재사용하는 경우가 많았거든.


추가로 화주 대시보드에 "표준 템플릿 사용하면 발행이 더 빨라요!"라는 안내를 넣었더니, 표준 템플릿 사용률이 올라가면서 OCR 자체가 필요 없는 케이스도 늘어났다.


BL 유형별 자동 발송의 함정


마지막 관문은 BL 유형별 메일 발송 로직이었다. BL은 크게 세 가지 유형이 있다:


Original B/L: 선사에게만 발송

Sea Waybill: 화주에게만 발송

Surrender B/L: 선사와 화주 모두에게 발송


처음엔 간단해 보였다. 부킹 데이터에 BL 유형 필드가 있으니, 그거 보고 분기 처리하면 되는 거 아닌가?

그런데 실제로는 복잡했다. BL 유형은 부킹 시점에 확정되지 않고, 선적 직전에 화주가 변경 요청하는 경우가 많았다. 또 Original B/L이지만 화주에게도 사본을 보내달라는 요청이 빈번했다.

"이거 예외 케이스가 너무 많은데요?" 개발자가 고개를 저었다.

결국 발송 로직을 유연하게 만들기로 했다. 기본 룰은 BL 유형에 따르되, 운영자가 수동으로 수신자를 추가하거나 제외할 수 있는 옵션을 넣었다. 그리고 발송 전 마지막 확인 화면에서 수신자 목록을 한 번 더 보여주도록 했다.


그래서 결과는?


3개월간의 고생 끝에, 2024년 10월 말 BL 자동 발행 기능이 배포되었다.초기 예상했던 2주보다 10배나 오래 걸렸지만, 결과는 성공적이었다.

주요 성과:

운영자 1인당 주간 BL 발행 건수: 10건 → 25건 (150% 증가)

평균 발행 소요 시간: 40분 → 15분 (62.5% 감소)

파싱 정확도: 초기 60% → 3개월 후 85% (학습 효과)

API 비용: 예상 대비 30% 절감 (캐싱 효과)


하지만 숫자보다 중요한 건 운영팀의 반응이었다.

"PM님, 이거 진짜 편해졌어요. 처음엔 복잡해 보였는데, 막상 써보니까 오히려 실수가 줄어들었어요."

이 말을 들었을 때, 3개월간의 고생이 보상받는 기분이었다.(물론 상용 배포 후 예상치 못한 곳에서 문제가 발생하여 급하게 핫픽스한 부분도 있지만 이 부분은 다음에 짚고 넘어가겠다.)


배운 것들


이 프로젝트를 통해 내가 배운 교훈들을 정리하면:


1. "간단해 보이는" 외부 API 연동은 절대 간단하지 않다

API 문서가 친절하고 샘플 코드가 잘 되어 있어도, 실제 데이터는 예상과 다르다. 특히 B2B 서비스에서 고객마다 다른 데이터 형식을 다뤄야 한다면, 매핑과 검증에 예상보다 2~3배 시간이 든다.


2. Fallback은 선택이 아니라 필수다

자동화가 실패했을 때의 플랜B가 없으면, 전체 시스템이 멈춘다. 그리고 Fallback은 단순히 "수동으로 하세요"가 아니라, "어디까지 자동화됐고 무엇을 해야 하는지" 명확히 알려줘야 한다.


3. 비용은 처음부터 고려해야 한다

"일단 만들고 최적화는 나중에"라는 생각은 위험하다. 특히 호출당 과금되는 외부 API는 캐싱, 재시도 로직, 중복 요청 방지를 처음부터 설계에 포함시켜야 한다.


4. 사용자 피드백이 가장 중요하다

기술적으로 완벽해 보여도, 실제 사용자가 불편하다면 의미가 없다. 운영팀과 지속적으로 소통하며, 실제 업무 흐름에 맞춰 기능을 조정하는 과정이 필수다.


다음 편 예고


BL 자동 발행 기능을 만들면서 데이터 검증의 중요성을 절실히 느꼈으며, 잘못된 데이터로 발행된 BL 한 건이 얼마나 큰 문제를 일으키는지도 경험했습니다.

다음 편에서는 "적하목록 신고 자동화"를 다룰 예정입니다. AI Parsing과 Fallback 로직을 더 정교하게 만들면서, 실패율을 40%에서 10% 미만으로 줄인 이야기를 들려드리겠습니다.


본 글은 실제 프로젝트 경험을 바탕으로 작성되었으며,
회사의 보안 정책에 따라 일부 내용은 각색되었다.
작가의 이전글물류 포워딩 PM의 도전