왜 같은 데이터를 여러 곳에 저장할까?

MySQL, Redis, Elasticsearch, MongoDB가 서로 다른 문제를 해결하는 이유와 여러 저장소를 사용할 때 발생하는 동기화 비용을 쉽게 설명한다.

대용량 트래픽 시리즈 4/7

서비스는 어떻게 100만 사용자를 버틸까? — 대용량 트래픽과 데이터베이스를 공부하며 이해한 것들

이전 글 — 데이터베이스는 데이터가 많아지면 왜 느려질까?

온라인 쇼핑몰에서 “빨간색 러닝화”를 검색한다고 생각해 보겠습니다. 검색 화면은 상품 이름과 설명에서 관련 상품을 찾아야 합니다. 빨간색과 가격 조건으로 빠르게 걸러 내고, 인기 상품도 먼저 보여주면 좋습니다.

하지만 구매 버튼을 누르는 순간에는 우선순위가 달라집니다. 재고가 실제로 남았는지, 결제가 한 번만 처리되는지, 주문과 재고 변경이 함께 성공하거나 함께 실패하는지가 더 중요합니다.

검색과 구매는 같은 운동화 데이터를 사용합니다. 그런데 검색은 폭넓게 잘 찾는 능력이 중요하고, 구매는 틀리지 않는 능력이 중요합니다.

하나의 데이터베이스가 모든 일을 처리하면 가장 단순한데, 왜 서비스가 커지면 여러 저장소를 사용하기도 할까요?

하나의 운동화 상품 카드가 정확한 구매, 빠른 조회, 검색, 다양한 상품 정보 역할을 나타내는 네 개의 아이콘과 연결된 모습

같은 상품을 사용하더라도 구매, 반복 조회, 검색, 상품 정보 관리에는 서로 다른 능력이 필요하다는 점을 단순화한 그림입니다.

이 글은 대용량 트래픽과 데이터 저장 구조를 공부하며 이해한 내용을 제 언어로 정리한 학습 기록입니다. 제가 여러 데이터베이스를 대규모 프로덕션에서 운영한 경험이나 측정 결과를 소개하는 글은 아닙니다.

먼저 하나의 데이터베이스로 시작한다

작은 쇼핑몰이라면 하나의 데이터베이스로 시작하는 편이 일반적으로 더 단순합니다. 여기서는 MySQL을 예로 들어 보겠습니다.

MySQL은 RDBMS(Relational Database Management System, 관계형 데이터베이스 관리 시스템)입니다. 데이터를 표 형태로 저장하고 상품, 주문, 회원처럼 서로 관련된 정보의 관계와 규칙을 관리합니다.

MySQL 하나로도 상품 정보를 저장하고, 가격 조건으로 조회하고, 주문과 결제를 기록할 수 있습니다. 트랜잭션을 이용해 재고 감소와 주문 생성을 하나의 작업처럼 보호할 수도 있습니다.

MySQL이 검색을 할 수 없는 것은 아닙니다. 요구사항과 데이터 규모에 따라 조건 검색과 전문 검색 기능으로 충분할 수 있습니다. 데이터가 많아졌다는 이유만으로 반드시 다른 데이터베이스로 바꿔야 하는 것도 아닙니다.

하나의 데이터베이스가 필요한 기능과 응답시간을 만족한다면 저장소를 추가하지 않는 편이 더 안전합니다. 백업, 장애 대응, 접근 권한과 확인할 데이터가 한곳에 모여 있기 때문입니다.

저장소 분리는 유명한 기술을 더 많이 쓰기 위한 단계가 아닙니다. 실제로 해결해야 할 문제가 확인됐을 때 선택하는 대가가 있는 도구입니다.

정확한 거래를 책임지는 공식 원본

