date
slug
author
status
tags(최대 3개)
summary
type
thumbnail
category
updatedAt
아임웹은 매년 큰 폭으로 성장하고 있고, 그만큼 주문 트래픽도 늘어나 기존 방식만으로는 감당하기 어렵다는 문제의식에서 이번 TF를 시작했습니다. 특히 공동구매처럼 특정 시각에 상품이 열리는 이벤트는 일반적인 주문 흐름과 달리, 아주 짧은 시간에 트래픽이 폭발적으로 몰립니다. 평소엔 드러나지 않던 병목이 이 순간 한꺼번에 터지는데, 그 대표가 재고 처리 구간의 lock 경합이었습니다.
실제로 비슷한 이벤트를 여러 번 겪으며 lock wait가 길어졌고 slow query가 반복됐습니다. 쿼리가 끝나지 않으면 connection이 묶이고, 그 여파로 다른 사용자의 연결까지 불안정해졌습니다. 당시엔 DBA가 실시간으로 상황을 지켜보다 오래 실행되는 쿼리를 직접 kill해야 할 때도 있었습니다. 고객 경험에도, 내부 운영 리소스에도 좋지 않은 대응이었습니다.
그러던 중 한 고객사의 공동구매 일정이 잡혔습니다. 이전 판매 패턴을 보면 트래픽 대부분이 오픈 직후 5~10분에 몰렸고 그사이 수십억 원의 매출이 발생했습니다. 이번엔 단순히 버티는 수준이 아니라, 주문과 재고 처리 흐름에 숨은 병목을 구조적으로 개선해야 한다는 게 분명해졌습니다.
TF 시작 전 드러난 문제
장애 당시를 보면 옵션 재고 차감 과정에서 slow query와 500 에러가 집중적으로 발생했습니다.
원인을 처음 분석할 때는 트랜잭션 안의 쿼리가 너무 많은지, 주문 비즈니스 로직이 지나치게 긴지,
LIKE 조건 탓에 느린지 등 여러 가능성을 의심했습니다. 하지만 Datadog trace를 확인했을 때 특정 구간의 latency가 유독 길게 튀는 모습은 뚜렷하지 않았습니다.정작 장애 상황에서는 주문 API의 P95 latency가 급격히 치솟았고, 재고 차감 쿼리 중에서도 조합형 옵션의 재고 차감
UPDATE 쿼리가 slow query(lock wait timeout)에 걸리면서 수백 개의 DB 프로세스가 밀리고 있었습니다. 특정 옵션 재고를 동시에 갱신하려는 요청이 한 지점에 몰리면서, 대기 중인 세션이 계속 쌓인 것입니다.
운영 대응도 거칠 수밖에 없었습니다. DBA가 slow query를 실시간으로 모니터링하다, 기준 이상으로 오래 잡고 있는 쿼리를 강제로 kill하는 작업을 수시로 해야 했습니다.
이건 "응답이 조금 느리다" 수준이 아니라, 운영자가 DB 레벨에서 직접 개입해야 서비스가 버티는 상태였습니다.
느린 건 쿼리 실행이 아니라 lock wait였습니다
원래 주문 프로세스는 트랜잭션 안에서 재고 row를 직접 갱신하며 row lock을 잡는 구조였습니다. 평소엔 정합성을 지키는 데 효과적이지만, 순간 TPS가 매우 높아지면 같은 재고 row를 갱신하려는 요청이 한 지점으로 몰립니다.
문제는 여기서 시작됩니다. 먼저 들어온 요청이 lock을 쥐고 있는 동안 뒤따른 요청들은 쿼리를 실행하지도 못한 채 대기합니다. 늦게 들어올수록 앞선 요청이 끝나길 기다리는 시간이 계속 누적됩니다.

