본문으로 건너뛰기
jonghun.dev

Backend Engineer경력 3년 5개월

주종훈 · 백엔드 개발자커머스 주문·결제와 메시징, 고객이 보는 홈·전시를 맡고 있는 백엔드 개발자입니다.

설로인(회원 약 44만 명, 비회원 포함 최대 MAU 약 80만)에서 커머스 서비스와 어드민 전체를 맡아 주문·결제, 홈·전시(홈 화면, 기획전, 타임딜), 쿠폰·마일리지, 프로모션, 알림, CRM을 개발·운영합니다.

이메일블로그

01 about

소개

2023년 5월부터 백엔드 개발을 하고 있습니다. 지금은 설로인에서 커머스 서비스와 어드민을 맡고 있고, 그중 주문·결제와 메시징, 고객이 처음 보는 홈 화면·기획전·타임딜 같은 전시 영역을 주로 다룹니다.

성능, 결제 안정성처럼 사용자가 바로 느끼는 문제를 원인부터 찾아 구조로 고치는 일을 가장 많이 해 왔습니다.

  1. 01

    결제 정합성

    아임포트와 토스페이먼츠를 함께 운영하다가 토스로 단일화했습니다. 그 과정에서 웹훅과 결제 처리의 락 키가 달라 중복 결제가 날 수 있는 잠재 결함을 찾아 수정했습니다.

  2. 02

    장애 대응

    운영 장애 4건(큐 소비 약 90시간 정지, 워커 OOM, 쿠폰 동시 발급 유실, 택배 API 무응답)의 원인을 찾아 수정했습니다.

  3. 03

    메시징 신뢰성

    결제 후속 처리를 아웃박스로 분리해 운영 30일 발행·저장 실패 0건을 유지하고 있고, 새 알림 경로는 Inbox(message_id)로 같은 알림이 한 번만 나가게 설계했습니다.

관련 글온라인 식품 커머스, 상품 유형에 따라 시스템이 달라진다이제 와서 고쳐보는 2024년의 내 코드

how I work기획 판단, AI 활용, 장애 대응 방식일하는 방식

02 experience

경력

총 경력 3년 5개월 (4년차)

문제마다 상황, 과제, 행동, 결과(STAR)로 정리했습니다. 카드를 누르면 자세한 내용이 열립니다.