운동화의 공식 가격이 얼마인지, 재고가 몇 개 남았는지, 주문이 결제됐는지는 서로 충돌하면 안 됩니다. 이 예시에서는 MySQL이 상품, 재고, 주문의 Source of Truth(공식 원본 데이터)라고 가정하겠습니다.

공식 원본은 여러 시스템의 값이 다를 때 최종 판단 기준이 되는 저장소입니다. 검색 화면에 재고가 있다고 표시돼도 MySQL의 공식 재고가 0이라면 구매를 진행하면 안 됩니다.

결제 직전에는 가격과 재고를 공식 원본에서 다시 확인합니다. 주문 생성과 재고 감소도 트랜잭션으로 묶어 일부만 반영되는 상황을 막습니다.

다만 모든 시스템에서 MySQL이 반드시 공식 원본이라는 뜻은 아닙니다. 문서 작성 서비스라면 문서형 데이터베이스가 원본일 수 있고, 업무 경계에 따라 서로 다른 저장소가 각자 맡은 데이터의 원본이 될 수도 있습니다.

중요한 것은 제품 이름이 아니라 “어떤 값이 맞는지 마지막으로 판단할 곳”을 분명히 정하는 것입니다.

빠르게 꺼내 보는 임시 메모, Redis

쇼핑몰 첫 화면의 인기 상품 목록은 많은 사용자가 반복해서 봅니다. 매번 MySQL에서 같은 상품 이름, 대표 이미지와 요약 가격을 읽으면 공식 원본이 같은 질문을 계속 처리해야 합니다.

Cache(캐시)는 자주 조회하는 데이터를 더 가까운 곳에 임시로 복사해 빠르게 제공하는 방식입니다. Redis는 여러 기능을 가진 데이터 저장소지만, 이번 글에서는 인기 상품과 상품 요약 정보를 보관하는 캐시 역할에만 집중하겠습니다.

Redis에 인기 상품 목록이나 자주 보는 카테고리를 두면 반복 조회를 원본까지 보내지 않을 수 있습니다. 원본 데이터베이스는 주문과 재고처럼 더 중요한 작업에 자원을 사용할 여유를 얻습니다.

캐시는 원본을 대신하는 기억이 아니라, 빠르게 꺼내 보기 위해 만든 임시 메모에 가깝습니다.

임시 메모에는 대가가 있습니다. MySQL에서 운동화 가격을 바꿨지만 Redis에는 이전 가격이 잠시 남을 수 있습니다. 무엇을 저장하고 언제 지우며 얼마나 오래 유지할지도 정해야 합니다.

캐시가 만료되거나 사라진 순간 많은 요청이 동시에 원본으로 향할 수도 있습니다. 인기 상품 하나의 캐시가 끝났다고 모든 사용자가 동시에 MySQL에 같은 질문을 보내면, 캐시가 줄이려던 부하가 한꺼번에 돌아옵니다.

따라서 Redis를 추가하는 일은 조회 경로만 만드는 것이 아닙니다. 오래된 값을 지우는 방법, 캐시가 없을 때의 동작, 원본을 보호할 방법도 함께 설계하는 일입니다.

상품을 잘 찾기 위한 색인, Elasticsearch

이번에는 사용자가 정확한 상품명을 모른다고 해보겠습니다. 검색창에 “비 오는 날 신는 가벼운 운동화”라고 입력합니다.

Search Engine(검색 엔진)은 텍스트를 검색하기 좋은 단위로 분석하고, 어떤 문서에 어떤 단어가 있는지 찾을 수 있는 색인을 만듭니다. Elasticsearch는 이런 검색과 분석을 위해 설계된 시스템입니다.

Inverted Index(역색인)는 단어에서 그 단어가 들어 있는 문서로 찾아가는 구조입니다. 책마다 모든 단어를 읽는 대신 “러닝”이라는 단어가 등장한 상품 목록부터 찾아가는 방식과 비슷합니다.

