한 서버를 넘어서면 문제가 달라진다
초기 서비스의 데이터베이스는 보통 한 대의 서버에서 시작한다. 이 구조는 단순하다. 쓰기와 읽기가 같은 곳에서 일어나고, 장애 지점도 비교적 명확하다. 성능이 부족하면 CPU, 메모리, 디스크를 더 좋은 장비로 바꾸면 된다.
하지만 서비스가 성장하면 한 서버가 감당하기 어려운 순간이 온다. 읽기 요청이 많아지고, 저장해야 할 데이터가 늘어나고, 장애가 났을 때 서비스 전체가 멈추는 위험도 커진다. 이때 데이터베이스는 여러 노드로 나뉘기 시작한다.
문제는 여기서부터다. 여러 노드에 데이터를 저장하면 성능과 가용성은 좋아질 수 있지만, 데이터가 항상 같은 순간에 같은 값을 가진다고 말하기 어려워진다. 즉, 확장 문제는 곧 일관성 문제와 연결된다.
분산된 데이터베이스를 이해할 때는 다음 질문을 계속 던져야 한다.
쓰기는 어느 노드가 받는가?
읽기는 어느 노드에서 처리하는가?
복사본은 얼마나 늦게 최신 상태가 되는가?
특정 데이터는 어느 샤드에 저장되는가?
노드나 네트워크가 실패했을 때 어떤 값을 믿을 것인가?
AI 생성 이미지
복제: 같은 데이터를 여러 노드에 둔다
복제는 같은 데이터를 여러 데이터베이스 노드에 복사해 두는 방식이다. 가장 흔한 구조는 리더-팔로워 복제다. 리더는 쓰기를 받고, 팔로워는 리더의 변경 로그를 따라가며 같은 데이터를 복사한다.
일반적인 흐름은 다음과 같다.
애플리케이션이 리더에
UPDATE,INSERT,DELETE를 보낸다.리더는 변경을 커밋한다.
변경 로그가 팔로워로 전달된다.
팔로워는 로그를 적용해 리더와 같은 상태가 된다.
애플리케이션은 읽기 요청 일부를 팔로워로 보낸다.
이 구조의 장점은 명확하다. 읽기 요청을 여러 팔로워로 분산할 수 있다. 상품 상세 조회, 게시글 조회, 관리자 목록 조회처럼 읽기가 많은 기능에서 효과가 크다. 또한 리더 장애 시 팔로워 중 하나를 새 리더로 승격할 수 있어 가용성도 높일 수 있다.
하지만 복제는 공짜가 아니다. 가장 중요한 비용은 복제 지연이다. 리더에서 커밋된 변경이 팔로워에 적용되기까지 시간이 걸린다. 보통은 수 밀리초에서 수 초일 수 있지만, 트래픽 증가, 긴 쿼리, 네트워크 문제, 디스크 병목이 있으면 더 길어질 수 있다.
개발자 입장에서는 "쓰기 성공 후 바로 읽었는데 예전 값이 보이는" 현상으로 나타난다. 데이터베이스가 틀린 것이 아니라, 읽은 노드가 아직 최신 변경을 받지 못한 것이다.
복제 방식에 따라 이 문제가 줄어들 수도 있다. 예를 들어 동기 복제나 준동기 복제는 커밋 전에 일부 복제본의 확인을 기다리므로 데이터 유실 가능성을 낮출 수 있다. 대신 쓰기 지연이 커지고, 복제본이나 네트워크에 문제가 생겼을 때 쓰기 가능성이 떨어질 수 있다. 결국 복제에서도 성능, 가용성, 일관성 사이의 균형을 선택해야 한다.
읽기 후 쓰기 일관성: 프로필 수정 사례
사용자가 프로필 이름을 민수에서 김민수로 바꾼다고 하자. 요청 흐름은 다음과 같다.
1. PATCH /me 요청이 리더 DB에 반영된다.
2. 서버는 성공 응답을 반환한다.
3. 사용자가 곧바로 GET /me를 호출한다.
4. 이 읽기 요청은 팔로워 DB에서 처리된다.
5. 팔로워에는 아직 변경이 적용되지 않아 이름이 `민수`로 보인다.사용자에게는 저장이 실패한 것처럼 보인다. 새로고침을 몇 번 하면 갑자기 바뀐 값이 보일 수도 있다. 이런 현상을 읽기 후 쓰기 일관성이 깨졌다고 말한다. 방금 내가 쓴 값을 내가 다시 읽었을 때 볼 수 있어야 한다는 기대가 깨진 것이다.
이 문제를 줄이는 방법은 여러 가지다.
첫째, 쓰기 직후 일정 시간 동안 같은 사용자의 읽기를 리더로 보낸다. 프로필 수정, 비밀번호 변경, 배송지 추가처럼 사용자 본인이 즉시 확인하는 데이터에 적합하다. 모든 읽기를 리더로 보내면 복제의 이점이 줄어드므로 범위를 제한해야 한다.
둘째, 세션 단위로 최신 위치를 기억한다. 예를 들어 쓰기 시점의 로그 위치나 버전을 기록해 두고, 팔로워가 그 위치까지 따라온 경우에만 읽게 한다. 구현은 복잡하지만, 읽기 확장과 일관성 사이의 균형을 맞출 수 있다.
셋째, 중요한 화면은 리더에서 읽는다. 결제 결과, 주문 완료, 계정 보안 설정처럼 오래된 값을 보여주면 사용자가 혼란을 겪거나 사고가 날 수 있는 기능은 리더 읽기를 선택하는 편이 안전하다.
넷째, UI에서 낙관적으로 반영하되 원본 검증을 놓치지 않는다. 저장 성공 후 화면에는 새 값을 즉시 보여줄 수 있다. 다만 이후 서버에서 다시 읽은 값과 충돌할 가능성은 고려해야 한다. UI 보정은 사용자 경험을 돕는 방법이지 데이터 일관성의 근본 해결책은 아니다.
복제 지연은 운영 지표로도 관리해야 한다. "팔로워가 리더보다 몇 초 늦는가"는 단순한 내부 지표가 아니라 사용자 경험과 직접 연결된다. 특히 쓰기 직후 확인이 많은 기능에서는 평균 지연보다 최악의 지연, 즉 가끔 몇 초 이상 늦어지는 상황이 더 문제가 될 수 있다.
장애 조치: 팔로워를 리더로 올릴 때 생기는 일
리더가 장애를 일으키면 시스템은 팔로워 중 하나를 새 리더로 승격할 수 있다. 이를 장애 조치라고 한다. 문제는 어떤 팔로워를 새 리더로 삼을지, 이전 리더가 다시 살아났을 때 어떻게 처리할지다.
비동기 복제에서는 리더에 커밋됐지만 팔로워에 아직 전달되지 않은 변경이 있을 수 있다. 이 상태에서 팔로워를 리더로 승격하면 일부 커밋 데이터가 새 리더에는 없는 상태가 된다. 사용자 입장에서는 성공했던 작업이 사라진 것처럼 보일 수 있고, 이전 리더가 가진 데이터와 새 리더의 데이터가 충돌할 수도 있다.
반대로 이전 리더가 뒤늦게 살아나면 두 리더가 동시에 쓰기를 받는 위험도 있다. 이를 흔히 split brain이라고 부른다. split brain이 생기면 어느 쪽의 쓰기를 정답으로 볼지 결정하기 어려워지고, 나중에 데이터를 합치는 과정도 매우 복잡해진다.
그래서 운영 환경에서는 자동 장애 조치를 너무 가볍게 보면 안 된다. 승격 기준, 헬스 체크, 쓰기 차단, 애플리케이션 연결 전환, 이전 리더 재합류 절차가 필요하다. PostgreSQL이나 MySQL의 복제 구성에서도 이 부분은 도구가 일부 도와줄 뿐, 서비스의 장애 허용 범위와 복구 절차는 팀이 결정해야 한다.
장애 조치를 설계할 때는 "얼마나 빨리 새 리더를 세울 것인가"만 보지 말고 "데이터 유실을 얼마나 허용할 것인가"도 함께 봐야 한다. 빠른 전환은 중요하지만, 잘못된 전환은 장애 시간을 줄이는 대신 데이터 정합성 문제를 더 크게 만들 수 있다.
샤딩: 데이터를 나누어 저장한다
복제가 같은 데이터를 여러 노드에 복사하는 방식이라면, 샤딩은 데이터를 여러 노드에 나누어 저장하는 방식이다. 예를 들어 사용자 ID가 1번부터 1억 번까지 있을 때, 일부 사용자는 샤드 A에, 일부 사용자는 샤드 B에 저장한다.
샤딩의 목적은 저장 용량과 쓰기 처리량을 늘리는 것이다. 복제만으로는 모든 데이터가 각 노드에 존재하므로 전체 저장 용량 한계를 넘기 어렵다. 샤딩은 각 노드가 전체 데이터의 일부만 담당하게 하므로 더 큰 데이터셋을 다룰 수 있다.
하지만 샤딩의 핵심은 "무엇으로 나눌 것인가"다. 이 기준을 샤딩 키라고 한다.
사용자 중심 서비스라면 user_id가 좋은 후보가 될 수 있다. 한 사용자의 프로필, 설정, 알림, 주문 목록을 자주 함께 조회한다면 같은 샤드에 모으는 것이 유리하다. 반면 주문을 시간순으로 전체 집계하거나 운영자가 모든 주문을 검색해야 한다면 user_id 기준 샤딩은 불편할 수 있다.
주문 중심 서비스에서 order_id를 샤딩 키로 쓰면 주문 단건 조회는 쉽다. 하지만 특정 사용자의 모든 주문을 찾으려면 여러 샤드를 뒤져야 할 수 있다. tenant_id를 기준으로 나누면 기업 고객별 데이터 격리가 쉬워지지만, 아주 큰 고객 하나가 특정 샤드에 몰리는 문제가 생길 수 있다.
샤딩 키는 다음에 영향을 준다.
데이터 분포: 특정 샤드에 데이터가 몰리지 않는가?
핫스팟: 특정 키나 범위에 쓰기가 집중되지 않는가?
조회 지역성: 자주 함께 읽는 데이터가 같은 샤드에 있는가?
조인 가능성: 조인해야 하는 데이터가 같은 샤드에 있는가?
재분배 비용: 나중에 샤드 수를 늘릴 때 얼마나 어려운가?
예를 들어 생성 시간이 증가하는 created_at이나 순차 증가 ID를 기준으로 범위 샤딩을 하면 최근 데이터가 한 샤드에 몰릴 수 있다. 이벤트 로그처럼 최신 쓰기가 많은 시스템에서는 심각한 핫스팟이 된다. 해시 기반 샤딩은 분포를 고르게 만들기 쉽지만, 범위 조회와 정렬에는 불리할 수 있다.
샤딩은 애플리케이션 코드에도 영향을 준다. 단일 데이터베이스에서는 단순했던 조인, 유니크 제약, 트랜잭션이 여러 샤드로 흩어지면 어려워진다. "이메일은 전체 사용자에서 유일해야 한다" 같은 규칙도 한 샤드만 보면 검증할 수 없다. 별도의 전역 인덱스, 중앙 검증 테이블, 또는 이메일처럼 유일해야 하는 값을 기준으로 한 별도 조회 경로가 필요할 수 있다.
또한 샤딩은 한번 도입하면 되돌리기 어렵다. 샤드 수를 늘리거나 키 전략을 바꾸려면 데이터를 옮기고, 라우팅 규칙을 바꾸고, 이전 데이터와 새 데이터를 동시에 처리하는 기간을 견뎌야 한다. 그래서 샤딩은 단순히 "트래픽이 많아졌으니 나누자"가 아니라, 데이터 증가 속도와 조회 패턴을 보고 신중하게 결정해야 한다.
수직 확장과 수평 확장
데이터베이스 확장에는 크게 두 방향이 있다. 수직 확장은 더 강한 한 대의 서버로 키우는 방식이고, 수평 확장은 여러 대의 서버로 나누는 방식이다.
| 구분 | 수직 확장 | 수평 확장 |
|---|---|---|
| 방식 | CPU, 메모리, 디스크가 큰 장비로 교체 | 노드를 추가해 부하를 분산 |
| 장점 | 구조가 단순하고 애플리케이션 변경이 적음 | 용량과 처리량을 크게 늘릴 수 있음 |
| 단점 | 장비 한계와 비용 증가가 빠르게 옴 | 복제, 샤딩, 라우팅, 일관성 문제가 생김 |
| 운영 난이도 | 비교적 낮음 | 높음 |
| 장애 영향 | 한 서버 의존도가 큼 | 설계에 따라 일부 장애를 흡수 가능 |
| 애플리케이션 영향 | 적음 | 쿼리, 트랜잭션, 데이터 배치 전략 변경 가능 |
실무에서는 수직 확장을 먼저 고려하는 경우가 많다. 인덱스와 쿼리를 개선하고, 커넥션 풀과 캐시를 조정하고, 장비 사양을 올리는 것이 샤딩보다 훨씬 싸고 안전할 때가 많다.
하지만 데이터 크기나 쓰기량이 계속 증가해 단일 서버의 한계를 넘는다면 수평 확장이 필요하다. 이때는 단순히 노드를 늘리는 문제가 아니다. 애플리케이션이 어느 노드에 접근해야 하는지 알아야 하고, 장애 시 다른 노드로 어떻게 전환할지도 설계해야 한다.
분산 데이터베이스는 복잡성을 숨기지만 없애지는 않는다
분산 데이터베이스는 복제, 샤딩, 장애 조치, 데이터 재배치를 데이터베이스 시스템이 더 많이 담당하는 형태다. MongoDB의 샤딩 클러스터, Cassandra 같은 분산 저장소, CockroachDB나 YugabyteDB 같은 분산 SQL 데이터베이스가 예다.
이런 시스템은 애플리케이션이 직접 샤드 위치를 관리하지 않아도 되게 해준다. 일부는 SQL과 트랜잭션을 제공하면서 여러 노드에 데이터를 나누어 저장한다. 운영 편의성이 커질 수 있다.
그러나 복잡성이 사라지는 것은 아니다. 네트워크를 건너는 쿼리는 느리다. 여러 노드가 합의해야 하는 쓰기는 단일 노드 쓰기보다 지연이 커질 수 있다. 분산 트랜잭션은 편리하지만, 충돌과 재시도, 타임아웃 가능성을 애플리케이션이 이해해야 한다.
따라서 분산 데이터베이스를 선택할 때는 "자동으로 확장된다"는 문구보다 다음을 확인해야 한다.
주된 쿼리가 단일 파티션에서 끝나는가?
여러 노드 조인이 자주 필요한가?
쓰기 지연 증가를 서비스가 견딜 수 있는가?
장애 시 읽기와 쓰기 중 무엇을 우선할 것인가?
팀이 클러스터 운영, 리밸런싱, 백업과 복구를 감당할 수 있는가?
분산 데이터베이스는 좋은 선택지가 될 수 있지만, 모든 서비스의 기본값은 아니다. 단일 노드에서 충분히 빠르고 안전하게 운영할 수 있는 규모라면, 먼저 단순한 구조를 유지하는 편이 더 나을 때가 많다.
CAP를 실무 언어로 이해하기
CAP는 분산 시스템에서 자주 나오는 개념이다. 세 가지 요소를 말한다.
일관성 Consistency: 성공한 쓰기 이후의 읽기가 그 결과를 일관되게 볼 수 있는가?
가용성 Availability: 정상적으로 동작 중인 노드가 받은 요청에 실패 대신 응답할 수 있는가?
파티션 허용성 Partition tolerance: 노드 사이의 네트워크가 끊기거나 지연되어도 시스템이 동작을 계속할 수 있는가?
분산 시스템에서는 네트워크 단절을 피할 수 없다. 같은 데이터센터 안에서도 순간적인 지연이나 연결 실패가 생길 수 있고, 여러 리전에 걸치면 더 흔해진다. 그래서 현실에서는 파티션 허용성을 포기하기 어렵다. 남는 질문은 네트워크 문제가 있을 때 일관성과 가용성 중 무엇을 우선할 것인가다.
예를 들어 은행 잔액 이체 시스템은 오래된 값을 기준으로 출금하면 안 된다. 네트워크가 불안정하면 일부 요청을 실패시키더라도 강한 일관성을 선택하는 편이 맞다. 반대로 게시글 좋아요 수, 추천 피드, 접속 상태 표시는 잠시 오래된 값이어도 서비스 응답을 유지하는 것이 더 중요할 수 있다.
CAP는 특정 데이터베이스를 단순히 CP나 AP로 분류하는 표가 아니다. 더 중요한 것은 기능별 판단이다. 같은 서비스 안에서도 결제, 재고, 권한은 일관성을 우선하고, 통계 카운터, 알림 배지, 추천 목록은 어느 정도 지연을 허용할 수 있다.
또한 CAP만으로 모든 설계를 설명할 수 있는 것은 아니다. 실제 시스템에서는 지연 시간, 운영 복잡도, 데이터 모델, 장애 복구 방식, 비용도 함께 봐야 한다. CAP는 "네트워크가 실패했을 때 무엇을 포기할 것인가"를 생각하게 해주는 출발점으로 보는 편이 좋다.
개발자가 기억해야 할 판단 기준
여러 노드로 확장하는 순간 데이터베이스 설계는 저장 구조를 넘어 운영 정책이 된다. 읽기를 팔로워로 보낼지, 쓰기 직후에는 리더를 읽을지, 어떤 키로 샤딩할지, 장애 시 요청을 실패시킬지 오래된 값을 보여줄지 결정해야 한다.
좋은 분산 설계는 모든 문제를 완벽히 막는 설계가 아니다. 어떤 데이터가 반드시 최신이어야 하는지, 어떤 데이터는 잠시 늦어도 되는지 구분하는 설계다.
실무에서는 다음 기준으로 판단하면 좋다.
사용자 본인이 방금 쓴 데이터는 가능한 한 즉시 다시 보여준다.
돈, 재고, 권한처럼 틀리면 안 되는 데이터는 오래된 읽기를 피한다.
팔로워 읽기는 복제 지연을 감시하면서 제한적으로 적용한다.
샤딩 키는 현재 화면이 아니라 장기적인 조회 패턴과 데이터 분포를 기준으로 정한다.
샤딩은 마지막 수단에 가깝다. 먼저 쿼리, 인덱스, 캐시, 수직 확장을 검토한다.
분산 데이터베이스를 쓰더라도 재시도, 타임아웃, 중복 처리, 장애 복구는 애플리케이션 책임으로 남는다.
데이터가 여러 노드로 나뉘면 성능은 좋아질 수 있다. 대신 "언제나 최신 값 하나만 존재한다"는 단순한 가정은 깨진다. 개발자는 이제 빠른 시스템뿐 아니라, 지연과 실패 속에서도 사용자가 납득할 수 있는 일관성을 설계해야 한다.
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.