주요 성과

  1. 017b11dHEAD main2025.01 ~ 현재

    SirLoin (주)설로인

    설로인 커머스 개발·운영

    회원 약 44만 명(비회원 포함 최대 MAU 약 80만) 규모 커머스의 서비스 서버와 어드민 서버를 맡고 있습니다. 주문·결제, 쿠폰·마일리지, 프로모션, 홈·전시, 알림, CRM 기능을 개발하고 운영합니다. 가장 큰 일은 토스페이먼츠 직접 연동이었습니다.

    Payment

    토스페이먼츠 직접 연동과 PG 전환
    S 상황·제약

    마케팅 프로모션에 토스 직접 연동이 필요했지만, 결제 코드가 아임포트에 묶여 있었고 운영 결제는 멈출 수 없었습니다.

    T 과제

    결제를 멈추지 않고 6가지 흐름을 옮기기. 전환 중 중복 승인 0.

    A 판단·행동
    • 한 번에 교체 대신 두 PG 병행 운영을 택함
    • PaymentClient 인터페이스로 PG 추상화
    • 분산 락 + Idempotency-Key 두 겹, 웹훅 Secret 검증
    • 웹훅·결제 락 키 불일치(중복 결제 가능) 수정
    R 결과
    • 토스 결제 비중0%→첫 30일 약 78%운영 로그 2026.04~05
    • 중복 결제 위험웹훅·결제 락 키 불일치→첫 30일 락 실패·서명 불일치 0건운영 로그
    자세히 보기

    Architecture

    결제와 물류 연동 분리 (Outbox, Kafka)
    S 상황·제약

    월 1만 건 이상의 결제가 WMS를 동기로 호출해, 물류가 느리거나 발행이 실패하면 결제와 후속 처리가 함께 멈췄습니다.

    T 과제

    물류가 실패해도 결제는 끝나고, 물류로 보낼 일은 잃지 않게 하기.

    A 판단·행동
    • 결제와 아웃박스 저장을 한 트랜잭션으로, 물류 전달은 릴레이가 Kafka로
    • CDC 대신 폴링 + 5분 주기 재발행 릴레이를 택함
    • 공유 아웃박스 5초 폴링 릴레이 → Kafka로 OMS 발행 직접 구현
    R 결과
    • 결제 응답WMS 응답을 기다림→아웃박스 저장까지만 하고 응답물류 전달은 릴레이가 Kafka로
    • 발행 실패 시배송·마일리지 처리 멈춤→5분 주기 릴레이가 재발행최대 10회, 이후 Slack
    • 운영 30일 발행·저장 실패후속 처리 정지 장애→0건운영 로그 2026.09~10

    관련 글Outbox만으로 충분할까? CDC와 DLQ를 통한 기초적인 이벤트 기반 설계를 알아보자Coroutine과 ReactiveMongo 다중DB 환경에서 @Transactional을 사용해 보자MongoDB의 WiredTiger Engine과 Transaction에 대해서 알아보자

    자세히 보기

    Performance2025.05 ~ 2025.10

    홈 화면 조회 성능 개선
    S 상황·제약

    명절 최대일 트래픽이 평시의 약 5~10배인데, 홈 바디 조회는 영역을 순서대로 읽어 평균 782ms(k6)였습니다.

    T 과제

    명절 전에 홈 응답을 줄이고, 같은 조건으로 다시 재서 확인하기.

    A 판단·행동
    • 병목 3가지 특정: 순차 조회, 선형 탐색, 캐시 부재
    • 의존 없는 영역 조회만 코루틴 병렬로
    • 호출 수는 DataLoader와 Redis 캐시로 줄임
    • k6 + InfluxDB로 같은 조건 재측정
    R 결과
    • 홈 바디 평균 응답782ms→212msk6 로컬 단일 VU
    • 바로가기 조회75ms→3msRedis 캐시 히트 시

    관련 글비동기로 성능개선을 한다고? Coroutine, Semaphore, BulkHead로 안정적인 성능 만들기코틀린 코루틴의 정석 (1): 실무 중심으로 다시 공부하는 코루틴

    자세히 보기

    Incident2026.08

    워커 메시지 소비 중단 장애 대응
    S 상황·제약

    큐 선언 실패 세 번에 리스너가 영구 정지해, 그 큐의 소비가 약 90시간 멈췄고 어떤 경보도 울리지 않았습니다.

    T 과제

    사람 없이 스스로 복구되고, 멈추면 바로 알기.

    A 판단·행동
    • 지수 백오프(5초~최대 5분)로 큐 재선언
    • 재선언 실패 시 Slack 경보
    • 파드 교체 시 취소 전파 + 최대 15초 드레인
    R 결과
    • 큐 선언 실패 시약 90시간 소비 정지, 재배포로 복구→5초~최대 5분 간격 자동 재선언운영 로그 · 설정값
    • 최근 30일 큐 선언 실패 279회3번 실패하면 영구 정지→영구 정지 0회운영 로그 2026.09~10
    • 파드 교체 시에러 115건→취소 전파 + 15초 드레인운영 로그
    자세히 보기

    Messaging2026.09 ~

    알림 발송 파이프라인: 중복 발송을 막고, 결과를 모르는 건은 숨기지 않게

    설계·구현 전체 담당

    S 상황·제약

    월 약 2만 건의 거래성 알림톡을 상주 워커가 직접 보내, 재시도·배포 때 사라지거나 두 번 나갈 수 있었습니다.

    T 과제

    같은 알림은 한 번만 보내고, 결과를 모르는 건은 숨기지 않기.

    A 판단·행동
    • Outbox · Relay · Broker · Inbox · 벤더 경계로 책임 분리
    • quorum 큐 + DLX, 재시도 가치에 따라 TTL 재시도와 파킹을 나눔
    • Inbox: message_id로 SENDING 선점, 한 번만 발송
    • 재발송 대신 ERROR + Slack을 택함
    R 결과
    • 재배달·재발행 시같은 알림 중복 발송 가능→message_id 선점으로 한 번만구조 변경
    • 벤더 무응답 시결과를 알 수 없음→ERROR로 남기고 Slack 알림구조 변경
    • 배포·노드 교체 시알림 유실 가능→quorum 큐에 남아 재전달구조 변경, 운영 반영

    관련 글Outbox만으로 충분할까? CDC와 DLQ를 통한 기초적인 이벤트 기반 설계를 알아보자

    자세히 보기

    Batch2026.09 ~

    commerce-batch: 스케줄 잡을 워커에서 분리

    2인 공동 프로젝트. 본인: 서비스 구조·잡 실행 구조(CronJob 차트·공통 러너)·실패 지점 재개·알림 발송 대기열 중계

    S 상황·제약

    결제 후속 처리·알림과 같은 워커에서 스케줄 잡이 겹치면 힙이 넘쳐 OOM이 났고, 피크에 맞춰 키우면 비용이 하루 종일 들었습니다.

    T 과제

    잡은 돌 때만 자원을 쓰고, 같은 회차는 한 번만, 끊기면 실패 지점부터.

    A 판단·행동
    • Kubernetes CronJob + Spring Batch, 잡마다 전용 파드
    • Forbid + JobInstance로 같은 회차 중복 완료 차단
    • keyset reader + 청크 커밋(50~500건)으로 재개
    • 실패 시 Slack 경보, ArchUnit 12개·커버리지 70% 게이트
    R 결과
    • 실행 위치결제 후속 처리·알림과 한 워커에 상주→잡마다 전용 파드, 끝나면 종료배포 설정
    • 실패 후 재처리어디까지 처리했는지 모름→마지막 커밋 청크 다음부터 재개Testcontainers 검증
    자세히 보기
    • Kotlin
    • TypeScript
    • Node.js
    • Spring Boot
    • Express
    • Prisma
    • Coroutine
    • Webflux
    • GraphQL
    • MongoDB
    • PostgreSQL
    • Redis
    • RabbitMQ
    • Kafka
    • ArgoCD
  2. 99fb5542023.05 ~ 2025.01

    Orangei (주)오렌지아이

    평가 데이터 관리·모니터링 SaaS (BTS)

    2023.08 ~ 2025.01

    2차전지 기업의 평가 데이터를 관리하고 모니터링하는 B2B SaaS입니다. 4인 팀(BE 2, FE 2)에서 백엔드를 맡았고, 론칭 후 3개 고객사(삼성전기 등)가 운영했습니다.

    Performance

    대용량 데이터 조회 성능 개선
    S 상황·제약

    기업별 평가 데이터가 하루 120만 건 이상 쌓이면서 조회가 눈에 띄게 느려졌습니다.

    T 과제

    여러 고객사가 함께 쓰는 만큼 기업별 데이터를 분리하면서 조회 속도도 올려야 했습니다.

    A 판단·행동
    • 멀티테넌시로 기업별 데이터를 분리
    • 복합 인덱스로 쿼리 실행 계획 개선
    • 분산 인스턴스 환경에 Redis 캐시 적용
    R 결과
    90%
    대용량 조회 응답 시간 단축
    자세히 보기

    Cost

    배치 서버를 서버리스로 전환
    S 상황·제약

    배치는 잠깐 도는데 Spring Batch 서버는 상시 떠 있어 유휴 비용과 운영 부담이 컸습니다.

    T 과제

    배치가 도는 동안만 자원을 쓰는 구조로 바꾸기로 했습니다.

    A 판단·행동
    • 배치를 AWS Lambda, EventBridge, SQS 기반으로 전환
    R 결과
    약60%
    배치 운영 비용 절감
    자세히 보기

    DevOps

    Jenkins CI/CD 도입
    S 상황·제약

    수동 배포에 30분이 걸려 릴리스가 밀리고 실수할 여지가 있었습니다.

    T 과제

    배포를 자동화하고 정적 분석을 파이프라인에 넣었습니다.

    A 판단·행동
    • Jenkins CI/CD 파이프라인 도입
    • SonarQube와 Quality Gate로 정적 분석
    R 결과
    30분 → 2분
    배포 시간
    자세히 보기
    • API
      백엔드 개발
      • JWT 토큰 인증, Role 기반 접근 제어, tenant 인증·전환 필터
      • 고객사·프로젝트·평가 도메인 REST API, S3 기반 계약서 관리
      • 프로젝트 생성·수정 API에 DB 락으로 동시성 처리
    • Java
    • Spring Boot
    • JPA
    • Querydsl
    • Spring Security

    평가 데이터 압축 모듈 (Java)

    2024.04 ~ 2024.10

    충방전기 시계열 데이터를 저장하기 위한 사내 압축 모듈입니다. Gorilla 논문을 분석해 부동소수점 압축을 구현했습니다.

    Algorithm

    Gorilla 알고리즘 기반 시계열 압축
    S 상황·제약

    시계열 평가 데이터가 빠르게 쌓이는데, 모든 칼럼에 같은 ZIP을 쓰고 있었습니다.

    T 과제

    칼럼 성격에 맞춘 무손실 압축으로 ZIP보다 작고 빠르게.

    A 판단·행동
    • 칼럼 유형별 알고리즘: Gorilla-Value · TimeStamp · RLE · RLE Delta
    • 실데이터 100만 건 벤치마크: 압축률보다 속도를 택해 Gorilla-Value
    • 애플리케이션 계층 압축, 끝 비트 문제는 row 수 저장으로 해결
    • Private Maven 배포로 사내 공용 모듈화
    R 결과
    • 저장 공간ZIP(Deflate) 일괄 적용→70% 절감사내 벤치마크, ZIP 대비
    • 압축·해제 속도ZIP→45.62% 향상사내 벤치마크, ZIP 대비

    관련 글데이터 압축을 위한 Gorilla 알고리즘 적용 사례: 사내 솔루션 개발기 및 회고

    자세히 보기
    • Java 8
    • Gradle
    • PostgreSQL
    • Private Maven

03 more work

그 밖의 작업