Elasticsearch는 상품명과 설명을 검색용 단어로 나누고 관련도에 따라 결과를 정렬할 수 있습니다. 설정에 따라 오타, 유사어, 가격·색상 필터와 텍스트 검색을 조합하는 데도 도움을 줍니다.

하지만 Elasticsearch를 추가한다고 검색이 무조건 빨라지거나 좋아지는 것은 아닙니다. 어떤 단어를 같은 뜻으로 볼지, 관련도 순위를 어떻게 정할지, 데이터를 어떤 형태로 색인할지 설계해야 합니다.

이 예시에서 Elasticsearch는 주문과 재고의 공식 원본이 아닙니다. 검색 색인은 MySQL의 변경보다 늦게 갱신될 수 있습니다. 검색 결과에 재고가 있다고 보여도 구매 직전에는 공식 원본의 재고를 다시 확인해야 합니다.

MySQL 검색으로 요구사항을 만족한다면 Elasticsearch를 추가할 이유가 없습니다. 더 정교한 텍스트 분석과 검색 운영이 실제로 필요할 때 비교하는 선택지입니다.

모양이 다른 상품 정보와 MongoDB

상품마다 필요한 정보의 모양은 다릅니다. 노트북에는 CPU, RAM과 저장 용량이 필요합니다. 신발에는 발 크기, 색상과 소재가 필요합니다. 식품에는 원재료, 알레르기 정보와 유통기한이 중요합니다.

MongoDB는 Document Database(문서형 데이터베이스)입니다. 관련 정보를 JSON과 비슷한 하나의 문서 구조로 저장합니다. 함께 읽는 중첩 데이터를 한 문서로 표현하거나, 상품 종류마다 다른 필드를 다루는 업무에 편리할 수 있습니다.

그렇다고 MongoDB가 아무 규칙도 없는 데이터베이스라는 뜻은 아닙니다. 애플리케이션이 지켜야 할 데이터 규칙이 필요하고, Schema Validation(스키마 검증)을 사용해 필드의 자료형이나 허용 범위를 검사할 수도 있습니다.

데이터 사이의 관계가 중요하거나 여러 표를 함께 바꾸는 거래가 많다면 관계형 데이터베이스가 더 알맞을 수 있습니다. 조회 방식과 변경 규칙을 먼저 확인해야 합니다.

MongoDB가 MySQL의 복사본이어야 하는 것도 아닙니다. 상품 설명 편집처럼 문서 형태가 중요한 독립 업무의 공식 원본이 될 수 있습니다. 반대로 그런 요구가 없다면 강의나 유명 서비스에서 사용한다는 이유만으로 추가할 필요가 없습니다.

같은 상품도 목적에 따라 다르게 저장된다

같은 운동화와 관련된 정보라도 각 저장소가 똑같은 데이터를 전부 가질 필요는 없습니다. 이 표는 원리를 이해하기 위한 하나의 예시입니다.

목적 저장소 예시 저장하는 내용
정확한 상품·주문·재고 MySQL 공식 가격, 재고와 거래 정보
빠른 반복 조회 Redis 인기 상품과 자주 보는 상품의 임시 요약
상품 검색 Elasticsearch 상품명, 설명, 색상과 검색 필터
문서형 업무 MongoDB 형태가 다양한 상품 설명이나 독립 문서 데이터

Redis에는 상품 상세 설명 전체가 아니라 화면에 필요한 이름, 대표 이미지와 가격만 둘 수 있습니다. Elasticsearch에는 결제 정보 대신 검색에 필요한 상품명, 설명과 필터만 색인할 수 있습니다.

MongoDB도 자동으로 MySQL의 상품을 복제한다는 뜻이 아닙니다. 별도의 문서형 업무가 있을 때 그 업무에 필요한 데이터 모델을 가질 수 있습니다.

저장소별로 필요한 데이터만 두면 복제 범위와 개인정보 노출을 줄일 수 있습니다. 대신 어떤 필드가 어느 저장소에 있고 누가 갱신하는지 분명히 기록해야 합니다.

