
트래픽이 폭주해도 무너지지 않는 쇼핑몰, DB 설계부터 다릅니다
대용량 트래픽에도 무너지지 않는 쇼핑몰을 위한 DB 설계 전략. 반정규화, CQRS 패턴, Redis를 활용한 재고 관리 등 실전 경험에서 우러나온 노하우를 공유합니다.
송찬영
CTO

"CTO님, DB CPU가 90%를 넘었습니다. 커넥션 풀이 꽉 차서 주문이 안 들어갑니다!"
이 말을 듣는 순간 등골이 서늘해지지 않는 개발 리더는 아마 없을 겁니다. 몇 년 전, 블랙프라이데이 이벤트를 야심 차게 준비했다가 오픈 10분 만에 서버가 먹통이 되었던 그날 밤의 기억은 아직도 생생합니다. 마케팅 팀은 항의 전화에 시달리고, 개발팀은 로그를 뒤지며 식은땀을 흘리던 그 지옥 같던 시간. 결국 원인은 애플리케이션 코드가 아니라, 급격히 늘어난 트래픽을 감당하지 못한 데이터베이스 설계의 구조적 한계였습니다.
많은 분이 쇼핑몰제작을 고민할 때 화려한 프론트엔드 UI나 결제 연동 같은 기능 구현에 집중하곤 합니다. 하지만 시스템의 생명을 결정짓는 것은 결국 보이지 않는 곳, 데이터베이스 아키텍처에 있습니다. 오늘은 그때의 뼈아픈 실패를 통해 배운, 대용량 트래픽을 견디는 쇼핑몰 DB 설계 전략에 관해 이야기해보려 합니다.
정규화의 함정, 그리고 반정규화의 결단
초기 스타트업 시절, 우리는 교과서적인 정규화(Normalization) 원칙을 철저히 지켰습니다. 데이터의 중복을 없애고 무결성을 지키는 것이 개발자의 미덕이라 믿었기 때문이죠. 상품 테이블, 옵션 테이블, 재고 테이블, 카테고리 테이블을 완벽하게 분리했습니다.
하지만 트래픽이 몰리자 이 아름다운 구조가 독이 되었습니다. 상품 상세 페이지 하나를 보여주기 위해 5~6개의 테이블을 조인(Join)해야 했고, 수천 명의 사용자가 동시에 조회를 요청하자 DB는 비명을 질렀습니다.

그때 깨달았습니다. 대규모 읽기 트래픽이 발생하는 이커머스 환경에서는 '조회 성능'이 '데이터 중복 최소화'보다 우선되어야 한다는 것을요. 우리는 과감하게 반정규화(Denormalization)를 단행했습니다. 자주 조회되는 상품 정보와 옵션 정보를 하나의 테이블 혹은 JSON 컬럼으로 묶어버렸고, 쿼리 한 번으로 데이터를 가져오도록 구조를 바꿨습니다. 물론 데이터 정합성을 맞추는 추가 로직이 필요했지만, 조회 속도는 획기적으로 개선되었습니다.
쓰기 지옥에서 탈출하기: CQRS 패턴의 도입
조회 성능을 잡고 나니 이번엔 '주문'이 문제였습니다. 주문이 몰리면 재고를 차감하는 쓰기(Write) 작업 때문에 락(Lock)이 걸리고, 이로 인해 상품 목록 조회(Read)까지 덩달아 느려지는 현상이 발생했습니다.
이 문제를 해결하기 위해 도입한 것이 바로 CQRS(Command Query Responsibility Segregation) 패턴입니다. 쉽게 말해 명령(쓰기)과 조회(읽기)를 분리하는 것입니다.
우리는 Master DB는 오직 주문 생성, 재고 차감 같은 쓰기 작업만 전담하게 하고, 여러 대의 Slave DB(Read Replica)를 두어 상품 조회 트래픽을 분산시켰습니다. 사용자가 상품을 볼 때는 Slave DB에서 데이터를 읽어오고, 결제 버튼을 누를 때만 Master DB를 건드리는 구조죠. 쇼핑몰제작 프로젝트 초기에는 단일 DB로 충분할지 몰라도, 스케일업을 고려한다면 이처럼 읽기와 쓰기의 부하를 물리적으로 분리하는 설계가 필수적입니다.
재고 관리의 핵심, 레디스(Redis) 활용
이커머스 DB 설계의 끝판왕은 결국 '재고 관리'입니다. "선착순 100명 특가" 같은 이벤트에서 RDBMS만으로 재고를 관리하려다가는 데드락(Deadlock)에 걸려 시스템 전체가 멈출 수 있습니다. 디스크 기반의 DB는 수만 건의 동시 업데이트 요청을 감당하기엔 너무 느립니다.

우리는 이 문제를 해결하기 위해 인메모리 데이터베이스인 Redis를 적극 활용했습니다. 이벤트 시작 전 Redis에 재고 수량을 미리 올려두고, 주문 요청이 들어오면 Redis에서 DECR 연산으로 재고를 먼저 차감합니다. Redis는 싱글 스레드 기반이라 원자성(Atomicity)이 보장되면서도 속도가 엄청나게 빠릅니다. 실제 DB 업데이트는 메시지 큐(Kafka 등)를 통해 비동기로 처리함으로써 사용자는 대기 시간 없이 주문 성공 결과를 받아볼 수 있게 되었죠.
성공적인 쇼핑몰 구축을 위한 체크리스트
조회 패턴 분석: 사용자가 가장 많이 조회하는 화면이 어디인가? 그 화면을 위해 몇 번의 조인이 발생하는가? 필요하다면 과감히 테이블을 합치세요.
캐싱 전략 수립: DB까지 가지 않아도 되는 데이터는 무엇인가? 카테고리 목록이나 메인 배너 정보는 굳이 매번 DB를 조회할 필요가 없습니다. 로컬 캐시나 Global Cache를 적극 활용하세요.
동시성 제어 계획: 재고 차감 시 동시성 이슈를 어떻게 해결할 것인가? 낙관적 락(Optimistic Lock)을 쓸지, 비관적 락(Pessimistic Lock)을 쓸지, 아니면 Redis를 쓸지 시나리오별로 결정해야 합니다.
수평적 확장(Sharding) 고려: 데이터가 천만 건을 넘어가면 단일 테이블로는 버티기 힘듭니다. 사용자 ID나 주문 날짜를 기준으로 데이터를 쪼개는 샤딩 전략을 미리 염두에 두어야 합니다.
결국 기술은 비즈니스를 위해 존재합니다
트래픽을 견디는 DB 설계는 단순히 기술적인 자랑거리가 아닙니다. 고객이 사고 싶은 물건을 제때 살 수 있게 해주는 것, 그것이 바로 비즈니스의 본질이기 때문입니다. 화려한 기능을 넣기 위해 고민하는 시간만큼, 혹은 그 이상으로 데이터가 흐르는 길을 닦는 데 공을 들여야 합니다.
혹시 지금 쇼핑몰제작을 준비하고 계신가요? 겉모습보다는 튼튼한 뼈대를 먼저 고민해 보시길 권합니다. 오픈 당일 터져나가는 서버를 보며 망연자실하는 경험은, 저 하나로 족하니까요. 데이터베이스 설계에 대한 깊은 고민이 여러분의 서비스를 한 단계 더 성장시키는 밑거름이 되기를 진심으로 응원합니다.