사례로 따로 다루지 않은 작업 중 신규 기능, 장애 개선, 성능 개선을 골랐습니다. 쿠폰·등급·알림 같은 CRM 작업은 CRM으로 표시했습니다.

  • 신규 기능Admin2026.10

    어드민 MCP 세팅·정책 조회 툴 (전사 Claude 연동)

    어드민 서버를 MCP 서버로 만든 팀 작업에서, 사이드바 화면의 세팅값을 읽는 툴 99개와 노션 정책 문서 툴을 설계·구현했습니다. 툴은 화면이 쓰는 resolver·service를 그대로 부르고, 권한은 그 화면이 속한 메뉴의 읽기 권한을 따릅니다. 회원·주문 추출도 신 고객등급제 기준으로 맞췄습니다.

    결과운영자가 전사 Claude에서 상품·배송·혜택 등 8개 메뉴의 세팅값과 커머스 정책을 채팅으로 조회합니다. 툴 99개를 공통 정의 하나로 같은 권한·응답 형식에 맞췄습니다.

    • Node.js
    • TypeScript
    • MCP SDK
    • Zod
    • Notion API
  • 신규 기능Commerce2026.10

    LMS 대체 문자용 자체 단축 URL

    외부 단축 URL 서비스가 종료된 뒤 알림톡·브랜드 메시지의 대체 문자(LMS)에 긴 UTM 주소가 그대로 나갔습니다. base62 7자 자체 단축 링크로 바꾸고, 같은 원본은 같은 코드를 쓰게 했습니다.

    결과500ms 안에 줄이지 못하면 원본 주소로 보내, 단축 때문에 발송이 막히지 않게 했습니다.

    • Kotlin
    • PostgreSQL
    • base62
    • LRU Cache
  • 신규 기능CRM2026.07 ~ 2026.10

    신 고객등급제 어드민·API

    회원 등급 API를 GraphQL에서 REST(Prisma, Swagger)로 다시 만들었습니다. 등급 혜택은 버전으로 관리하고, 강제조정은 기존 이력을 건드리지 않고 새 버전 행을 쌓는 방식으로 구현했습니다.

    같은 강제조정 요청 5건을 동시에 보내도 등급 버전은 1개만 생성 (통합 테스트)

    • Node.js
    • TypeScript
    • Prisma
    • PostgreSQL
    • Kotlin
  • 장애 개선Commerce2026.09

    워커 OOM 대응

    증상
    워커가 연달아 OOM으로 종료됐습니다.
    원인
    쿠폰 컬렉션 전체 스캔, 만료 알림 대상 전량 적재, 엑셀 파일 전체를 힙에 올리는 구조.
    조치
    필요한 구간만 조회하고, 고객 500명 단위로 나눠 처리하고, 엑셀은 임시 파일 스트리밍으로 바꿨습니다.

    워커 OOM 4일 3회 → 배포 후 17일 0회 (같은 날 배포된 힙 설정 변경과 함께)

    • Kotlin
    • Coroutine Flow
    • MongoDB
    • Apache POI
    • AWS S3
  • 신규 기능Admin2026.08 ~ 2026.09

    진열 스케줄·편성표

    기획전·타임딜·매거진을 스케줄로 묶어 편성하는 어드민 기능입니다. 편성 저장, 타임라인 편성표, 기획전 기간을 바꿀 때 스케줄 겹침 검증을 만들었습니다.

    편성 건수와 상관없이 편성 콘텐츠 조회를 1회로 고정

    • Node.js
    • TypeScript
    • MongoDB
  • 성능 개선Commerce2026.08 ~ 2026.09

    조회 API REST 전환과 응답 캐싱

    기획전 오픈 직후 요청이 평시 분당 300~700건에서 최대 약 2만 건(30배 이상)으로 늘고 그날 5xx가 났습니다. 상품그룹마다 $lookup을 세 번씩 반복하던 조회를 분리 조회로 바꿔 쿼리 수를 고정하고, 완성된 응답은 Redis에 1분 캐싱했습니다.

    p95 스토어 438ms → 98ms, 기획전 360ms → 63ms (7일)

    • Kotlin
    • MongoDB
    • Redis
  • 성능 개선Commerce2026.08

    코루틴 캐시 계층

    Spring @Cacheable이 suspend 함수를 지원하지 않아 캐시가 계속 빗나가고 있었습니다. 함수 호출형 캐시를 만들고, Redis 장애 시에는 원본 조회로 넘어가게 했습니다.

    Redis 장애 시 캐시 대기 상한 9초 → 200ms

    • Kotlin
    • Coroutine
    • Redisson
    • Redis
  • 신규 기능Commerce2026.08

    증정품(사은품) 기능

    어드민에서 증정품을 제품과 연결해 관리하고, 결제 완료된 배송에 증정품을 지급해 물류 수집에 반영합니다. 잔여 수량이 다음 1회 제공량보다 적어지면 Slack으로 알립니다.

    100개 등록에 200개가 출고되던 버그 수정, 잔량 조건부 원자 갱신으로 초과 지급 차단

    • Kotlin
    • Node.js
    • MongoDB
  • 장애 개선CRM2026.08

    쿠폰 동시 발급 유실 수정

    증상
    코드 쿠폰 여러 장을 한꺼번에 받으면 일부가 에러 없이 사라졌고, 발급 이력은 남아 다시 받을 수도 없었습니다.
    원인
    쿠폰 문서를 읽고 배열에 더해 통째로 다시 저장하는 구조.
    조치
    조건부 원자적 추가($push + $cond)로 바꿔 동시 발급과 중복 클릭을 모두 처리했습니다.
    • Kotlin
    • MongoDB
  • 성능 개선Admin2026.02 ~ 2026.07

    어드민 엑셀 다운로드 비동기화

    주문·배송 엑셀은 쿼리를 묶고 스트리밍으로 생성하게 바꿨습니다. 이후 엑셀 12종을 RabbitMQ 요청, 워커 생성, S3 보관, Slack 전달 방식으로 옮겨 어드민 서버에서 무거운 작업을 뺐습니다.

    주문 엑셀 5,000건 기준 쿼리 약 700회 → 350회, 동시에 메모리에 올라가는 셀 30만 → 60

    • Node.js
    • ExcelJS
    • RabbitMQ
    • Kotlin
    • AWS S3
  • 신규 기능CRM

    고객 기념일 쿠폰

    고객 기념일에 맞춰 쿠폰을 지급하는 배치입니다. 지급 실패를 회원 단위로 격리해, 한 명이 실패해도 나머지 고객에게는 쿠폰이 나가게 했습니다.

    • Kotlin
    • Spring Batch
  • 신규 기능CRM

    멀티 채널 알림 배치

    고객별 수신동의 범위에 맞는 채널(FCM 푸시, 카카오 친구톡·브랜드메시지, LMS)로 보내고, 실패한 대상만 다음 채널로 다시 보냅니다. 푸시에 성공한 대상에게도 대체 발송이 나가던 버그를 찾아 수정했습니다.

    • Kotlin
    • FCM
    • RabbitMQ
  • 신규 기능CRM

    회원·비회원 행동 로그 수집

    상품 조회, 장바구니, 가입, 로그인 같은 행동을 회원과 비회원 모두 같은 구조로 쌓는 로그 파이프라인을 만들었습니다. CRM 타겟 메시징에 씁니다.

    • Kotlin
  • 신규 기능CRM

    행동 기반 인앱 메시지·배너

    고객 행동과 상태 조건으로 인앱 메시지, 플로팅 배너, 띠 배너를 노출합니다. 운영팀이 배포 없이 노출 조건과 템플릿을 바꿀 수 있게 했습니다.

    • Kotlin
    • Node.js
  • 신규 기능Commerce

    타임딜·프레스티지

    타임딜 일정 알림을 1분 주기로 보내고, 일정이 바뀌면 구독을 초기화해 잘못된 알림이 가지 않게 했습니다. 프레스티지 상품은 0원 결제를 뺀 결제에만 마일리지를 추가 적립합니다.

04 skills

기술 스택

01Backend

  • Kotlin
  • Spring Boot
  • Webflux
  • Coroutine
  • TypeScript
  • Node.js
  • Express
  • Prisma
  • Java
  • JPA
  • QueryDSL

02Messaging

  • RabbitMQ
  • Kafka

03Database

  • MongoDB
  • PostgreSQL
  • Redis
  • Amazon RDS

04DevOps · Cloud

  • Kubernetes
  • ArgoCD
  • Docker
  • Jenkins
  • ECR
  • AWS Lambda
  • SQS

05Observability

  • Grafana
  • Loki
  • Elasticsearch
  • Fluentd
  • Kibana

05 education