복사본은 잠시 이전 값을 보여줄 수 있다

운동화 가격을 50,000원에서 45,000원으로 바꾸는 순간을 따라가 보겠습니다.

  1. 공식 원본인 MySQL의 가격이 45,000원으로 변경됩니다.
  2. Redis에는 잠시 50,000원이 남아 있습니다.
  3. Elasticsearch 검색 결과도 아직 50,000원을 보여줄 수 있습니다.
  4. 변경 정보가 전달되면 두 복사본도 45,000원으로 바뀝니다.

이처럼 변경 직후에는 시스템마다 값이 다를 수 있지만 시간이 지나면 같은 값으로 맞춰지는 방식을 Eventual Consistency(최종 일관성)라고 합니다.

공식 원본의 가격이 먼저 바뀌고, 변경 이벤트를 받은 Redis와 Elasticsearch가 차례로 갱신됩니다. 전달이 끝나기 전에는 복사본이 잠시 이전 값을 보여줄 수 있으므로 결제 가격과 재고는 원본에서 다시 확인해야 합니다.

최종 일관성이 모든 데이터에 허용되는 것은 아닙니다. 검색 목록에서 가격이 잠시 늦게 바뀌는 것은 정책에 따라 허용할 수 있지만, 실제 결제 금액과 재고를 오래된 복사본만 믿고 결정해서는 안 됩니다.

검색 화면과 상품 목록은 잠시 이전 가격을 보여줄 수 있습니다. 구매 버튼을 누른 뒤에는 공식 원본에서 가격과 재고를 다시 확인하고, 화면의 값과 다르면 사용자에게 알려야 합니다.

얼마나 늦어도 되는지는 업무마다 다릅니다. 허용할 수 있는 지연을 정하지 않았다면 “나중에 맞아진다”는 말만으로는 안전한 설계가 되지 않습니다.

두 곳에 직접 쓰면 실패가 둘로 나뉜다

애플리케이션이 MySQL에 상품을 저장한 직후 Elasticsearch에도 직접 저장한다고 생각해 보겠습니다. MySQL 저장은 성공했지만 네트워크 문제로 Elasticsearch 저장이 실패할 수 있습니다.

첫 번째 저장을 되돌리기도 어렵고, 실패한 두 번째 저장을 잊어버리면 검색 결과가 계속 오래된 상태로 남습니다. 저장소를 추가하는 순간 데이터 동기화와 실패 복구도 함께 설계해야 합니다.

실패한 전달은 다시 시도할 수 있어야 합니다. 같은 변경이 여러 번 전달돼도 결과가 망가지지 않아야 하고, 필요하면 검색 색인을 원본으로부터 다시 만들 수 있어야 합니다.

한 단계 더 깊게: 변경을 안전하게 전달하는 방법

Transactional Outbox Pattern(트랜잭셔널 아웃박스 패턴)은 원본 데이터 변경과 전달할 이벤트를 같은 데이터베이스 트랜잭션에 기록합니다. 별도 작업자가 아웃박스에 남은 이벤트를 읽어 다른 시스템에 전달합니다.

이 방법은 상품 변경은 저장됐는데 전달할 사실 자체가 사라지는 틈을 줄입니다. 다만 이벤트를 여러 번 전달할 가능성과 처리 순서, 실패한 이벤트의 재시도를 다뤄야 합니다.

CDC(Change Data Capture, 변경 데이터 캡처)는 데이터베이스의 변경 기록을 읽어 다른 시스템에 전달하는 방식입니다. 예를 들어 도구가 MySQL의 변경 로그를 읽고 상품 가격이 바뀌었다는 이벤트를 만들 수 있습니다.

Outbox와 CDC가 모든 동기화 문제를 자동으로 해결하지는 않습니다. 전달 지연, 중복, 순서 변경과 소비자 장애를 관찰하고 복구하는 설계가 여전히 필요합니다. 이번 글에서는 원리까지만 다루고 구체적인 구현은 별도 주제로 남기겠습니다.

