미국의 한 화물 포워더(운송 주선사)의 운영 업무를 자동화하는 일을 맡았다. 처음 메일함을 열어보고 든 생각은 “여기 있는 정보가 다 어디로도 안 흘러가고 있구나”였다.
포워더 운영은 통째로 이메일 위에서 돈다. 견적 요청이 메일로 오고, 선사가 운임을 메일로 회신하고, 화물이 어디쯤 왔는지도 도착 통지 메일로 온다. 정산 청구서도 첨부파일로 온다. 문제는 이 모든 게 스레드 안에만 있다는 것이다. “이 건 어디까지 진행됐지”, “이 선사한테 받은 운임이 얼마였지”, “청구서 금액이 우리가 낸 견적이랑 맞나” — 답이 전부 메일 어딘가에 있는데, 아무도 꺼낼 수 없다. 사람이 매번 스레드를 거슬러 읽고 손으로 옮긴다.
메일이 데이터가 되면 그 다음이 열린다. 무료 보관 마감이 임박한 화물을 미리 경고하고, 도착 통지에서 다음 서류를 자동으로 채우고, 청구서가 견적과 어긋나면 짚어낸다 — 사람이 스레드를 뒤지느라 놓치던 것들이다. 다만 그건 데이터가 먼저 쌓인 다음의 이야기고, 이 글은 그 아래층, 메일을 데이터로 바꾸는 파이프라인을 만든 기록이다. 완성된 자랑이 아니라, 설계하면서 세 번쯤 판단을 뒤집은 과정에 가깝다.
왜 하필 ‘입력’부터인가
한 가지 짚고 시작하자. 물류 회사가 새 관리 시스템이나 가시성 도구를 도입하려 할 때, 진짜 걸림돌은 대개 기능이 아니다. 입력이다.
어느 조직이든 새 시스템을 기존 업무 위에 얹으면, 사람이 같은 정보를 두 번 넣게 된다. 원래 하던 대로 메일과 문서로 일을 처리하고, 그걸 다시 새 시스템에 손으로 입력한다. 이 이중 입력이 부담스러워서 새 시스템은 점점 안 쓰이게 된다. 도구가 나빠서가 아니라, 입력이라는 마찰에 걸려 넘어지는 것이다. 이건 특정 회사 직원들의 문제가 아니라 어디서나 반복되는 구조다.

입력은 흔히 “고객이 알아서 넣어야 할 몫”으로 미뤄진다. 하지만 그게 도입의 병목이라면, 그 병목을 없애는 것 자체가 제품이 된다. 이미 메일함으로 쏟아지는 문서들 — 선하증권, 도착 통지, 청구서 — 에서 데이터를 자동으로 뽑아 채워주면, 사람이 새로 입력할 게 없어진다. 그래서 나는 화려한 기능이 아니라 가장 지루한 곳, 입력부터 손대기로 했다. 이게 이 파이프라인을 만든 진짜 이유다. 뽑아낸 데이터가 어느 시스템으로 흘러가는지는 그 다음 문제고, 특정 도구에 묶이지도 않는다.
정본이 어디 있는지부터 정했다
무엇을 만들지 정하기 전에, 무엇이 진실의 원본(source of truth)인지부터 정해야 했다. 자동화가 데이터를 여기저기 복사하기 시작하면, 곧 어느 게 맞는 값인지 아무도 모르게 되기 때문이다. 그래서 세 줄로 못을 박았다.
메일 = 원문 정본 / 시트 = 상태 정본 / AI = 파서

메일은 사람이 주고받은 원본이니 절대 건드리지 않는다. 삭제도, 이동도 안 한다. 구조화된 업무 상태(어느 화물이 어느 단계인지)는 별도 시트가 정본이다. 그리고 AI는 둘 사이에서 메일을 읽어 필드로 뽑아내는 파서 역할만 한다. 이 세 줄이 이후 모든 결정의 기준이 됐다. 헷갈릴 때마다 “이건 원문이야, 상태야?”로 돌아갔다.
첫 벽: 키워드 필터로는 원리상 안 된다
메일함을 자동화하려면 먼저 이 메일이 무슨 종류인지를 알아야 한다. 도착 통지인지, 청구서인지, 그냥 사람끼리 조율하는 대화인지. 처음엔 당연히 제목·본문 키워드 필터로 분류하려 했다. 메일 규칙은 어느 메일 서비스에나 있으니까.
이게 원리적으로 불가능하다는 걸 깨닫는 데 오래 걸리지 않았다. 대표적인 함정이 POD였다. 물류에서 POD는 Port of Discharge(양륙항)이기도 하고 Proof of Delivery(배송 증빙)이기도 하다. 완전히 다른 두 개념이 같은 세 글자다. 키워드 필터는 이걸 구분할 방법이 없다. 실제로 스팸 메일이 배송 증빙으로 분류되고, 관세 관련 메일이 사전 통지로 잘못 걸렸다.
POD만이 아니었다. 오타, 인용문이 잔뜩 붙은 긴 답장(원래 메일 키워드가 다 딸려온다), 한 메일에 여러 성격이 섞인 경우 — 키워드로는 어느 것도 안정적으로 못 잡는다. 결국 기존 키워드 필터를 전부 삭제했다. 메일 규칙은 분류기가 아니라 “초인종” 역할, 즉 “운영 메일이 하나 도착했다”만 알리는 데까지만 쓰기로 했다. 실제 분류는 AI가 메일을 읽고 판단한다.
1단계: 분류기와 “큐 드레인”