학력·교육

  1. 2024.09 ~ 2024.11항해99 Backend Plus수료
  2. 2022.11 ~ 2023.05KH정보교육원수료
  3. 2019.03 ~ 2023.08용인대학교 컴퓨터과학과졸업

06 contact

연락처

채용이나 협업 관련 연락은 메일로 부탁드립니다.

wn1331@gmail.com

GitHub 활동 · 최근 1년기여 4274회

최근 1년 기여 합계 4274회. 개인 101회, 회사 4173회. 기여한 날 235일, 가장 많았던 날 2026.09.29 (84회).

10월11월12월1월2월3월4월5월6월7월8월9월10월월수금

Pangyo, South Korea

© 2026 주종훈Astro로 빌드해 S3 + CloudFront에서 서빙합니다build 2026-10-31 06:02 UTC

사이트 디자인과 구현에는 Claude를 활용했습니다. 경력 내용과 수치는 직접 확인했습니다.

+ details

자세히 보기 (STAR)

SirLoin · 설로인 커머스 개발·운영토스페이먼츠 직접 연동과 PG 전환
S · 상황·제약
마케팅팀이 결제수단별 PG 프로모션(퀵 계좌이체 즉시할인 등)과 제휴를 하려면 토스페이먼츠가 필요했습니다. 아임포트를 거쳐 토스를 붙이면 수수료가 더 드는 구조라 직접 연동해야 했지만, 결제 코드는 아임포트(KCP)를 직접 의존하고 있었습니다. 가장 큰 제약은 운영 중인 결제를 멈출 수 없다는 점이었습니다. 회원 약 44만 명이 쓰는 카드, 가상계좌, 0원 결제, 취소·환불, 빌링키, 웹훅 6가지 흐름을 바꿔야 했습니다.
T · 과제
결제를 멈추지 않고 PG를 직접 연동으로 바꾸고, 전환 중에도 중복 승인이나 결제 상태 불일치가 생기지 않게 하는 것이 목표였습니다.
A · 판단·행동
  • 아임포트를 거친 연동과 직접 연동을 비교해, 수수료 차이 때문에 직접 연동을 택함
  • 6가지 흐름을 한 번에 바꾸면 되돌리기 어렵다고 판단해, 두 PG를 함께 운영하며 옮기기로 함. 그러려면 서비스 코드가 PG를 몰라야 해서, PG마다 다른 결제 API를 PaymentClient 인터페이스 하나로 추상화하고 아임포트와 토스페이먼츠를 구현체로 둠
  • 카드, 가상계좌, 0원 결제, 취소·환불, 빌링키, 웹훅 6가지 흐름을 토스 기준으로 구현
  • 승인 요청과 웹훅이 거의 동시에 들어올 수 있어, 분산 락(Redisson)과 Toss Idempotency-Key를 함께 써 중복 승인을 두 겹으로 막고, 웹훅은 Secret으로 검증
  • 기존 흐름을 읽다가 웹훅과 결제 처리의 락 키가 달라 같은 결제를 동시에 처리할 수 있는 잠재 결함을 찾아 같은 키로 맞춤
  • 승인 이후 후속 처리가 실패하는 지점마다 로그와 운영 알림을 남기고, 주요 결제 시나리오는 통합 테스트로 고정
R · 결과
  • 토스 결제 비중: 0% → 첫 30일 약 78% (운영 로그 2026.04~05)
  • 중복 결제 위험: 웹훅·결제 락 키 불일치 → 첫 30일 락 실패·서명 불일치 0건 (운영 로그)
  • 약 78% 전환 첫 30일 결제 중 토스 처리 비중
  • 0건 첫 30일 웹훅 락 실패·타임아웃·서명 불일치
전환 후 첫 30일 동안 결제의 약 78%가 토스로 처리됐고, 이후 아임포트를 제거해 토스로 단일화했습니다. 첫 30일 동안 웹훅 결제 처리의 락 획득 실패·락 타임아웃·서명 불일치는 0건이었습니다.
서비스 레이어PaymentClient«interface»아임포트구현체토스페이먼츠구현체
서비스 코드는 PaymentClient 인터페이스에만 의존하고, 두 PG 구현체가 함께 동작합니다

Kotlin · Toss Payments · Redisson · Idempotency-Key · Webhook

SirLoin · 설로인 커머스 개발·운영결제와 물류 연동 분리 (Outbox, Kafka)
S · 상황·제약
운영 결제는 한 달에 1만 건 이상입니다. 결제가 끝나면 커머스 서버가 WMS API를 동기로 호출했고, 알림톡·결제 취소 메시지도 결제 흐름 안에서 바로 발행했습니다. 그래서 물류가 느리거나 실패하면 주문·결제 응답이 같이 늦어졌고, 발행이 한 번 실패하면 배송·물류·마일리지 후속 처리 전체가 멈추는 장애가 있었습니다.
T · 과제
물류가 느리거나 실패해도 결제는 끝나야 하고, 물류로 보낼 일은 하나도 잃지 않아야 했습니다. 실패하면 사람이 알 수 있어야 했습니다.
A · 판단·행동
  • 결제 승인과 아웃박스(PENDING) 저장을 한 트랜잭션으로 커밋하고, 물류(OMS)로 넘길 일은 커밋 뒤 바로 보내지 않고 릴레이가 아웃박스를 폴링해 Kafka로 발행. 발행과 SENT 표시는 서로 다른 시스템이라 원자적으로 묶을 수 없어 at-least-once로 설계하고, 발행에 성공한 것만 조건부로 SENT 표시
  • 발행 방식은 CDC(Debezium)와 폴링을 비교함. 배송·주문 전달은 초 단위 실시간성이 필요 없는데 CDC는 초기 세팅과 운영 부담이 커서 오버 엔지니어링이라고 보고, 폴링과 재발행 릴레이를 택함
  • 알림톡·결제 취소·마일리지 같은 후속 메시지는 커밋 뒤 RabbitMQ로 바로 발행하고, 실패해 PENDING으로 남은 건은 5분 주기 릴레이가 생성 후 60초가 지난 것부터 다시 발행. 10번 넘게 실패하면 FAILED로 두고 Slack 알림
  • 주문·취소처럼 OMS로 넘길 일은 같은 트랜잭션에서 공유 아웃박스에 적재하고, 5초 주기 릴레이가 아웃박스를 폴링해 CAS로 PROCESSING을 선점한 뒤 Kafka로 OMS에 발행하도록 직접 구현. 발행 결과를 모르는 FAILED는 지우지 않고 다음 폴링에서 다시 시도
R · 결과
  • 결제 응답: WMS 응답을 기다림 → 아웃박스 저장까지만 하고 응답 (물류 전달은 릴레이가 Kafka로)
  • 발행 실패 시: 배송·마일리지 처리 멈춤 → 5분 주기 릴레이가 재발행 (최대 10회, 이후 Slack)
  • 운영 30일 발행·저장 실패: 후속 처리 정지 장애 → 0건 (운영 로그 2026.09~10)
  • 0건 운영 30일 아웃박스 발행·저장 실패
  • 0건 운영 7일 릴레이 FAILED
물류가 느리거나 실패해도 결제는 아웃박스 저장까지로 끝납니다. OMS로 넘길 주문은 아웃박스를 폴링하는 릴레이가 Kafka로 발행하고, 후속 메시지는 실패분을 5분 주기 릴레이가 다시 보냅니다. 운영 30일 동안 후속 메시지 즉시 발행·SENT 표시·저장 실패가 0건이었고, 운영 7일 동안 릴레이 실행 실패와 FAILED도 0건이었습니다.
같은 트랜잭션주문 · 결제 완료 저장Outbox 저장릴레이5초 폴링 · CASKafkaOMS주문 관리
결제 승인과 아웃박스 저장을 한 트랜잭션으로 커밋합니다. OMS로 넘길 주문은 커밋 뒤 바로 보내지 않고 5초 주기 릴레이가 아웃박스를 폴링해 CAS로 선점한 뒤 Kafka로 발행하고, 알림·취소 같은 후속 메시지는 RabbitMQ로 발행한 뒤 실패분을 5분 주기 릴레이가 다시 보냅니다

