쇼피파이는 왜 Redis를 걷어내고 MySQL로 돌아갔을까
트래픽이 몰린다는 얘기가 나오면 엔지니어들이 가장 먼저 꺼내는 카드가 있습니다. 캐시입니다. Redis 한 대 앞에 세우고 DB 부하를 덜자는 건 거의 반사 신경에 가깝습니다. 그런데 블랙 프라이데이에 초당 수백만 요청을 받아내는 쇼피파이가 재고 예약 경로에서 정반대로 움직였습니다. Redis를 걷어내고 MySQL만 남겼습니다.
미리 하나 밝혀둡니다. 이번 주제는 커뮤니티에서 지금 한창 논의되는 최신 이슈라기보다, 시스템 설계 판에서 꾸준히 회자되는 사례에 가깝습니다. 최근 30일 기준으로 새로 올라온 대규모 토론은 거의 없었습니다. 그래서 이 글은 실시간 반응 중계보다 이 결정에 깔린 설계 관점을 뜯어보는 쪽에 무게를 둡니다.
재고 예약은 캐시가 손대기 가장 껄끄러운 데이터입니다
재고는 좀 특별합니다. 상품 설명이나 이미지 URL은 1초 늦게 반영돼도 아무도 안 죽습니다. 그런데 재고 수량은 다릅니다. 남은 1개를 두 명에게 팔면 그 순간 고객 둘 중 하나는 환불 메일을 받습니다.
플래시 세일을 떠올려보면 더 선명합니다. 한정판 스니커즈 100켤레에 3만 명이 동시에 몰립니다. 이때 필요한 건 “대충 빠른 읽기"가 아니라 “정확히 100번만 성공하는 쓰기"입니다. 캐시가 잘하는 쪽은 앞이고, 지금 필요한 건 뒤입니다.
재고 예약은 읽기 중심 워크로드가 아닙니다. 읽고 판단하고 쓰는 작업이 하나의 원자적 단위로 묶여야 하는 트랜잭션입니다. 캐시가 원래 못하는 영역에 캐시를 밀어 넣은 셈입니다.
Redis를 쓰면 정합성 문제를 직접 떠안게 됩니다
Redis에 재고 카운터를 두는 순간 골치 아픈 질문이 줄줄이 따라붙습니다.
Redis 값과 MySQL 값이 어긋나면 뭘 믿어야 할까요. Redis 인스턴스가 죽으면 진행 중이던 예약은 어떻게 되나요. 결제가 실패해서 재고를 되돌려야 하는데 하필 그 사이 Redis 장애가 나면 그 수량은 어디로 갈까요.
답하려면 결국 코드로 메꿔야 합니다. 두 저장소를 맞추는 동기화 로직, 어긋난 값을 찾아내는 정합성 검사 배치, 장애 시 복구 절차, 이걸 전부 검증하는 테스트까지. 문제는 이게 분산 트랜잭션이라는 점입니다. 컴퓨터 과학에서 가장 어려운 축에 드는 문제고요.
MySQL은 이걸 이미 풀어놨습니다. SELECT ... FOR UPDATE로 행을 잠그고, 트랜잭션 안에서 재고를 차감하고, 커밋하면 끝입니다. 롤백도 공짜, 크래시 복구도 공짜입니다. 수십 년간 다듬어진 기능입니다. 그 위에 캐시를 얹는 순간 그 검증된 보장이 애플리케이션 코드가 직접 책임져야 할 숙제로 바뀝니다.
MySQL은 우리 생각보다 훨씬 빠릅니다
여기서 흔한 오해가 하나 걸립니다. “그래도 DB보다 Redis가 훨씬 빠르잖아요"라는 반응인데요.
맞습니다. 다만 전제가 붙습니다. 인덱스가 제대로 걸린 기본키 조회에서 MySQL은 대체로 1밀리초 안팎에 응답합니다. Redis가 0.2밀리초라고 해도 차이는 1밀리초 미만입니다. 그런데 웹 요청 하나가 브라우저에서 서버까지 왕복하는 데는 보통 수십에서 수백 밀리초가 듭니다. 전체 그림에서 이 차이는 잘 보이지도 않습니다.
핫한 데이터라면 어차피 MySQL의 버퍼 풀에 올라가 있습니다. 디스크를 때리는 게 아니라 메모리에서 읽는다는 뜻이죠. 애초에 “메모리 저장소 대 디스크 저장소"라는 구도부터가 실제와 어긋난 프레임이었습니다.
물론 MySQL이 항상 이긴다는 얘기는 아닙니다. 복잡한 조인이 얽힌 조회, 세션이나 레이트 리밋 같은 휘발성 데이터, 읽기가 대부분인 경로에서는 캐시가 확실히 제값을 합니다. 요점은 재고 예약이 그런 경우가 아니었다는 것입니다.
진짜 병목은 대개 캐시로 해결되지 않습니다
캐시를 붙이면 편해지는 것도 사실입니다. 코드를 안 고쳐도 되니까요. 느린 쿼리가 있으면 결과를 캐시에 담아두면 됩니다. 근본 원인은 그대로 남지만 지표는 좋아집니다.
문제는 그다음입니다. 캐시가 그 쿼리를 가려주는 동안 아무도 그 쿼리를 고치지 않습니다. 시간이 지나 데이터가 불어나면 이제 캐시 미스 한 번이 치명타가 됩니다. 트래픽이 몰리는 순간 캐시가 비어 있으면 그동안 숨어 있던 느린 쿼리가 한꺼번에 DB로 쏟아집니다. 그게 그 유명한 썬더링 허드입니다. 캐시가 있어서 장애가 더 크게 터지는 구조죠.
쇼피파이 같은 규모의 시스템이 캐시 없이 버티는 이유는 마법을 부려서가 아닙니다. 인덱스를 제대로 설계하고, 데이터를 샤딩으로 쪼개고, 커넥션 풀을 조율하고, 쿼리 자체를 다듬었기 때문입니다. 지루한 작업입니다. 대신 새벽 3시에 캐시 무효화 버그를 디버깅하는 것보다 훨씬 예측 가능합니다.
복잡도를 줄이는 것이 곧 확장성이라는 역설
이 사례에서 건질 건 “Redis는 나쁘다"가 아닙니다. 그런 결론이라면 오히려 원래 문제를 반복하는 셈입니다. 도구를 먼저 정하고 문제를 끼워 맞추는 것 말입니다.
교훈은 순서에 있습니다. 시스템에 부품을 하나 추가하면 상태 저장소가 하나 늘고, 장애 지점이 하나 늘고, 운영해야 할 대상이 하나 늘고, 신입 엔지니어가 배워야 할 개념이 하나 늡니다. 이 비용은 처음엔 잘 안 보입니다. 2년쯤 지나서 한꺼번에 청구됩니다.
그래서 순서를 이렇게 뒤집어볼 만합니다. 먼저 측정합니다. 실제로 느린 게 뭔지 확인합니다. 인덱스, 쿼리, 스키마부터 손봅니다. 그래도 안 되면 그때 캐시를 검토합니다. 이 순서를 지키면 캐시를 붙일 때도 왜 필요한지 설명할 수 있게 됩니다.
마무리
쇼피파이의 선택은 질문 하나로 좁혀집니다. 이 캐시는 실제로 측정된 문제를 풀고 있는가, 아니면 “다들 그렇게 하니까” 붙어 있는가. 재고 예약처럼 정확성이 속도보다 중요한 경로에서는 부품 하나를 빼는 쪽이 더 나은 확장 전략입니다.
지금 여러분 시스템에 있는 Redis는 어느 쪽인가요. 그걸 걷어내면 무슨 일이 벌어지는지 바로 답이 나오시나요. 안 나온다면 한 번쯤 들여다볼 만합니다.
댓글
댓글을 불러오는 중...