데이터베이스 샤딩(Sharding): 대규모 트래픽 지옥에서 벗어나기
동시 접속자 수가 기하급수적으로 늘어날 때, 데이터베이스는 가장 먼저 병목 현상을 겪는 지점 중 하나입니다.
이러한 한계를 극복하기 위한 강력한 기술인 샤딩(Sharding)에 대해 깊이 있게 알아보고, 도입 시 고려해야 할 점과 체크리스트까지 살펴보겠습니다.
샤딩(Sharding)의 등장
샤딩(Sharding)이란 데이터베이스를 수평적으로 분할하여 여러 개의 작은 데이터베이스(샤드)로 나누는 기술입니다. 즉, 하나의 거대한 데이터베이스를 여러 서버에 물리적으로 분산시켜 관리하는 방식입니다.
하나의 데이터베이스는 하나의 서버에 설치되므로 max_connections
와 같은 물리적인 한계가 명확합니다. 샤딩은 이 물리적인 한계를 극복하기 위해 더 많은 물리적 자원을 투입하는 전략입니다. 4개의 데이터베이스를 사용한다면, 이론적으로 처리 가능한 커넥션 수도 4배로 늘어나 CCU 증가에 효과적으로 대응할 수 있습니다.
샤딩의 장점
- 확장성(Scalability): 더 많은 커넥션을 처리할 수 있어 CCU가 늘어나도 안정적인 서비스 운영이 가능합니다.
- 성능(Performance): 각 샤드는 독립적으로 작동하여 요청을 병렬적으로 처리할 수 있으므로, 전반적인 시스템 성능이 향상됩니다.
- 가용성(Availability): 특정 샤드에 문제가 발생하더라도 다른 샤드는 영향을 받지 않으므로, 전체 서비스 중단을 막고 가용성을 높일 수 있습니다.
샤딩 적용 방식
수평적 샤딩은 주로 다음과 같은 키 기반 분할 방식을 사용합니다.
-
모듈로 기반 샤딩 (Modulo Sharding)
플레이어 ID와 같은 숫자 키를 샤드의 총 개수로 나눈 나머지를 이용해 샤드를 결정합니다.
// 플레이어 ID % 샤드의 총 개수 = 저장될 샤드 번호 const shardNo = playerId % NUM_OF_SHARDS;
-
범위 기반 샤딩 (Range-Based Sharding)
ID의 특정 범위를 기준으로 샤드를 할당합니다.
if (playerId >= 1 && playerId <= 1000) { return 0; // 샤드 0 } else if (playerId >= 1001 && playerId <= 2000) { return 1; // 샤드 1 } // ...
해시 함수와 샤딩
샤딩 키를 분산시킬 때 해시 함수가 중요한 역할을 합니다. 좋은 해시 함수는 데이터를 여러 샤드에 균등하게 분배하여 특정 샤드에만 부하가 쏠리는 것을 막아줍니다. 마치 자바스크립트의 `Map`이 내부적으로 해시 테이블을 사용하여 키를 효율적으로 관리하는 것과 유사합니다.
해시 함수에서 다른 키가 같은 해시 값을 갖는 해시 충돌(Hash Collision)은 피할 수 없는 문제입니다. 따라서 충돌을 최소화하고 해시 값을 최대한 균등하게 분산시키는 것이 좋은 해시 함수의 핵심 덕목입니다. 샤딩에서도 마찬가지로, 데이터가 모든 샤드에 고르게 분산되지 않으면 샤딩의 의미가 퇴색됩니다.
생각지도 못했던 문제: 리샤딩(Resharding)
모듈로 기반 샤딩(playerId % NUM_OF_SHARDS
)은 단순하지만 치명적인 문제가 있습니다. 바로 샤드의 수가 변경될 때입니다. 게임이 흥행하여 서버를 증설하거나, 혹은 반대의 이유로 축소해야 하는 상황을 가정해 봅시다. 샤드의 총 개수가 4개에서 3개로 줄어들면 어떻게 될까요?
playerId % 4
로 계산되던 샤드 위치가 playerId % 3
으로 바뀌면서, 대부분의 데이터가 원래 있던 샤드와 다른 샤드를 가리키게 됩니다. 이는 곧 대규모 데이터 재배치(Data Migration)를 의미하며, 이 과정에서 막대한 비용과 시스템 부하가 발생합니다.
기존 데이터를 그대로 두고 새로운 규칙을 적용하면 되지 않을까?
불가능합니다. 기존 사용자 데이터에 접근할 때도 새로운 규칙(
playerId % 3
)이 적용되므로, 데이터 이동 없이는 올바른 샤드를 찾을 수 없어 심각한 데이터 불일치 문제가 발생합니다.
샤드의 개수가 바뀔 때마다 이런 대공사가 발생한다면 매우 곤란합니다. 이 문제를 해결하기 위해 등장한 것이 바로 Consistent Hashing(안정 해시)입니다.
Consistent Hashing: 유연한 샤딩을 위한 해법
Consistent Hashing은 해시 링(Hash Ring)을 기반으로 동작하는 해싱 기법입니다. 데이터 키와 샤드 서버를 모두 동일한 해시 공간 위의 링에 배치합니다. 데이터는 링 위에서 시계 방향으로 가장 먼저 만나는 샤드에 저장됩니다.
이 방식의 가장 큰 장점은 샤드가 추가되거나 제거될 때 나타납니다. 샤드가 하나 제거되더라도, 영향을 받는 데이터는 제거된 샤드에 저장되어 있던 데이터에 한정됩니다. 나머지 데이터는 전혀 이동할 필요가 없어 데이터 재배치 비용을 최소화할 수 있습니다.
또한, 가상 노드(Virtual Nodes) 개념을 도입하면 데이터 분산을 더욱 균등하게 만들 수 있습니다. 하나의 물리적 샤드를 여러 개의 가상 노드로 만들어 링 전체에 흩뿌려 놓음으로써, 특정 샤드로 데이터가 편중될 위험을 줄일 수 있습니다.
샤딩의 어두운 면: 복잡성
샤딩은 성능과 확장성을 제공하지만, 그 대가로 엄청난 복잡성을 동반합니다.
분산 트랜잭션 (Distributed Transactions)
일반적인 트랜잭션은 단일 데이터베이스 내에서만 ACID 속성을 보장합니다. 하지만 샤딩 환경에서는 여러 데이터베이스에 걸쳐 데이터의 일관성을 유지해야 하는 경우가 발생합니다. 예를 들어, A 샤드에 있는 유저가 B 샤드에 있는 유저에게 아이템을 보내는 경우입니다.
이를 위해 분산 트랜잭션이 필요하며, 보통 Two-Phase Commit (2PC) 프로토콜을 사용합니다. 하지만 이 방식은 여러 데이터베이스와 통신해야 하므로 매우 느리고 복잡하며, 네트워크 장애 시 처리하기가 까다롭습니다. 이 때문에 메시지 큐나 SAGA 패턴과 같은 다른 접근법을 사용해 애플리케이션 레벨에서 트랜잭션을 구현하기도 합니다.
Cross-Shard JOIN
여러 샤드에 분산된 테이블들을 JOIN하는 쿼리는 매우 비효율적이고 복잡합니다. 각 샤드에서 필요한 데이터를 가져와 애플리케이션 레벨에서 조합해야 하므로, 성능 저하를 감수해야 합니다.
결론: 샤딩, 도입 전 반드시 확인할 체크리스트
샤딩은 강력한 기술이지만 명확한 단점 때문에 필요한 순간에 도입하는 것이 현명합니다. 샤딩 도입을 고려하기 전에 아래 체크리스트를 통해 스스로 점검해 보세요.
✨ 샤딩 도입 전 체크리스트 ✨
- [ ] 데이터베이스의 크기가 단일 서버로 처리하기 어려울 정도로 거대한가요?
- [ ] 동시 접속자가 많아 단일 DB 서버의 커넥션 한계를 초과하고 있습니까?
- [ ] 현재 데이터베이스의 성능이 부족하여 응답 시간이 길거나 처리 속도가 느린가요?
- [ ] 복잡한 샤드 관리와 모니터링을 처리할 수 있는 기술적 역량과 리소스가 충분한가요?
- [ ] 분산 트랜잭션, Cross-Shard Join 등의 복잡성을 감당하고 관리할 수 있습니까?
위 체크리스트에 자신 있게 '예'라고 답할 수 있을 때가 바로 샤딩 도입을 진지하게 고려할 시점입니다. 샤딩은 특정 상황에서 매우 유용한 기술이지만, 모든 문제의 해결책은 아니며 그로 인한 복잡성과 관리 부담도 충분히 고려해야 한다는 사실을 잊지 마세요.
'Daily Logs > TIL (Today I Learned)' 카테고리의 다른 글
개발자 구글링 올바르게 사용하는 방법 (1) | 2025.06.23 |
---|---|
대규모 서비스를 위한 백엔드 아키텍처 도구 (2) | 2025.06.16 |
데이터베이스 인덱스 (1) | 2025.06.14 |
DB part.7 (2) | 2025.06.13 |
DB part.6 (4) | 2025.06.12 |