후속 개선

WMS의 커머스 DB 직접 쓰기를 끊고 Kafka 이벤트를 직접 소비

이전: WMS가 OMS·WMS의 배송 이벤트를 받아 커머스 MongoDB에 직접 썼습니다. 현재: 커머스 워커가 같은 Kafka 토픽을 직접 소비하고, 인박스와 상태 선행조건으로 한 번만 반영합니다.

이전

  • 처음에는 위의 5초 폴링 릴레이를 WMS 쪽에 붙여, 커머스 아웃박스를 WMS가 CAS로 선점해 Kafka로 OMS에 발행하게 했습니다. 결제와 물류는 떨어졌지만, 아웃박스의 주인이 아닌 쪽이 발행하는 구조였습니다.
  • 송장 발급(→ 배송준비중), 자동 출고(→ 배송중), 주문 상태 변경, 배송정보·송장 수정, 수집 완료 표시 이벤트는 WMS 컨슈머가 받아 커머스 MongoDB(DB 3개, 컬렉션 5개)에 직접 썼습니다. WMS 관리자 화면도 커머스 배송·상품을 직접 조회하고 고쳤습니다.
  • 그래서 커머스가 모르는 사이에 커머스 데이터가 바뀌었습니다. 결제완료 → 배송준비중 전이는 커머스 코드에 없었고, 커머스가 상태를 바꿀 때 붙는 후속 처리도 WMS의 쓰기에는 붙지 않았습니다.
  • WMS의 중복 방지는 업무 갱신을 커밋한 뒤 inbox를 저장해 원자적이지 않았습니다. 현재 상태를 보지 않고 덮어써서 늦게 온 이벤트가 배송완료를 배송준비중으로 되돌릴 수 있었고, 이미 반영된 정상 중복은 DLQ 알람이 됐습니다.

현재

  • 커머스 워커가 배송 이벤트 토픽 8종을 직접 소비합니다. 토픽과 payload를 그대로 써서, 발행하는 OMS·WMS의 코드는 고치지 않았습니다.
  • 인박스 기록(토픽+이벤트 id 유니크 키)과 배송 조건부 갱신(상태 선행조건)을 배송 DB의 한 Mongo 트랜잭션으로 커밋합니다. 오프셋은 그 뒤에 수동 커밋합니다.
  • 결과는 APPLIED·NOOP·PARTIAL·REJECTED로 남깁니다. 업무 예외는 바로 DLQ로 보내고, 일시 오류만 백오프로 재시도합니다.
  • 전환은 섀도 전용 컨슈머 그룹(판정만 로그) → WMS 리스너 정지 → 오프셋 인계 순서로 합니다. 전환 기간에는 WMS inbox에도 같이 기록해, WMS로 되돌려도 겹치는 메시지는 WMS가 건너뜁니다.
  • OMS로 넘길 주문은 커밋 뒤 바로 보내지 않고, 커머스 api의 릴레이가 아웃박스를 폴링해 CAS로 선점한 뒤 Kafka로 발행합니다. 아웃박스 주인이 발행까지 맡습니다. WMS 화면의 조회·재수집·상품 연결은 커머스 내부 API로 바꿔 WMS가 가진 커머스 MongoClient를 없앴습니다.
  • DB 쓰기
  • DB 읽기
  • Kafka
  • REST
  • WMS → 커머스 DB
커머스 worker커머스 MongoDB배송 · 상품커머스 apioutbox커머스 MongoDBKafka토픽OMSWMSWMS 컨슈머직접 쓰기직접 조회5초 폴링 · CAS릴레이토픽 8종 직접 소비인박스 + 갱신조회 · 연결커머스 내부 API

WMS가 커머스 MongoDB에 직접 붙어 배송·아웃박스·상품을 읽고 씁니다(붉은 선). 커머스는 자기 데이터가 바뀐 것을 모릅니다.

결합 지도: 이전에는 WMS가 커머스 MongoDB에 직접 쓰고 읽었고(붉은 선), 현재는 커머스 워커가 Kafka 이벤트를 직접 소비하고 화면용 조회만 내부 API로 받습니다
  1. 이벤트 도착

    자동 출고 이벤트(eventId e-101)가 옵니다. 배송은 배송준비중입니다.

  2. 사전 확인

    인박스에 출고토픽:e-101 키가 없으니 처리를 이어갑니다.

  3. 한 트랜잭션으로 반영

    인박스 insert와 조건부 갱신(상태가 결제완료·배송준비중이면 배송중)을 같은 배송 DB 트랜잭션으로 커밋합니다. 1건 반영.

  4. 오프셋 커밋

    DB 커밋이 끝난 뒤에 Kafka 오프셋을 수동 커밋합니다.

  5. 같은 eventId 재도착

    재발행으로 같은 이벤트가 다른 오프셋에 다시 실려 옵니다. 인박스에 키가 있어 반영하지 않고 오프셋만 커밋합니다.

  1. 현재 상태

    배송은 이미 배송완료입니다.

  2. 늦은 이벤트 도착

    송장 발급(→ 배송준비중) 이벤트 e-087이 뒤늦게 옵니다. 인박스에는 키가 없습니다.

  3. 상태 선행조건 불일치

    조건부 갱신의 조건은 결제완료·배송준비중이라 0건이 바뀝니다. 배송완료가 되돌아가지 않고 NOOP으로 인박스에 남습니다.

  4. 오프셋 커밋

    정상 중복이라 DLQ로 보내지 않고 오프셋을 커밋합니다.

  5. eventId가 다른 같은 일

    같은 출고 요청이 한 번 더 들어와 eventId가 다른 출고 이벤트(e-102)가 옵니다. 인박스는 통과하지만 상태 선행조건이 맞지 않아 NOOP이고 오프셋을 커밋합니다.