첫 단계는 도착한 운영 메일을 8가지 이벤트(부킹·사전통지·도착통지·청구서·배송증빙·배송지시·선사알림·일반대화)로 분류해 라벨을 붙이는 워크플로우다. 메일 규칙은 캐치올(catch-all) 라벨 하나만 붙이고, AI가 메일을 읽어 확정 라벨을 단다.
여기서 배운 재사용 패턴이 “처리 완료를 어딘가에 기록해야 한다”는 것이다. 분류가 끝난 메일은 캐치올 라벨을 떼어낸다. 그러면 “아직 라벨 안 뗀 것 = 미처리”가 되어, 다음 실행이 그것만 집는다. 이걸 큐 드레인(queue drain)이라 부른다. 이 표시를 안 하면 워크플로우가 같은 메일을 매번 다시 처리한다. 뒤에 문서 파서를 만들 때도 똑같은 문제가 반복됐다 — 이미 파싱한 첨부를 어떻게 “완료”로 표시하느냐. 답은 늘 같았다. 상태를 남겨라.
이 분류 규칙은 워크플로우 코드에 박아넣지 않고 외부 문서 하나로 빼뒀다. 문서를 고치면 곧 분류 기준이 바뀐다. 이 이야기는 따로 다룰 만큼 효과가 커서 위성 글로 뺐다.
2단계: 선하증권을 텍스트가 아니라 “눈으로” 읽기
분류 다음은 문서에서 데이터를 뽑는 일이다. 사전 통지 메일에는 보통 선하증권(BL, Bill of Lading)이 PDF로 붙어 온다. 여기서 화주·수하인·컨테이너 번호·중량·품목을 뽑아야 사람이 손으로 옮기던 일이 사라진다.
처음엔 PDF에서 텍스트를 추출해 AI에 넘겼다. 결과가 엉망이었다. 선하증권은 칸(box)으로 나뉜 표 양식인데, 텍스트만 뽑으면 칸의 배치가 무너져서 어느 값이 어느 필드인지 뒤섞인다. 그래서 방식을 바꿔 PDF를 이미지처럼 통째로 AI에게 보여주는 비전(vision) 방식으로 갔다. 표 구조를 그대로 이해하니 필드가 정확히 잡혔다. 흥미롭게도 더 큰 모델을 써도 정확도가 오르지 않아, 가벼운 쪽으로 확정했다.
파싱을 실제 서류로 검증하다 도메인 지식 하나를 크게 배웠다. 컨테이너 번호가 선적을 합치는 열쇠라는 것이다. 실제 화주와 수하인은 HBL(House BL, 포워더가 화주에게 발행)에 적히고, 선사 화물 번호와 선박 정보는 MBL(Master BL, 선사가 포워더에게 발행)에 적힌다. 한 선적의 완전한 그림을 얻으려면 이 둘을 합쳐야 하는데, 둘을 직접 매칭할 공통 키가 없다. 유일하게 양쪽에 다 있는 게 컨테이너 번호였다. 이걸 병합 키로 잡으니 흩어진 조각이 하나로 모였다.