slow query로 보이던 현상은 "DB가 쿼리를 느리게 실행했다"기보다 row lock 대기 시간이 길어져 전체 쿼리 시간이 늘어난 것에 가까웠습니다. 실제 SQL 실행 시간은 짧아도, lock을 기다린 시간이 길어지면 그 대기까지 전체 쿼리 시간으로 로그에 잡힙니다.

같은 조합형 옵션 재고를 갱신하는 UPDATE가 동시에 들어오면, 먼저 도착한 요청만 lock을 잡고 실행되고 나머지는 대기한다
그래서 트래픽이 몰리는 순간 뒤쪽 요청일수록 응답 시간이 가파르게 늘었고, 일부는 slow query·timeout·500 에러로 이어졌습니다. 이 문제는 인덱스를 추가하는 수준이 아니라, 주문 과정에서 재고를 차감하는 경로 자체를 더 짧고 덜 막히는 구조로 바꿔야 풀 수 있었습니다.
게다가 재고를 다루는 주체가 주문 도메인, 상품 도메인, 어드민, OPEN-API, 배포 인프라까지 여러 곳에 흩어져 있었고, 트래픽이 몰릴수록 이 복잡성이 그대로 병목이 됐습니다. 이번 프로젝트의 목표는 "쿼리 하나를 튜닝하는 것"이 아니라 주문 과정 전체에서 재고를 다루는 방식을 다시 설계하는 것에 가까웠습니다.
이런 배경에서 TF가 시작됐고, 과제도 명확했습니다.
- slow query에 걸리지 않는 안정적인 재고 차감과 복구
- lock을 오래 잡지 않는 구조
- 커머스 전용 Redis 분리
- 주문 도메인에서 쿼리 실행을 방해하는 요소 점검
개선의 핵심 구조
이번 TF에서는 재고 차감 SQL만 보지 않고, 주문 도메인 내부에서 무엇이 재고 차감 트랜잭션을 방해하는지, 옵션 조회 구조는 어떤지, Redis와 DB의 역할을 어떻게 나눌지를 함께 검토했습니다.
가장 중요한 결정은 재고 처리를 한 곳으로 모으는 것이었습니다. 여러 시스템이 각자 재고를 만지면 트래픽이 몰릴 때 병목도 여러 군데에서 터지고 복구도 어려워집니다. 그래서 재고 처리의 중심을 상품 도메인 한 곳으로 모으고, 여러 서비스가 이 경로를 호출하도록 정리했습니다. 주문에 따른 재고 차감뿐 아니라 결제 기한 만료 등에 따른 재고 복구 요청도 상품 도메인을 통해 한 경로로 모았습니다.

구조의 핵심 포인트는 세 가지였고, 각각을 아래에서 자세히 살펴봅니다.
1. 재고 변경 경로를 한 곳으로 모았습니다
주문서버, OPEN-API, 어드민, 배치서버 등 재고를 건드리는 서버가 모두 상품 도메인을 호출하게 하고, 상품 도메인 한 곳에서 재고를 관리해 재고 변경 지점을 단일화했습니다.

성능 튜닝보다 운영 구조를 단순하게 만드는 일이 먼저였습니다. 재고를 바꾸는 경로가 한 곳에 모여야 이후 병목도 한 군데에서 줄일 수 있고, 장애가 났을 때 복구 지점도 명확해지기 때문입니다.
2. hot path를 Redis 중심으로 짧게 만들었습니다
상품 도메인은 먼저 cache를 확인하고, miss일 때만 DB를 조회해 cache를 채운 뒤 재고를 처리합니다. 이때 분산 락으로 동시성을 제어해 같은 재고에 대한 경쟁 상태를 줄였습니다.

핵심은 모든 요청이 DB row lock 구간까지 내려가지 않게 만드는 것이었습니다. 그래야 높은 TPS가 들어와도 lock wait가 누적되며 뒤 요청이 한꺼번에 밀리는 상황을 줄일 수 있습니다.
3. DB 반영을 비동기로 처리했습니다
재고 변경 이벤트는 Kafka 메시지로 발행하고, 별도 consumer가 DB 반영과 Redis TTL 갱신을 맡아 DB 변경이 단일 지점에서만 일어나게 했습니다.