인박스 키
없음
배송 상태
배송준비중
오프셋 커밋
대기
결과
—
1 / 5
두 겹의 멱등: 같은 eventId는 인박스 유니크 키가, eventId가 다른 같은 일과 늦게 온 이벤트는 상태 선행조건이 막습니다. 예시 값입니다
왜 Kafka인가
OMS·WMS가 이미 이 이벤트를 Kafka로 내고 있어 계약을 그대로 쓸 수 있었습니다. 파티션 키 단위로 순서가 지켜지고, 오프셋이 남아 섀도 검증·구간 재생·오프셋 인계로 전환하고 되돌릴 수 있습니다. 커머스가 쓰던 RabbitMQ는 소비한 메시지가 큐에서 사라져 이런 재생·인계 방식의 전환이 어렵습니다.
다른 방법과 뺀 이유
WMS 직접 쓰기에 가드만 더하는 안은 뺐습니다. 후속 처리 누락과 권한 문제가 그대로였습니다. OMS·WMS가 커머스 REST API를 부르는 안도 뺐습니다. 동기 결합이 돌아오고 발행 쪽을 고쳐야 했습니다. 다만 즉시 응답이 필요한 화면용 조회·연결은 내부 API로 했습니다. WMS 컨슈머 그룹에 합류해 파티션을 나누는 안은 리밸런스마다 다른 인박스로 재처리될 수 있고 롤백 지점이 불분명했습니다. 그래서 새 그룹 + 섀도 + 오프셋 인계를 택했습니다. 인박스는 DB를 넘는 트랜잭션이 안 되어 배송 데이터와 같은 배송 DB에 뒀습니다.
exactly-once 대신 at-least-once + 멱등 소비
Kafka의 exactly-once는 오프셋 커밋과 출력 토픽 쓰기를 한 트랜잭션으로 묶는 방식이라 Kafka에서 Kafka로 갈 때만 원자적입니다. 이 소비의 결과는 MongoDB 쓰기이고, 오프셋을 Mongo에 같이 저장하더라도 발행 쪽 재시도(아웃박스 재발행, 재시작 후 재전송)로 생긴 중복은 다른 오프셋으로 들어와 걸러지지 않습니다. 그래서 처리 후 오프셋을 커밋(at-least-once)하고, 인박스 유니크 키로 같은 eventId를 막고 상태 선행조건으로 eventId가 다른 같은 일과 늦게 온 이벤트를 막습니다.
CDC를 쓰지 않은 이유
배송 상태와 주문 전달은 초 단위 지연이 업무에 영향을 주지 않아 5초 폴링으로 충분했습니다. CDC(Debezium)를 쓰려면 Kafka Connect 클러스터와 MongoDB 커넥터 운영, change stream 권한, resume token·커넥터 장애 복구와 모니터링이 새로 필요합니다. 이벤트 모양을 맞추려면 아웃박스 컬렉션은 그대로 둬야 해서 얻는 것은 몇 초의 지연 단축뿐이었고, 월 1만 건 규모에 새 인프라를 늘리는 것은 오버 엔지니어링이라고 판단했습니다.

다음 단계

  • 굿스플로 배송추적 폴링을 OMS 워커로 옮겨 커머스는 이벤트만 받게 합니다.
  • 마지막 값으로 덮어쓰는 이벤트(배송정보·송장)에 발생 시각 가드를 둡니다.
  • 실시간성이 필요해지면 그때 CDC를 다시 검토합니다.

Kotlin · Webflux · MongoDB · Outbox · Inbox · Kafka · ShedLock

관련 글: Outbox만으로 충분할까? CDC와 DLQ를 통한 기초적인 이벤트 기반 설계를 알아보자

관련 글: Coroutine과 ReactiveMongo 다중DB 환경에서 @Transactional을 사용해 보자

관련 글: MongoDB의 WiredTiger Engine과 Transaction에 대해서 알아보자

SirLoin · 설로인 커머스 개발·운영홈 화면 조회 성능 개선
S · 상황·제약
명절에는 하루 트래픽이 평시의 약 5배(2025 추석)~10배(2026 설)까지 오르고, 명절 달 구매 고객은 평월의 약 3배입니다. 홈 바디 조회도 명절 최대일에 평시의 약 5~10배로 늘어납니다. 그런데 홈 바디 조회는 띠배너·기획전 영역을 하나씩 순서대로 조회하고, 상품 노출 여부를 리스트에서 선형 탐색했습니다. k6로 측정한 평균 응답이 782ms였고, 홈은 모든 사용자가 가장 먼저 부르는 화면이라 이 지연이 명절에는 그대로 서버 부하가 될 수 있었습니다. 명절 전에 운영에 반영해야 한다는 일정 제약도 있었습니다.
T · 과제
명절 전에 홈 바디 평균 응답을 줄이고, 같은 조건의 k6 측정으로 개선 폭을 확인하기로 했습니다.
A · 판단·행동
  • 병목을 세 가지로 좁힘: 영역별 순차 조회, 상품 노출 여부의 선형 탐색, 캐시 부재
  • 서로 의존하지 않는 영역 조회는 순서대로 기다릴 이유가 없어 코루틴으로 병렬화. 다만 병렬화는 대기 시간을 겹칠 뿐 호출 수는 그대로라, 반복 조회 자체는 DataLoader와 캐시로 줄이기로 함
  • GraphQL N+1은 DataLoader로 묶어 한 번에 조회
  • 모든 사용자가 같은 홈 데이터를 반복 조회한다는 점에서 캐시를 붙이고, 이후 바로가기 조회에도 Redis 캐시를 붙임(캐시 히트 시 75ms → 3ms)
  • k6 + InfluxDB로 변경 전과 같은 조건(로컬, 단일 VU)에서 다시 측정
R · 결과
  • 홈 바디 평균 응답: 782ms → 212ms (k6 로컬 단일 VU)
  • 바로가기 조회: 75ms → 3ms (Redis 캐시 히트 시)
  • 약 72% 홈 바디 평균 응답 782ms → 212ms (k6, 로컬 단일 VU)
  • 75ms → 3ms 바로가기 조회, Redis 캐시 히트 시
홈 바디 평균 응답이 782ms에서 212ms로 줄었습니다(약 72%, k6 로컬 단일 VU 기준). 이후 평시의 약 5배 트래픽이 들어온 2025 추석에도 서버 다운 없이 운영했습니다.
홈 바디 요청섹션 1 조회DB섹션 2 조회DB섹션 3 …DB홈 바디 요청Redis 캐시먼저 확인DataLoader응답 직전 · 따로 동작응답Coroutine 병렬섹션 1섹션 2섹션 3 …DB섹션 조회 · 결과는 캐시에 저장DB상품 id를 모아 한 번에 조회히트미스BEFORE섹션을 하나씩 순서대로 · 섹션마다 DB 직접 조회(N+1) · 캐시 없음AFTER
개념도: 홈 바디 API 안의 흐름. 캐시를 먼저 보고, 없으면 섹션을 병렬로 조회해 캐시에 담습니다. DataLoader는 응답 직전에 따로 돌며 상품 id를 모아 한 번에 추가 조회합니다

Kotlin · Coroutine · GraphQL · DataLoader · Redis · k6

관련 글: 비동기로 성능개선을 한다고? Coroutine, Semaphore, BulkHead로 안정적인 성능 만들기

관련 글: 코틀린 코루틴의 정석 (1): 실무 중심으로 다시 공부하는 코루틴

SirLoin · 설로인 커머스 개발·운영워커 메시지 소비 중단 장애 대응
S · 상황·제약
큐 선언이 세 번 실패하자 RabbitMQ 리스너 컨테이너가 영구 정지했고, 그 큐의 소비가 재배포 때까지 약 90시간(3일 반) 멈췄습니다. 재배포하자 밀려 있던 18건이 한꺼번에 소비됐습니다. 다른 큐는 정상이었고 아웃박스도 발행 성공(SENT)으로 남아 어떤 경보에도 잡히지 않았습니다. 같은 시기 파드를 교체할 때마다 진행 중인 작업이 취소를 삼키며 에러 115건이 쏟아지기도 했습니다.
T · 과제
사람이 발견하기 전에 리스너가 스스로 다시 붙어야 하고, 멈췄다면 그 사실을 바로 알 수 있어야 한다고 봤습니다.
A · 판단·행동
  • 원인 확인: 큐 선언이 정해진 횟수만큼 실패하면 리스너 컨테이너가 스스로 멈추고, 그 정지가 에러로 잡히지 않아 알람이 울리지 않았음
  • 재시도 간격은 고정 5초와 지수 백오프를 비교함. 고정 간격은 브로커 장애가 길어질수록 실패 로그와 요청을 계속 쌓기 때문에, 5초에서 최대 5분까지 늘어나는 지수 백오프로 큐를 다시 선언하고 리스너는 멈추지 않게 함
  • 재선언에 실패하면 Slack으로 경보해, 다른 큐가 정상이어도 멈춘 큐를 바로 알 수 있게 함
  • 파드 교체 시에는 코루틴 취소를 전파하고 최대 15초 동안 진행 중인 이벤트를 마무리(드레인)한 뒤 종료