여기서 겪은 조용한 버그 하나가 특히 아팠다. 첨부가 여러 장일 때 코드가 항상 첫 번째 첨부만 읽고 있었다. 테스트를 1건으로 할 땐 첫 번째가 곧 그 건이라 멀쩡해 보였고, 여러 건을 돌리자 전부 같은 값으로 나왔다. “1건 테스트로는 안 드러나는 버그”의 전형이다.
3단계: 통합 파서 — 추출과 저장의 축을 나눴다
도착 통지(A/N, Arrival Notice)에는 예상 도착 시각, 무료 보관 마감일(LFD, Last Free Day), 픽업 번호, 터미널 같은 운영에 꼭 필요한 정보가 들어 있다. 이걸 파싱하는 단계에서 설계를 한 번 크게 뒤집었다.
처음 생각은 “문서 종류마다 전용 파서를 따로 만들자”였다. 선하증권 파서, 도착통지 파서, 청구서 파서… 그런데 이렇게 쪼개면 파편화가 심해진다. 같은 선적을 두고 문서마다 파서가 다르면, 나중에 합칠 때 지옥이 된다.
그래서 추출과 저장을 다른 축으로 분리했다. 추출은 통합한다 — 문서 하나를 한 번의 호출로 전부 읽어 필요한 걸 다 뽑는다. 저장만 성격별로 라우팅한다 — 컨테이너 단위 정보는 컨테이너 행으로, 선적 단위 정보는 별도로, 요금은 요금 탭으로. “한 번에 다 뽑고, 나눠 담는 건 저장 단계에서” 라는 원칙이다. 이건 혼자 낸 결론이 아니라, 내가 “요금은 얕게만 뽑고 나머진 청구서 파서로 넘기자”고 제안한 걸 반박당하면서 정리됐다. 추출을 쪼개지 말고 저장의 모양(shape)만 나누라는 지적이 맞았다.
이 통합 파서는 도착 통지 스무 건 남짓을 넣어 마흔여섯 개 컨테이너 행으로 뽑아내는 것까지 검증했고, 지금은 정해진 주기로 자동으로 돈다.
아직 안 된 것
여기까지가 메일을 데이터로 바꾸는 인제스천(수집) 레이어다. 분류 → 선하증권 파싱 → 도착 통지 파싱까지 라이브로 돌아간다. 하지만 이건 절반이다.
- 제품의 진짜 가치는 아직 안 만들었다. 데이터를 모으는 것과, 그 데이터로 사람을 돕는 것은 다르다. 무료 보관 마감일이 임박한 화물을 미리 알려주는 알림, 도착 통지에서 배송 지시서를 자동 생성하는 것 — 이게 사람이 진짜 원하는 것이고, 아직 다음 과제다.
- 피드백 루프는 아직 연결하지 않았다. AI 분류를 사람이 교정하면 그걸 학습에 반영하는 구조를 설계해뒀지만, 지금은 각 단계가 순서대로 잘 도는지 혼자 확인하는 단계라 아직 연결하지 않았다. 실제 운영 담당자가 라벨을 다루기 시작하면 그때 붙인다. 이건 위성 글에서 따로 다룬다.
- 단일 고객 데이터다. 지금까지의 규칙과 판단은 한 곳의 메일에서 나왔다. 다른 포워더에서도 같은 패턴이 반복될 때 비로소 일반화할 수 있다. 그 전엔 전부 잠정적이다.
정리 — 가져갈 원칙 4개
- 정본을 먼저 정하라. 무엇을 만들지보다 무엇이 진실의 원본인지가 먼저다. 원문·상태·파서의 역할을 나누면 헷갈릴 때 돌아갈 기준이 생긴다.
- 분류는 키워드가 아니라 판단이다. 다의어·오타·긴 인용문 앞에서 규칙 필터는 무너진다. 대신 규칙은 “초인종”으로만 쓰고, 판단은 AI에 맡긴다.
- 처리 완료를 반드시 기록하라. 큐에서 뺐다는 표시가 없으면 같은 일을 무한히 반복한다.
- 추출은 통합하고 저장은 나눠라. 문서마다 파서를 쪼개면 파편화된다. 한 번에 다 뽑고, 저장 단계에서 모양을 나눈다.
자주 묻는 질문
Q. 왜 메일을 삭제하거나 옮기지 않나?
메일이 원문 정본이기 때문이다. 자동화가 원본을 건드리면 나중에 문제가 생겼을 때 되짚을 곳이 사라진다. 처리 완료는 삭제가 아니라 라벨(상태 표시)로 남긴다.
Q. PDF에서 텍스트를 뽑는 것과 이미지로 보는 것은 뭐가 다른가?
칸으로 나뉜 표 양식 문서에서 텍스트만 뽑으면 칸의 공간 배치가 무너져 값이 뒤섞인다. 이미지로 통째로 보면 AI가 표 구조 자체를 이해해 필드를 정확히 매칭한다. 선하증권처럼 레이아웃이 제각각인 문서에서 특히 차이가 크다.
Q. 왜 처음부터 문서 종류별 파서를 만들지 않았나?
만들려다 뒤집었다. 같은 선적을 문서마다 다른 파서로 처리하면 나중에 합칠 때 파편화가 심하다. 추출은 하나로 통합하고, 저장만 정보 성격에 따라 나누는 편이 확장에 유리했다.
Q. 이 시스템이 지금 실제로 돌아가나?
메일 분류, 선하증권 파싱, 도착 통지 파싱까지는 정해진 주기로 자동 실행된다. 다만 그 데이터를 사람에게 알려주는 알림·업무 생성 레이어는 아직 다음 단계다.
이 파이프라인은 아직 절반이다. 다음 편들에서 각 단계를 더 깊이 파볼 생각이다 — 분류 규칙을 코드가 아니라 문서 한 장으로 관리하는 법, “오분류 0건”이라는 지표에 속았던 이야기, 그리고 실제 서비스가 정확도가 아니라 배관에서 터진 이야기까지.
기록으로 남기는 이유는 단순하다. 이런 운영 자동화는 도메인마다 디테일이 다르고, 남이 밟은 지점을 미리 보는 것만으로도 몇 주가 절약되기 때문이다. 비슷하게 이메일·문서에 업무가 묶여 있는 곳이라면, 여기 적은 판단들이 출발점이 될 수 있을 것이다.