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

카트 개발 연대기

컬리 카트 도메인이 1년간 4세대에 걸쳐 진화한 개발 연대기다. 라라벨 기반 카트가 주소지 기반 재고 노출이라는 비즈니스 요구와 데드락·응답속도라는 기술부채를 풀기 위해 Legacy에서 Stack, Modern, Multi 카트로 재설계된 과정을 담았다.

핵심 포인트
  • 2020년 김포센터 오픈에 맞춰 주소지 기반으로 물류센터 재고·전시·할인을 조회하는 구조로 카트를 재설계
  • 1세대 Legacy Cart: 3,000줄 컨트롤러를 Interface·Service로 분해하고 Manager Pattern·Cart Response 객체 도입
  • 2세대 Stack Cart: 트리에서 선형 구조 마이그레이션을 위한 dual write를 시도했으나 데드락으로 실패, 롤백도 불가
  • 카트 데드락의 원인은 회원의 n세션 1카트 구조와 최신 1개만 남기는 cart cleansing 경합
  • 4세대 Multi Cart: 카트끼리 아이템을 공유하는 구조와 cleansing 제거로 경합을 없애 데드락 근본 해결에 근접
상세 정리
  • 목표: 비즈니스 측면은 주소지에 포함되는 물류센터 코드 기반으로 재고·전시·할인 API를 조회하는 것, 개발 측면은 레거시 탈피·테이블 개선·DDD 도메인 분리·데드락 해결·응답속도 개선이었다.
  • 1세대 Legacy Cart: 라라벨에 맞춰 약 3,000줄이 몰린 컨트롤러를 Interface와 Service 단위로 분해하고, foreach로 매번 조회하던 걸 relation 구조 Eager Loading으로 바꿨으며, Manager Pattern과 Cart Response 객체로 상품구조를 관리했다.
  • 2세대 Stack Cart: 트리 구조 Legacy와 선형 구조 Modern은 형태가 달라 table migration이 어려워, 같은 데이터를 순간마다 동시에 적재하는 dual write로 자연스러운 전환을 노렸다.
  • Stack 실패: 데드락이 많던 기존 로직과 신규 로직이 dual write하며 크게 앓았고, 주문 골드타임(밤 10~11시)마다 API 성능 지표가 무너졌다.
  • 롤백 불가: 앱이 이미 배포됐고 카트가 네이티브를 쓰는데 Endpoint 규격이 바뀌어, 1세대로 온전히 롤백하려면 강제 업데이트가 필요해 리스크가 컸다.
  • 3세대 Modern Cart: Stack 배포 시 기반이 어느 정도 완성돼 약간의 리팩토링으로 배포했고, 개편 전이던 주문서가 Modern 구조를 못 받아 주문서 진입 전 상품 리스트를 구 카트로 부분 마이그레이션하는 방식으로 연결했다.
  • 남은 데드락: Modern으로도 특정 트래픽에서 카트가 휘청거려 데드락을 궁극적으로 해결할 때가 왔다고 판단했다.
  • 데드락 원인: 비회원은 1세션 1카트지만 회원은 n세션 1카트라, 여러 플랫폼 동시 로그인 시 최신 1개만 남기는 cart cleansing이 서로 카트를 뺏으며 경합·장애를 유발했다.
  • 4세대 Multi Cart: 카트끼리 서로의 아이템을 뺏지 않고 공유하는 구조로 바꾸고 cart cleansing을 제거해(1세션 유지에서 n세션 유지로) 경합을 없앴다. 대신 카트 id와 아이템 관계를 알아야 해 쿼리는 복잡해졌다.
  • 기술부채 상환: 100원딜 상품 검증 로직과 쿼리를 리팩토링하고, 카트 아이템 개수 표현에 캐시 레이어를 넣었다.
  • Modern vs Multi: 추가·수량 변경·삭제·리스트·체크아웃 동작에서 Modern은 B 카트 아이템을 A로 강제 Update 후 처리하는 반면, Multi는 A·B 카트를 함께 조회하고 group by로 처리하도록 비교·정리했다.
  • 5세대 계획: 카트가 다른 도메인의 장애 지표가 되지 않도록 불필요한 DML을 줄여 Write DB 리소스를 아끼고 대규모 트래픽을 빠르게 처리하는 걸 목표로 설계 중이다.
왜 읽나커머스 카트처럼 세션 경합·데드락·레거시 마이그레이션을 안고 도메인을 여러 세대로 재설계하는 백엔드 개발자에게 dual write·공유 구조 전환의 실전 회고.
마켓컬리
마켓컬리 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