R · 결과
  • 큐 선언 실패 시: 약 90시간 소비 정지, 재배포로 복구 → 5초~최대 5분 간격 자동 재선언 (운영 로그 · 설정값)
  • 최근 30일 큐 선언 실패 279회: 3번 실패하면 영구 정지 → 영구 정지 0회 (운영 로그 2026.09~10)
  • 파드 교체 시: 에러 115건 → 취소 전파 + 15초 드레인 (운영 로그)
  • 약 90시간 정지 → 0회 최근 30일 선언 실패 279회에도 영구 정지 0회
최근 30일 동안 큐 선언 실패가 279번 있었지만 컨테이너 영구 정지는 0회였고, 자동 재선언으로 소비가 이어졌습니다. 재선언이 실패하면 Slack으로 바로 알리고, 파드 교체 때 쏟아지던 에러도 취소 전파와 드레인으로 막았습니다.
큐 선언 실패리스너 영구 정지선언 실패 3번 · 경보 없음약 90시간 소비 정지재배포로 복구큐 선언 실패재선언지수 백오프 · 최대 5분자동 복구Slack 경보재선언 실패 시파드 교체취소 전파최대 15초 드레인진행 중 이벤트 마무리BEFOREAFTER파드 교체 시
큐 선언이 실패해도 리스너가 멈추지 않고 최대 5분 간격으로 다시 선언하며, 실패는 Slack으로 알립니다. 파드 교체 때는 취소를 전파하고 최대 15초 동안 마무리합니다

Kotlin · Spring AMQP · RabbitMQ · Coroutine · Kubernetes

SirLoin · 설로인 커머스 개발·운영알림 발송 파이프라인: 중복 발송을 막고, 결과를 모르는 건은 숨기지 않게

설계·구현 전체 담당

S · 상황·제약
운영 워커는 배송·쿠폰·마일리지 같은 거래성 알림톡을 한 달에 약 2만 건 보냅니다. 이 알림을 상주 워커가 직접 보냈기 때문에 재시도, 배포, 노드 교체 때 알림이 사라지거나 같은 알림이 두 번 나갈 수 있었습니다. 알림은 한 번 나가면 되돌릴 수 없고, 벤더가 타임아웃이나 5xx를 주면 실제로 발송됐는지 알 수 없습니다. 벤더 호출 한도가 있어 소비 속도도 마음대로 올릴 수 없었습니다.
T · 과제
같은 알림이 여러 번 와도 고객에게는 한 번만 가게 하는 것(중복 발송 0)이 목표였습니다. 브로커까지는 재전달로 넘기되, 결과를 모르는 발송은 다시 보내지 않고 확인 가능한 상태로 남기기로 했습니다.
A · 판단·행동
  • 발송을 Outbox(적재), Relay(전달), Broker(보관·재시도), Inbox(소비), 벤더 경계(외부 호출) 다섯 단계로 나누고, 단계마다 책임을 하나씩만 두어 어디서 멈췄는지 상태로 남게 함
  • Outbox: 사건(예: 기념일 쿠폰 지급)을 기록하는 쪽이 같은 흐름에서 발송 대기열에 넣고, dedup_key UNIQUE로 같은 사건은 한 번만 적재. Relay는 대기 행에 AMQP message_id를 붙여 RabbitMQ로 넘기고, 넘기지 못한 행은 다음 실행이 다시 넘김
  • Broker: durable quorum 큐와 dead-letter 전략 at-least-once로 브로커 구간에서 메시지가 사라지지 않게 함. 다시 시도할 가치가 있는 실패는 10분 TTL 큐로 재시도하고, 배달 횟수 상한을 넘거나 message_id가 없는 메시지는 바로 파킹 큐로 보냄. 벤더 한도 때문에 단일 활성 소비자로 속도를 고정
  • Inbox: 워커가 벤더 호출 직전에 message_id로 발송 내역 행을 SENDING으로 선점. 재배달이나 재발행으로 같은 메시지가 와도 한 번만 발송
  • 벤더 경계: 결과를 모르는 발송(타임아웃, 5xx)을 다시 보낼지, 오류로 남길지 비교함. 같은 알림이 두 번 나가는 것보다 확인 가능한 누락이 낫다고 판단해 재발송하지 않고, 정리 스케줄러가 SENDING을 ERROR로 바꾸고 Slack으로 알림
R · 결과
  • 재배달·재발행 시: 같은 알림 중복 발송 가능 → message_id 선점으로 한 번만 (구조 변경)
  • 벤더 무응답 시: 결과를 알 수 없음 → ERROR로 남기고 Slack 알림 (구조 변경)
  • 배포·노드 교체 시: 알림 유실 가능 → quorum 큐에 남아 재전달 (구조 변경, 운영 반영)
  • at-least-once 배치에서 워커까지 전달 (재전달, quorum 큐, DLX)
  • message_id 선점 같은 메시지가 여러 번 와도 한 번만 발송
  • 재발송 대신 ERROR 결과를 모르는 외부 발송은 확인 가능한 상태로 남김
브로커 구간은 at-least-once로 넘기고 워커에서 message_id로 한 번만 보냅니다. 벤더 응답이 없는 건은 다시 보내지 않고 ERROR로 남겨, 누락은 숨지 않고 사람이 확인할 수 있습니다.
at-least-once 전달멱등 소비 (한 번만 발송)Outboxdedup_keyRelaymessage_idRabbitMQquorum · DLXWorker Inboxmessage_id 선점재시도TTL 10분파킹DLX카카오LMS대체못 넘긴 행 재전달
왼쪽은 재전달로 넘기는 구간(at-least-once), 오른쪽은 같은 메시지가 여러 번 와도 한 번만 보내는 구간(멱등 소비)입니다

Kotlin · PostgreSQL · RabbitMQ · Quorum Queue · DLX · Outbox · Inbox

관련 글: Outbox만으로 충분할까? CDC와 DLQ를 통한 기초적인 이벤트 기반 설계를 알아보자

SirLoin · 설로인 커머스 개발·운영commerce-batch: 스케줄 잡을 워커에서 분리

2인 공동 프로젝트. 본인: 서비스 구조·잡 실행 구조(CronJob 차트·공통 러너)·실패 지점 재개·알림 발송 대기열 중계

S · 상황·제약
결제 후속 처리, 고객 알림, 스케줄 잡이 모두 상주 워커 한 대 안에서 돌았습니다. 잡끼리 실행 시각이 겹치면 힙이 넘쳐 OOM이 났고, 같은 워커의 알림·주문 동기화까지 함께 실패했습니다. 잡은 한 인스턴스에서만 실행되므로 파드를 늘려도 잡이 몰리는 시각의 부담은 줄지 않고 비용만 늘었고, 메모리를 키우면 잠깐의 피크를 위해 하루 종일 그 비용을 내야 했습니다. 잡이 늘수록 시각을 피해 배치하는 방식으로도 감당할 수 없었고, 끊긴 잡은 어디까지 처리했는지 알 수 없었습니다.
T · 과제
상주 워커에는 결제 후속 처리와 알림만 남기고, 스케줄 잡은 돌 때만 자원을 쓰게 하는 것이 목표였습니다. 같은 회차는 한 번만 완료되고, 끊기면 실패한 지점부터 이어 가야 했습니다.
A · 판단·행동
  • 실행 단위를 잡 하나당 파드 하나로 나눔. 잡마다 파드를 띄우고 종료 코드로 끝나는 Kubernetes CronJob과 Spring Batch 독립 서비스로 설계 (Kotlin, JDK 21, Spring Boot 4). 잡 파드는 그 잡에 필요한 만큼만 쓰고 끝나면 내려감
  • 같은 CronJob이 겹쳐 뜨지 않게 하고(concurrencyPolicy: Forbid), Spring Batch JobInstance(잡 이름 + 식별 파라미터)로 같은 회차가 두 번 완료되지 않게 함
  • 실패했을 때 처음부터 다시 돌리는 대신 이어 가도록, keyset reader와 청크 커밋(잡별 50~500건)으로 마지막으로 커밋한 청크 다음부터 재개. Testcontainers(Postgres)로 끊긴 청크부터 재개되는지 검증
  • 알림 잡은 보낼 알림을 dedup_key가 있는 발송 대기열에 넣고, 중계 잡이 워커로 넘기게 함 (알림 발송 파이프라인)
  • 모든 잡이 공통 러너를 지나게 해 실패한 실행마다 Slack 경보 1건을 보내도록 함. ArchUnit 규칙 12개와 커버리지 70% 게이트로 계층 구조를 빌드에서 확인
