원본 사진에서 멀티채널 재고까지: 견고한 인제스트 파이프라인 설계하기
사진으로 물품을 식별하고, 판매 게시물을 준비하며, 여러 마켓플레이스에 걸쳐 생애주기를 동기화하는 파이프라인에 대한 아키텍처 회고.
원본 사진에서 멀티채널 재고까지
사진 몇 장으로 판매 게시물을 만드는 일은 단순해 보인다. 물품을 인식하고, 제목을 생성하고, 가격을 추정한 뒤 마켓플레이스 API를 호출하면 된다. 이 그림은 데모에서는 통한다. 그러나 상자 하나를 통째로 처리하거나, 사진이 뒤섞여 들어오거나, 같은 물품을 여러 채널에 내보내야 하는 순간 부족해진다.
그때 진짜 문제는 더 이상 콘텐츠 생성이 아니다. 불완전한 입력에서 판매에 이르기까지, 유일한 물리적 물품의 생애주기를 혼동·중복·재고 충돌 없이 통제하는 일이다.
이 글은 먼저 사진 폴더에서 게시물 초안까지 이어지는 실운영 파이프라인을 설명한다. 이어서 내부 데이터베이스가 진실의 원천으로 남고, 어댑터가 eBay, Leboncoin 등 다른 유통 채널을 동기화하는 멀티채널 아키텍처로의 발전을 제안한다.
지침: AI는 관찰과 제안을 만들어낼 수 있다. 식별 정보, 재고, 되돌릴 수 없는 전이는 명시적인 애플리케이션 규칙의 통제 아래 있어야 한다.
사진, 물품, 게시물은 세 가지 다른 식별 정보다
책 한 상자가 이 문제를 잘 보여준다. 같은 책이 표지, 뒤표지, 책등, 속표지, 여러 본문 페이지로 표현될 수 있다. 반대로 비슷한 두 권이 저자, 총서, 제본, 거의 같은 외관을 공유할 수도 있다.
따라서 최소한 다음을 구분해야 한다.
- 내용과 해시로 식별되는 미디어 파일
- 여러 사진을 가질 수 있는 유일한 한 권, 즉 물리적 물품
- 제목, 상태, 속성, 가격을 모은 정규 레코드
- 공급자와 그 제약에 고유한 원격 게시물
- 재고 단위를 소비하거나 예약하는 판매
이 식별 정보를 혼동하면 가장 비싼 버그가 생긴다. 엉뚱한 책에 붙은 사진, 중복 게시물, 마켓플레이스 고유 데이터로 상품을 수정하는 일, 한 점뿐인 물품의 이중 판매다.
최초 파이프라인: 상자를 검증 가능한 책으로 바꾸기
시스템의 첫 버전은 사진이 이미 묶여 있을 때 책 한 권을 올바르게 처리했다. 상자로 확장하려면 사전 단계가 하나 추가된다. 폴더 순서, 파일 이름, 인위적인 구분자에 기대지 않고 각 물품에 해당하는 사진 집합을 찾아내는 것이다.
1. 신뢰할 수 없는 입력을 목록화하기
각 이미지는 먼저 신뢰할 수 없는 입력으로 취급한다.
- 확장자가 아니라 디코딩으로 실제 형식을 검증한다
- 파일 수, 용량, 압축 해제된 픽셀 수에 상한을 둔다
- 원본을 수정하지 않고 EXIF 방향을 로컬에서 보정한다
- 식별, 재개, 추적을 위해 해시를 계산한다
- 존재하면 EXIF 촬영 시각을 읽는다
- 결과가 검증될 때까지 원본을 보존한다
파일 시스템 날짜는 업무 신호로 쓰지 않는다. 복사, 내보내기, 클라우드 경유 과정에서 너무 쉽게 바뀌기 때문이다.
2. 묶기 전에 기술하기
저렴한 멀티모달 모델이 닫힌 스키마에 따라 이미지를 배치로 기술한다. 모델은 경로를 고르지도, 파일을 옮기지도 않는다. 각 사진에서 특히 다음을 추출한다.
- 보기 유형: 표지, 뒤표지, 책등, 속표지, 본문
- 보이는 서지 요소: 제목, 저자, 출판사, ISBN
- 구별되는 표시: 제본, 결함, 무늬, 메모
- 책이 실제로 있는지, 그리고 관찰의 확신도
이 구조화된 관찰이 이어서 희소한 후보 그래프를 채운다. 동일한 ISBN, 겹치는 텍스트, 양립하는 시각적 단서, 시간적 근접성으로 두 이미지가 후보가 될 수 있다.
타임스탬프는 의도적으로 약한 단서다. 몇 분 간격으로 찍힌 두 사진은 같은 물품을 담고 있을 가능성이 높지만, 서로 다른 두 권이 연달아 촬영됐을 수도 있다. 따라서 시간적 근접성은 검증을 열 뿐, 그 자체로 병합을 촉발하지 않는다.
3. 잘못된 혼합보다 한 번 더 나누기를 택하기
모든 후보 쌍이나 그룹은 표적화된 시각 검증을 거친다. 병합은 긍정적 증거가 있을 때만 받아들인다. 강한 모순--예를 들어 서로 다른 두 ISBN--은 대조를 막는다. 불확실한 판단은 어떤 관계도 만들지 않는다.
이 선택은 재현율보다 정밀도를 우선한다. unassigned 큐에 남은 사진 한 장은 사람의 확인 한 번이 비용의 전부지만, 한 게시물에 섞인 두 권은 잘못된 설명, 모순된 가격, 분쟁이 되는 판매를 낳을 수 있다.
결과는 완전하고 추적 가능한 분할이다.
- 안전하다고 간주된 그룹
- 검토 대상으로 표시된 그룹
- 미할당 사진
- 활용 가능한 물품이 감지되지 않아 무시된 이미지
4. 되돌릴 수 없는 경계에 사람을 두기
인터페이스는 이 입출력 순서를 그대로 반영한다. 가져오기, 묶기, 검증, 그리고 생성. 안전한 그룹은 미리 선택될 수 있지만 모호한 경우는 결코 그렇지 않다. 미할당·무시된 사진은 계속 보이되, 다음 파이프라인으로 조용히 넘어가지 않는다.
확인 뒤 각 그룹은 단위 파이프라인의 입력이 된다.
- 물품과 상태의 상세 분석
- 카테고리 속성 구성
- 비교 대상 검색과 가격 추정
- 충실한 사진 준비
- 미디어 업로드
- 자동 게시 없는 초안 생성
"메타데이터와 가격" 가지와 "사진 준비" 가지는 초안을 조합하는 시점에만 서로 의존하므로 병렬로 실행할 수 있다.
이 첫 아키텍처가 이미 해결하는 것
모델의 정확한 선택보다 네 가지 성질이 더 중요하다.
- 신중함: 약한 유사성만으로 두 물품을 병합하지 않는다.
- 재개 가능성: 비싼 관찰과 판단은 해시, 모델, 프롬프트 버전을 기준으로 캐시된다.
- 추적 가능성: 각 미디어는 최종 매니페스트에 정확히 한 번 나타나며, 배치와 관련 판단을 동반한다.
- 책임의 분리: 모델이 관찰하고, 업무 핵심이 결정하고, 인터페이스가 확인을 요청하고, 어댑터가 공급자와 대화한다.
이 아키텍처는 하나의 마켓플레이스가 유일한 유통 채널이고 그 초안이 원격 재고 역할을 할 수 있는 동안에는 충분하다. 그러나 유일한 물품이 여러 곳에 게시되는 순간 구조적 한계에 부딪힌다.
게시 파이프라인에서 재고 시스템으로
멀티채널 시스템에서 eBay, Leboncoin 또는 다른 유통 채널이 물품의 진실의 원천이 되어서는 안 된다. 각 플랫폼은 고유한 카테고리, 상태, 식별자를 가지며, 어느 곳도 다른 곳을 신뢰할 만큼 파악하지 못한다.
내부 데이터베이스가 정규 상태를 지녀야 한다. 물품 식별 정보, 재고 단위, 미디어, 기준 가격, 생애주기, 원격 게시물, 판매다. 애플리케이션은 이 데이터베이스를 읽고, 공급자는 각자의 계약에 맞춘 투영을 받는다.
목표 아키텍처는 고전적인 세 패턴을 결합한다.
- 각 마켓플레이스의 특수성을 격리하는 포트와 어댑터
- 의도를 잃거나 위험하게 재실행하지 않고 데이터베이스와 원격 호출을 동기화하는 트랜잭셔널 outbox/inbox
- 시스템 간의 불가피한 어긋남을 바로잡는 주기적 대사
물리적 물품을 중심에 둔 데이터 모델
한 점씩 판매하는 물품이라면 최소 모델은 단순하게 유지할 수 있다.
| 엔티티 | 책임 |
|---|---|
media_asset | 원본, 해시, EXIF 메타데이터, 파생물 |
item | 마켓플레이스와 무관한 정규 레코드 |
inventory_unit | 물리적 한 점과 실제 가용성 |
listing | 특정 공급자를 위한 물품의 투영 |
provider_binding | 원격 식별자, 버전, 마지막으로 확인된 상태 |
reservation | 거래 중 재고의 일시적 확보 |
sale | 성사된 판매, 발생 공급자, 재무 데이터 |
inbox_event | 중복 제거된 수신 이벤트 |
outbox_event | 실행할 동기화 의도 |
sync_attempt | 시도, 지연 시간, 정규화된 응답, 오류 |
상업적 내용은 채널마다 달라도 되지만, 식별 정보와 수량을 각 게시물에 복제해서는 안 된다. 한 점뿐인 책이라면 inventory_unit.available_quantity는 0 또는 1이며, 이 제약은 데이터베이스가 트랜잭션으로 보장해야 한다.
취약한 이중 쓰기 없이 게시하기
순진한 구현은 두 작업을 연달아 수행한다. 데이터베이스를 갱신한 뒤 마켓플레이스를 호출한다. 그 사이에 프로세스가 죽으면 데이터베이스와 공급자가 어긋난다. 순서를 뒤집어도 해결되지 않는다. 로컬 트랜잭션은 실패했는데 게시물만 만들어질 수 있다.
트랜잭셔널 outbox는 이 함정을 피한다.
- 로컬 트랜잭션이 물품을 수정하고
outbox_event에 의도를 삽입한다 - 워커가 그 의도를 읽고 해당 어댑터를 호출한다
- 어댑터는 멱등 키 또는 안정적인 업무 식별자를 사용한다
- 원격 응답이
provider_binding과 게시물 상태를 갱신한다 - 재시도는 같은 의도를 재실행할 뿐, 새로운 논리적 생성이 아니다
분산 환경의 가상적인 "exactly once"를 좇지 않는다. at least once 전달을 받아들이고, 각 처리를 멱등하게 만든다.
어댑터는 공통 어휘를 노출한다. 예를 들면:
upsert_draft(item)publish(listing)update_price(listing, price)reserve_or_pause(listing)end_listing(listing, reason)fetch_status(binding)fetch_recent_sales(cursor)
각 공급자는 이 계약을 자신의 API--또는 플랫폼이 허용하는 동기화 방식--로 번역하되, 그 세부를 업무 도메인으로 흘리지 않는다.
불리언 대신 생애주기를 관리하기
published = true 필드로는 부족하다. 명시적인 상태 기계가 전이를 관찰 가능하게 만들고 불가능한 조합을 막는다.
게시물에는 고유한 상태--초안, 게시 중, 활성, 일시중지 중, 종료, 오류--가 있지만, 여전히 재고 단위의 투영이다. inventory_unit이 이미 sold를 가리키면 활성 게시물이 물품을 가용하게 만드는 일은 결코 없다.
한 채널의 판매는 다른 채널을 닫아야 한다
마켓플레이스가 판매를 알리면, 수신 처리 역시 멱등한 inbox를 거친다. 원시 이벤트는 보존되고, 공급자 식별자로 중복 제거되며, 로컬 트랜잭션에서 적용된다.
available → sold 전이는 잠금, 버전 비교, 조건부 갱신 중 하나를 사용해야 한다. 그래야 동시에 발생한 두 이벤트가 데이터베이스에서 같은 단위를 소비할 수 없다.
이것이 물리적 위험을 완전히 없애지는 않는다. 내리기가 전파되기 전에 두 구매자가 두 외부 플랫폼에서 거의 동시에 확정할 수 있다. 아키텍처는 이 창을 크게 좁히고, 충돌을 감지하며, 해결 절차를 제공한다. 최대한의 보장은 각 공급자가 허용하는 웹훅, 예약 메커니즘, 지연에 달려 있다.
웹훅은 대사를 대체하지 않는다
웹훅은 유실되거나, 늦게 오거나, 여러 번 올 수 있다. 어떤 플랫폼은 모든 이벤트에 웹훅을 제공하지 않는다. 따라서 이벤트 기반 아키텍처에서도 폴러와 대사 작업은 여전히 필요하다.
cron은 업무 로직을 담아서는 안 된다. cron은 멱등한 작업을 촉발하며, 그 작업은 동시 실행으로부터 보호되고 다른 처리와 같은 시스템에서 관찰 가능해야 한다.
합리적인 초기 주기는 다음과 같을 수 있다.
| 참고 주기 | 점검 | 기대 결과 |
|---|---|---|
| 1〜5분마다 | 웹훅 없는 판매와 예약 | 다른 채널을 신속히 닫기 |
| 10〜15분마다 | 활성 게시물의 상태와 수량 | 재고 어긋남 감지 |
| 매시간 | 멈춘 작업, 재시도, 만료된 예약 | 복구 또는 알림 |
| 매일 밤 | 전수 대사 | 고아 게시물과 놓친 판매 찾기 |
| 매일 아침 | 운영 요약 | 팀에 검토 우선순위 제공 |
이 빈도는 각 플랫폼의 한도와 이용 약관을 지켜야 한다.
이상 징후 중심의 대시보드
좋은 대시보드는 게시물 수를 보여주는 데서 그치지 않는다. 네 가지 질문에 빠르게 답해야 한다. 무엇을 가지고 있는가, 어디에 게시되어 있는가, 무엇이 어긋나고 있는가, 어떤 사람의 조치가 필요한가.
가장 유용한 지표는 다음과 같다.
- 가용, 예약, 판매, 검토 중인 물품
- 공급자별 활성 게시물
- 고아이거나 정규 물품이 없는 원격 게시물
- 가격, 수량, 상태의 어긋남
- 판매에서 다른 채널 내리기까지의 지연
- 처리되지 않은 가장 오래된 inbox/outbox 이벤트의 경과 시간
- 어댑터별 실패율과 재시도 횟수
- 미할당 사진 비율과 검토로 넘어간 그룹
- 인제스트된 물품당 AI 호출의 평균 비용
- 채널별 판매량과 마진
알림은 영향도로 분류할 수 있다.
- 치명적: 판매 충돌, 판매된 물품이 다른 곳에서 여전히 활성
- 높음: 처리되지 않은 판매 이벤트, 원격 내리기 실패
- 중간: 동기화가 어긋난 게시물, 멈춘 작업, 만료된 예약
- 정보: 검토율 상승 또는 인제스트 비용 편차
각 알림은 물품, 관련 게시물, 마지막으로 성공한 전이, 권장 조치를 가리켜야 한다. 맥락 없는 알림은 진단 작업을 운영자에게 떠넘길 뿐이다.
파이프라인을 다시 쓰지 않고 이 발전을 배포하기
이전은 점진적으로 유지할 수 있다.
- 정규 모델을 도입한다. 이미 지원되는 공급자의 초안을 만들기 전에 물품과 재고 단위를 영속화한다.
- 첫 공급자를 캡슐화한다. 기존 연동을 어댑터로 바꾸고 원격 식별자를 체계적으로 저장한다.
- outbox, inbox, 대사 작업을 추가한다. 채널 수를 늘리기 전에 재시도를 멱등하게 만든다.
- 두 번째 유통 채널을 연결한다. 초안부터 시작해 게시, 마지막으로 판매 회신을 다룬다.
- 멀티채널 종료를 자동화한다. 지연을 측정하고, 충돌을 테스트하며, 사람의 해결 모드를 남긴다.
- 알림과 대시보드를 구축한다. 병렬 카운터가 아니라 같은 이벤트와 상태에서 공급한다.
이미지 파이프라인은 eBay나 Leboncoin을 알 필요가 없다. 정규 물품과 검증된 미디어를 만들어낸다. 어댑터는 사진이 어떻게 묶였는지 알아서는 안 된다. 이 경계 덕분에 AI 모델, 인터페이스, 공급자를 독립적으로 발전시킬 수 있다.
결론
사진 몇 장에서 멀티채널 유통으로 넘어가는 일은 API 목록에 루프를 하나 더하는 것이 아니다. 진실이 어디에 사는지, 누가 재고를 소유하는지, 각 전이를 어떻게 재실행·감사·보상할 수 있는지를 결정하도록 강제한다.
따라서 견고한 파이프라인은 두 단으로 세워진다.
- 불확실한 시각적 입력을 검증된 정규 물품으로 바꾸기
- 생애주기의 통제권을 넘기지 않고 그 물품을 여러 채널로 투영하기
AI는 식별, 기술, 가격 책정을 가속한다. 정규 데이터베이스, 트랜잭셔널 전이, 멱등한 어댑터, 대사가 운영을 지킨다. 설득력 있는 자동화를 진짜 재고 시스템으로 바꾸는 것은 모델 하나가 아니라 바로 이 조합이다.