여기엔 안전장치가 반드시 필요합니다. 메시지 발행이 실패하면 바로 포기하지 않고 retry하며, 정해진 횟수까지 실패하면 fallback으로 DB를 직접 변경하도록 설계했습니다. 빠른 경로는 Redis와 Kafka로 처리하되, 장애 상황에서도 재고 반영이 유실되지 않도록 안전망을 함께 둔 것입니다.
배포 과정
1. QA와 함께 부하 테스트를 반복했습니다
개선된 코드를 완성한 뒤 내부 QA팀과 K6로 부하 테스트를 반복했습니다. 약 2주간 기준 환경과 개선 구조를 계속 비교하며, 트래픽을 점진적으로 늘리거나 조건을 바꿔 가며 무엇이 실제로 효과 있는지 확인했습니다. 이 과정에서 재고 로직뿐 아니라 주변 병목도 함께 정리됐습니다.

- 재고 증가/차감/변경 API의 Kafka 비동기 처리
- 주문 도메인과 상품 도메인 간 안정적인 API 호출 구조
- 내부 서버 인증 토큰의 성능 최적화
2. 작은 범위로 운영 배포를 시작했습니다
부하 테스트 후에는 특정 사이트를 대상으로 프로덕션에서 선배포와 부하 테스트를 함께 진행했습니다. 한 번에 전체를 바꾸지 않고 실제 운영에 가까운 환경에서 작은 범위부터 검증한 것입니다. 이후 트래픽을 점진적으로 높이며 테스트를 반복해, 아임웹 평소 최고 주문 트래픽의 약 20배 수준까지 검증을 마쳤습니다. 아임웹 대부분의 기능은 점진적 배포로 안정성을 확보하는데, 이번 개선 제품도 대상 사이트를 점진적으로 늘려 가며 안정화했습니다.
3. 큰 이벤트로 실제 검증을 마쳤습니다
실제 공동구매 이벤트에 개선 버전을 적용했습니다. 12:00 정각 오픈, 약 2만 개 재고 규모의 이벤트로, 주문·재고 처리 구조가 실전에서 버티는지 확인하는 가장 강한 검증 구간이었습니다.
결과: 서버 다운 한 번 없이 안정적으로 운영됐고, 기대했던 매출도 성공적으로 만들어 냈습니다. 이제는 DBA가 실시간으로 slow query를 모니터링하며 엔드유저의 주문 요청을 강제로 종료하지 않아도 됩니다.
마무리
엔드유저의 쇼핑 경험에서 주문·결제 여정은 가장 중요한 구간이자, 이탈이 많이 발생하는 구간이기도 합니다. 이번 TF로 기존 주문 피크 트래픽의 약 20배 이상까지 감당하도록 성능을 개선했고, 이 구조는 전 고객사에 배포된 뒤 별도 이슈 없이 안정적으로 마무리됐습니다.
덕분에 대용량 주문이 몰려도 DBA가 실시간으로 slow query를 모니터링하며 개입할 필요가 없어졌습니다. fallback까지 갖추면서 예외 상황이 생겨도 주문 흐름이 쉽게 무너지지 않는, 더 안정적인 주문 처리 경로를 완성했습니다.
재고 차감은 DB만 빠르게 만든다고 해결되는 문제가 아니었습니다. 서비스 간 호출 구조, 토큰 처리 방식, MSA 간 호출 구조 개선, 캐싱, QA 시나리오, 운영 배포 전략까지 함께 맞물려야 실제 개선으로 이어졌습니다. 이번 TF는 재고 차감 쿼리 하나를 고친 작업이 아니라, 주문과 재고 처리의 전체 흐름을 다시 설계하고 운영 가능한 상태로 끌어올린 작업이었다는 점에서 더 의미가 있었습니다.