R · 결과
  • 실행 위치: 결제 후속 처리·알림과 한 워커에 상주 → 잡마다 전용 파드, 끝나면 종료 (배포 설정)
  • 실패 후 재처리: 어디까지 처리했는지 모름 → 마지막 커밋 청크 다음부터 재개 (Testcontainers 검증)
  • 22개 운영에서 잡마다 전용 파드로 도는 CronJob (공동 작업 전체)
  • 70% 도메인·유스케이스 커버리지 빌드 게이트
운영에서 22개 잡이 잡마다 전용 파드로 돌고 끝나면 내려갑니다. 같은 회차는 한 번만 완료되고, 실패하면 마지막으로 커밋한 청크 다음부터 이어 갑니다. 옮긴 잡은 메모리를 다 써도 결제 후속 처리와 알림을 맡는 워커에 영향을 주지 않습니다.
상주 워커 파드APIQueue consumer@Scheduled 잡들힙 공유CronJobForbidPod (잡 1개)Spring Batch종료 코드exitdedup outboxrelayRabbitMQ워커알림 발송BEFOREAFTER
상주 워커 안의 @Scheduled 잡을, 잡마다 떴다가 종료 코드로 끝나는 CronJob으로. 알림은 outbox 경로로 통일

Kotlin · Spring Batch · Kubernetes CronJob · Testcontainers · ArchUnit

Orangei · 평가 데이터 관리·모니터링 SaaS (BTS)대용량 데이터 조회 성능 개선
S · 상황·제약
기업별 평가 데이터가 하루 120만 건 이상 쌓이면서 조회가 눈에 띄게 느려졌습니다.
T · 과제
여러 고객사가 함께 쓰는 만큼 기업별 데이터를 분리하면서 조회 속도도 올려야 했습니다.
A · 판단·행동
  • 멀티테넌시로 기업별 데이터를 분리
  • 복합 인덱스로 쿼리 실행 계획 개선
  • 분산 인스턴스 환경에 Redis 캐시 적용
R · 결과
  • 90% 대용량 조회 응답 시간 단축
조회 응답 시간이 90% 줄었습니다.

Java · Spring Boot · JPA · Querydsl · Redis

Orangei · 평가 데이터 관리·모니터링 SaaS (BTS)배치 서버를 서버리스로 전환
S · 상황·제약
배치는 잠깐 도는데 Spring Batch 서버는 상시 떠 있어 유휴 비용과 운영 부담이 컸습니다.
T · 과제
배치가 도는 동안만 자원을 쓰는 구조로 바꾸기로 했습니다.
A · 판단·행동
  • 배치를 AWS Lambda, EventBridge, SQS 기반으로 전환
R · 결과
  • 약 60% 배치 운영 비용 절감
배치 운영 비용이 약 60% 줄었습니다.

AWS Lambda · EventBridge · SQS

Orangei · 평가 데이터 관리·모니터링 SaaS (BTS)Jenkins CI/CD 도입
S · 상황·제약
수동 배포에 30분이 걸려 릴리스가 밀리고 실수할 여지가 있었습니다.
T · 과제
배포를 자동화하고 정적 분석을 파이프라인에 넣었습니다.
A · 판단·행동
  • Jenkins CI/CD 파이프라인 도입
  • SonarQube와 Quality Gate로 정적 분석
R · 결과
  • 30분 → 2분 배포 시간
배포 시간이 30분에서 2분으로 줄었습니다.

Jenkins · SonarQube

Orangei · 평가 데이터 압축 모듈 (Java)Gorilla 알고리즘 기반 시계열 압축
S · 상황·제약
충방전 IoT 장비의 시계열 평가 데이터가 시간 단위로 한꺼번에 적재되며 빠르게 쌓였는데, 모든 칼럼에 같은 ZIP(Deflate)을 쓰고 있어 데이터 특성을 살리지 못했습니다. 입사 후 처음으로 혼자 설계부터 맡은 모듈이었고, 사내 개발자들이 가져다 쓰는 라이브러리여야 했습니다.
T · 과제
칼럼 성격에 맞춘 무손실 압축 모듈을 만들어, ZIP보다 저장 공간과 압축·해제 시간을 줄이는 것이 목표였습니다.
A · 판단·행동
  • Facebook Gorilla 논문을 분석해 칼럼 유형별로 알고리즘을 나눔: 부동소수점은 Gorilla-Value, 시간은 Gorilla-TimeStamp, 반복 정수는 RLE, 순차 증가 정수는 RLE Delta
  • 실제 평가 데이터 100만 건을 CSV로 뽑아 알고리즘별로 비교함. 부동소수점은 ZIP의 압축률이 더 높았지만 속도 차이가 커서 Gorilla-Value를 택함
  • 비교 기준이 되는 ZIP 구현은 ZIP4j, GZIP, java.util.zip이 모두 Deflate라 차이가 작아, 가장 빠르고 부가 기능이 없는 java.util.zip을 씀
  • DB 계층이 아니라 애플리케이션 계층에서 압축. 조회에 쓰는 칼럼은 압축하지 않고 남겨 인덱스로 찾은 뒤 압축·복원하므로, 애플리케이션 메모리 부담은 늘지만 스토리지를 크게 줄이는 쪽을 택함
  • 끝 비트가 0으로 채워져 마지막 값이 반복 복원되던 문제는, 이 데이터가 스트림과 달리 끝이 정해져 있다는 점을 이용해 압축한 row 수를 함께 저장해 해결
  • Javadoc을 쓰고 사내 Private Maven 저장소로 배포해 다른 개발자가 바로 가져다 쓰게 함
R · 결과
  • 저장 공간: ZIP(Deflate) 일괄 적용 → 70% 절감 (사내 벤치마크, ZIP 대비)
  • 압축·해제 속도: ZIP → 45.62% 향상 (사내 벤치마크, ZIP 대비)
  • 70% ZIP 대비 저장 공간 절감
  • 45.62% ZIP 대비 압축·해제 속도 향상
ZIP 대비 저장 공간을 70% 줄이고 압축·해제 속도를 45.62% 높여, 사내 Java 솔루션에 적용했습니다.
부동소수점 칼럼Gorilla-ValueDate형 칼럼Gorilla-TimeStamp증가하는 Number형 칼럼RLE Delta
칼럼 성격마다 맞는 무손실 압축 알고리즘을 고른다

Java 8 · Gradle · Gorilla · RLE Delta · PostgreSQL

관련 글: 데이터 압축을 위한 Gorilla 알고리즘 적용 사례: 사내 솔루션 개발기 및 회고

다운로드

다운로드 코드를 입력해 주세요.