pile·
백엔드·마켓컬리마켓컬리 Hello World·

컬리의 입고 시스템이 외부 인입 데이터를 안전하게 동기화하는 방법

1P 직매입에서 3PL 확대로 다양한 규격의 입고 예정 정보를 외부 파트너로부터 받게 되면서, DB 쓰기와 카프카 발행의 원자성 부재로 생기던 데이터 불일치를 Outbox 패턴 + Spring Kafka RetryableTopic으로 해결한 기록이다.

핵심 포인트
  • 기존엔 인터페이스 DB 저장과 카프카 발행이 분리돼 발행 후 커밋 실패 시 불일치가 났고, 컨슈머는 파트너 미등록 등으로 처리 실패 후 수동 개입이 필요했다.
  • Outbox 패턴으로 DB 쓰기와 메시지 발행을 한 트랜잭션에 묶어 원자성을 보장하고, namastack-outbox 0.3.0으로 테이블·폴링·재시도·인스턴스 관리를 위임했다.
  • RetryableTopic으로 컨슈머 실패 시 재시도 토픽으로 이동, 입고 정보가 1~2일 전 생성되는 특성을 활용해 10분 간격 144회(총 24시간) 자동 재시도한다.
  • 기본값 MULTIPLE_TOPICS는 재시도 토픽 144개를 만들어, SINGLE_TOPIC 전략으로 토픽 1개로 통합해 모니터링을 단순화했다.
  • 알림은 메인 실패만 슬랙, 재시도 실패는 무알림, 재시도 성공·DLT 도착만 알림으로 노이즈를 줄였다.
상세 정리
  • 레거시 실패: 파트너가 API를 호출하거나 컬리가 주기 조회 → 인터페이스 DB 적재 → 카프카 발행 → 리스너 변환의 흐름에서 발행과 커밋의 원자성이 없었다.
  • Outbox 라이브러리: namastack-outbox-starter-jpa 0.3.0(2025-10 릴리즈, Spring Boot 3), outbox_record 테이블(id·status·aggregate_id·event_type·payload·retry_count·next_retry_at 등) + 인덱스 6개.
  • 설정: poll-interval 2000ms, batch-size 10, exponential 재시도(initial 2000·max 60000·multiplier 2.0, max-retries 3), executor 4~8, heartbeat 5s.
  • Producer 변경: 카프카를 직접 발행하는 대신 OutboxRecord(eventType INBOUND_EXPECTATION_SAVE, payload는 메시지 JSON 직렬화)를 저장만 한다.
  • Poller: 별도 배치 인스턴스(@EnableBatchProcessing·@EnableScheduling·@EnableOutbox)가 OutboxRecordProcessor로 eventType을 분기해 카프카로 발행한다.
  • RetryableTopic 구현: @RetryableTopic(attempts=145 = 메인 1 + 재시도 144, @Backoff delay=600000ms=10분), @DltHandler가 최종 실패 시 슬랙 알림을 보낸다.
  • 이중 직렬화 문제: 메인 토픽에서 JsonSerializer로 직렬화된 메시지를 재시도 토픽에서 다시 직렬화하면 손상돼, retryKafkaTemplate은 StringSerializer로 직렬화를 스킵하도록 KafkaTemplate 2개로 분리했다.
  • 토픽 판별: TopicNameSet.isMainTopic/isRetryTopic으로 현재 토픽을 구분해 알림 정책을 적용한다.
  • 재시도 전략: 총 145회·10분 간격·최대 24시간으로, 입고 정보 생성까지의 1~2일 여유 동안 파트너·상품 동기화 완료를 기다린다.
  • 트레이드오프: 아웃박스 테이블 운영과 폴링 간격만큼의 발행 지연을 감수하는 대신, 검증된 라이브러리로 발행부/수신부 책임을 분리하고 자가 치유를 얻는다.
  • 결과: 원자성으로 메시지 유실이 불가능해지고, 24시간 자동 재시도로 수동 대응은 DLT만 남으며, 모니터링 토픽은 메인+재시도 2개로 줄었다.
왜 읽나Spring/Kafka로 외부 연동 메시지의 유실·순서 문제를 겪는 백엔드 개발자에게 Outbox + RetryableTopic 조합의 설정값·전략 레퍼런스.
마켓컬리
마켓컬리 Hello World 블로그
원문은 여기서 이어서 읽을 수 있어요
원문 읽기
읽음 (0)

이 글과 비슷한

  1. 백엔드·github-engGitHub Engineering·

    조기 종료를 없애야 벡터화된다 — 메모리 속도 소스 코드 케이스 폴딩

    GitHub의 코드 검색 엔진 Blackbird는 480TB 이상의 소스 코드를 인덱싱하기 전 모든 바이트에 case folding을 적용한다. 이 글은 Rust로 구현한 case folding을 메모리 대역폭 한계(45+ GiB/s)까지 끌어올린 두 가지 반직관적 최적화를 상세히 다룬다. 핵심은 루프 조기 종료(break) 제거로 LLVM 벡터화를 유도하고, UTF-8을 디코딩하지 않고 바이트 공간 산술만으로 fold를 수행하는 것이다.

    #rust#unicode#simd+2
  2. 백엔드·여기어때 (GC컴퍼니)여기어때 (GC컴퍼니)·

    트랜잭션 스크립트에서 숙소 메타 + 가격 계산 모듈로 — 전시 아키텍처 개선기 (2/3)

    여기어때 전시개발팀이 숙소 상세(PDP) API를 해부한 결과, 코드상으로는 DB 호출 3번처럼 보이던 요청이 실제로는 MongoDB $lookup 체인으로 컬렉션을 19회 접근하는 구조였다. 이 트랜잭션 스크립트 방식의 핵심 문제는 "aggregation이 I/O를 가린다"는 점으로, 독립적인 쿼리 10개가 단일 파이프라인에 직렬화되어 병렬화 기회를 잃고, 가격 때문에 거의 안 바뀌는 이미지까지 매 요청마다 읽어야 하는 읽기 증폭이 발생했다. V3에서는 "조회 시점 조립"을 "쓰기 시점 사전 조립"으로 전환하고, 화면별로 복제되던 가격 계산 로직을 goodsprice 단일 모듈로 수렴했다. 4개 API(PLP/PDP/RDP/ILP)의 반복 마이그레이션은 Claude Code skill로 절차를 고정하고 쉐도잉 + 동일성 검증으로 안전망을 마련하는 방식으로 진행됐다.

    #architecture#migration#caching+2