저장소가 늘어나면 운영 범위도 넓어진다

데이터베이스가 하나 늘면 접속 주소 하나만 추가되는 것이 아닙니다.

  • 배포하고 운영할 시스템과 모니터링 대상이 늘어납니다.
  • 장애 지점, 백업과 복구 절차가 늘어납니다.
  • 버전 업그레이드와 호환성 확인이 필요합니다.
  • 접근 권한, 네트워크와 보안 설정을 저장소마다 관리해야 합니다.
  • 데이터 동기화 지연과 누락을 검증해야 합니다.
  • 개발자가 각 저장소의 동작과 장애 방식을 학습해야 합니다.
  • 서버, 저장 공간과 운영 도구 비용이 늘어납니다.

개인정보가 복제된다면 책임은 더 커집니다. 이름이나 연락처가 MySQL뿐 아니라 검색 색인과 캐시에도 들어 있다면 암호화, 마스킹, 접근 통제와 삭제 요청을 모든 위치에서 처리해야 합니다.

복사본이라는 이유로 보안 기준이 낮아지지 않습니다. 어떤 개인정보가 어디에 저장됐는지 알 수 없다면 사용자의 삭제 요청이나 사고 대응도 완전하게 수행하기 어렵습니다.

저장소를 추가하기 전에 물어볼 것

새 저장소를 도입하기 전에는 다음 질문에 답할 수 있어야 합니다.

  1. 현재 데이터베이스로 요구사항을 충족할 수 없는가?
  2. 실제 측정에서 조회, 검색 또는 데이터 모델의 문제가 확인됐는가?
  3. 캐시, 검색, 문서형 데이터처럼 정말 다른 저장 방식이 필요한가?
  4. 복사본이 원본과 잠시 달라도 허용되는가?
  5. 동기화가 실패했을 때 다시 처리하거나 다시 만들 수 있는가?
  6. 운영 인력이 여러 시스템의 장애, 백업과 보안을 감당할 수 있는가?
  7. 쿼리 개선이나 단순한 데이터 모델 변경처럼 더 작은 해결책은 없는가?

“유명한 회사가 사용한다”는 답은 충분하지 않습니다. 그 회사와 내 서비스는 데이터, 트래픽, 팀 규모와 장애 비용이 다릅니다.

사용자가 많다는 이유만으로 MySQL, Redis, Elasticsearch와 MongoDB를 한꺼번에 넣지 않습니다. 현재 구조에서 측정된 문제와 분명한 요구사항이 있을 때 하나씩 비교해야 합니다.

저장소의 수보다 역할의 경계가 중요하다

데이터를 여러 곳에 저장하는 이유는 기술을 많이 사용하기 위해서가 아닙니다. 정확한 거래, 빠른 조회, 텍스트 검색, 다양한 데이터 형태처럼 서로 충돌할 수 있는 요구사항을 각 목적에 맞는 방식으로 처리하기 위해서입니다.

같은 데이터 전체를 무조건 복사할 필요도 없습니다. Redis에는 임시 요약을, Elasticsearch에는 검색할 필드를, 독립적인 문서 업무에는 MongoDB 문서를 둘 수 있습니다.

하지만 저장소가 늘어날수록 동기화, 장애, 보안과 운영 비용도 함께 늘어납니다. 하나의 데이터베이스로 충분하다면 그 단순함을 유지하는 것이 좋은 설계입니다.

다음 편에서는 알림 10만 건을 한꺼번에 보내면 왜 서버가 멈출 수 있는지, 그리고 메시지 큐가 어떻게 일을 나누어 처리하는지 알아보겠습니다.

아직 5편은 발행되지 않았으므로 존재하지 않는 링크는 만들지 않습니다.

더 읽어보기