2026년 5월 16일

CAP 이론과 일관성 모델 마스터하기: 분산 시스템 설계의 나침반

100
CAP 이론과 일관성 모델 마스터하기: 분산 시스템 설계의 나침반

CAP 이론과 일관성 모델 마스터하기: 분산 시스템 설계의 나침반

CAP 이론과 일관성 모델 마스터하기: 분산 시스템 설계의 나침반

안녕하세요! 10년 경력의 소프트웨어 엔지니어이자 기술 교육자로서, 오늘은 분산 시스템을 설계하고 이해하는 데 있어 가장 근본적이고 중요하지만, 종종 오해되기도 하는 개념인 CAP 이론과 다양한 일관성 모델에 대해 이야기해보고자 합니다. 이 주제는 면접에서도 단골 질문이며, 실제 시스템을 구축할 때 올바른 아키텍처를 선택하는 데 필수적인 지식입니다.

1. 개념 소개: 분산 시스템의 불가피한 선택

1. 개념 소개: 분산 시스템의 불가피한 선택

정의: CAP 이론이란 무엇인가?

CAP 이론은 분산 컴퓨팅 시스템이 동시에 만족시킬 수 없는 세 가지 보장 속성을 제시합니다. 이 세 가지 속성은 다음과 같습니다:

  • 일관성 (Consistency, C): 모든 클라이언트가 항상 최신 데이터를 볼 수 있음을 보장합니다. 즉, 어떤 노드에 쓰인 데이터든, 모든 후속 읽기 작업은 해당 쓰기 작업을 반영해야 합니다. 이는 마치 모든 사람이 똑같은 시계를 보고 똑같은 시간 정보를 얻는 것과 같습니다.
  • 가용성 (Availability, A): 모든 클라이언트 요청에 대해 (성공 또는 실패) 응답을 받을 수 있음을 보장합니다. 시스템의 일부 노드에 장애가 발생하더라도, 나머지 노드들은 계속해서 요청을 처리하고 응답할 수 있어야 합니다. 이는 언제든지 은행 창구에 가서 업무를 볼 수 있는 것과 비슷합니다.
  • 분할 내성 (Partition Tolerance, P): 분산 시스템에서 노드 간의 통신이 중단되는 '네트워크 분할' 상황에서도 시스템이 계속해서 작동할 수 있음을 보장합니다. 네트워크 분할은 노드들이 서로 통신하지 못하게 되는 상황을 의미하며, 이는 분산 시스템에서 피할 수 없는 현실입니다. 도로가 끊겨도 각 지역의 상점은 계속 영업하는 것과 유사합니다.

CAP 이론은 "분산 시스템은 일관성, 가용성, 분할 내성 중 최대 두 가지만 동시에 만족할 수 있다"고 말합니다. 즉, 이 세 가지 중 하나는 반드시 포기해야 한다는 의미입니다.

탄생 배경: 분산 시스템의 태동과 함께

CAP 이론은 2000년 Eric Brewer 교수가 제안한 "Brewer's Conjecture"에서 시작되었습니다. 당시 인터넷의 폭발적인 성장과 함께 대규모 분산 시스템의 필요성이 대두되면서, 여러 대의 컴퓨터가 협력하여 하나의 서비스를 제공하는 방식이 일반화되기 시작했습니다. 하지만 네트워크 불안정성, 노드 장애 등 분산 시스템이 직면하는 고유한 문제들이 발생했고, 이로 인해 데이터의 무결성과 서비스의 연속성을 동시에 보장하기가 매우 어려워졌습니다.

Brewer 교수는 이러한 분산 시스템의 본질적인 한계를 명확히 하고, 시스템 설계자들이 어떤 속성을 우선시할지 선택해야 함을 강조했습니다. 이후 2002년 Seth Gilbert와 Nancy Lynch에 의해 수학적으로 증명되면서 "CAP Theorem"으로 자리 잡게 되었습니다.

왜 중요한가?: 트레이드오프의 이해

CAP 이론이 중요한 이유는 분산 시스템을 설계할 때 우리가 마주하는 근본적인 트레이드오프를 이해하게 해주기 때문입니다. 현실의 분산 시스템에서 네트워크 분할은 언젠가는 반드시 발생합니다. 따라서 P (분할 내성)는 사실상 포기할 수 없는 속성입니다. 그렇다면 우리의 선택은 P를 전제로 **C (일관성)**를 선택할 것인지, 아니면 **A (가용성)**를 선택할 것인지로 귀결됩니다.

  • CP 시스템 (Consistency + Partition Tolerance): 네트워크 분할 상황에서 일관성을 유지하기 위해 시스템의 일부 노드가 요청에 응답하지 않거나(가용성 저하), 쓰기 작업을 거부할 수 있습니다. 예를 들어, 은행 시스템에서 계좌 잔고의 정확성이 최우선이라면, 네트워크 문제 시 잠시 서비스가 중단되더라도 데이터의 일관성을 유지하는 것이 중요합니다.
  • AP 시스템 (Availability + Partition Tolerance): 네트워크 분할 상황에서도 모든 요청에 응답하여 가용성을 유지하지만, 이로 인해 데이터의 일관성이 일시적으로 깨질 수 있습니다. 즉, 다른 노드들이 아직 최신 데이터를 반영하지 못할 수 있습니다. 예를 들어, 소셜 미디어 피드나 전자상거래 상품 목록 등은 잠시 오래된 정보가 보이더라도 서비스가 계속 제공되는 것이 더 중요할 수 있습니다.

이러한 이해 없이는 시스템이 장애 상황에서 어떻게 동작할지 예측할 수 없으며, 잘못된 선택은 심각한 데이터 손실이나 서비스 중단으로 이어질 수 있습니다.

2. 핵심 원리 설명: CAP 트레이드오프와 일관성 모델

2. 핵심 원리 설명: CAP 트레이드오프와 일관성 모델

CAP 이론의 핵심은 네트워크 분할(P)이 발생했을 때, 시스템이 C와 A 중 어떤 것을 포기할 것인가를 결정해야 한다는 것입니다. 분할이 없는 정상 상황에서는 C와 A를 모두 제공할 수 있습니다.

CAP 삼각형 다이어그램 (개념적 설명)

CAP 이론을 설명할 때 흔히 사용되는 것이 'CAP 삼각형'입니다. 상상해보세요. 세 꼭짓점에 각각 C, A, P가 있는 삼각형이 있습니다. 우리는 이 삼각형의 세 변 중 두 변만을 선택할 수 있습니다. 하지만 분산 시스템에서는 P를 피할 수 없으므로, 사실상 C와 A 중 하나를 선택하는 상황에 놓이게 됩니다.

  • CP 시스템: 네트워크 분할 시, 노드 간 데이터 불일치를 막기 위해 일부 노드를 격리시키거나, 쓰기 요청을 거부하여 일관성을 보장합니다. 이 경우, 해당 노드에 접근하려는 클라이언트는 응답을 받지 못하거나(가용성 저하), 오래된 데이터를 읽는 것을 방지하기 위해 대기해야 합니다.
    • 비유: 여러 명의 점원이 있는 은행이라고 생각해봅시다. 어느 날 갑자기 전산망이 끊겨 일부 점원들이 본점 서버와 통신할 수 없게 되었습니다. 이때 은행은 고객의 돈이 이중으로 인출되거나 잘못 입금되는 것을 막기 위해, 통신이 끊긴 점원들의 업무를 중단시킵니다. (일관성 유지, 가용성 저하)
  • AP 시스템: 네트워크 분할 시에도 모든 노드가 계속해서 요청에 응답하여 가용성을 보장합니다. 하지만 이 과정에서 분할된 노드들 간에 데이터 불일치가 발생할 수 있으며, 일관성이 깨질 수 있습니다. 시스템이 정상화되면 나중에 데이터가 동기화됩니다.
    • 비유: 여러 대의 서버가 운영되는 온라인 쇼핑몰이라고 생각해봅시다. 한 서버에 네트워크 문제가 생겨 다른 서버들과 통신이 안 됩니다. 하지만 쇼핑몰은 고객이 언제든 상품을 보고 주문할 수 있도록, 해당 서버가 가진 최신 정보를 바탕으로 계속 서비스를 제공합니다. 이 경우, 다른 서버에 업데이트된 재고 정보가 반영되지 않아 일시적으로 품절된 상품이 판매될 수도 있습니다. (가용성 유지, 일관성 저하)

다양한 일관성 모델

CAP 이론의 '일관성(C)'은 보통 '강한 일관성(Strong Consistency)'을 의미합니다. 하지만 실제 분산 시스템에서는 성능과 가용성을 위해 다양한 수준의 일관성 모델을 사용합니다.

  1. 강한 일관성 (Strong Consistency):

    • 선형화 가능성 (Linearizability): 가장 엄격한 일관성 모델입니다. 모든 연산이 특정 시점에 단일 순서로 실행된 것처럼 보이는 것을 보장합니다. 즉, 모든 클라이언트가 모든 노드에서 항상 동일하고 최신 데이터를 볼 수 있습니다. 분산 락, 분산 트랜잭션, Zookeeper 같은 코디네이션 서비스에서 중요합니다.
    • 순차적 일관성 (Sequential Consistency): 선형화 가능성보다 약간 약하지만, 여전히 강한 일관성에 속합니다. 한 프로세스 내의 연산 순서는 유지되지만, 여러 프로세스 간의 연산 순서는 다를 수 있습니다.
  2. 결과적 일관성 (Eventual Consistency):

    • 대부분의 분산 데이터베이스(Cassandra, DynamoDB 등)가 채택하는 모델입니다. 쓰기 작업이 발생한 후, 충분한 시간이 지나고 더 이상의 쓰기 작업이 없다면, 모든 노드가 최종적으로 동일한 데이터를 가지게 됨을 보장합니다. 일시적인 불일치는 허용하지만, 언젠가는 일관된 상태에 도달합니다.
    • 비유: 소문을 퍼뜨리는 것과 비슷합니다. 소문은 처음에는 특정 사람들에게만 전달되지만, 시간이 지나면 결국 모든 사람에게 퍼지고 모두가 같은 소문을 듣게 됩니다.
    • 결과적 일관성 시스템은 일반적으로 가용성과 확장성이 뛰어납니다.
  3. 약한 일관성 (Weak Consistency) 및 중간 일관성 모델:

    • 읽기 후 쓰기 일관성 (Read-Your-Writes Consistency): 사용자가 자신의 쓰기 작업 결과를 즉시 읽을 수 있음을 보장합니다. (예: 게시글을 올린 후 바로 내 게시글을 볼 수 있음)
    • 단조 읽기 (Monotonic Reads): 한번 읽은 데이터보다 더 오래된 데이터를 다시 읽지 않음을 보장합니다. (시간 역행 방지)
    • 단조 쓰기 (Monotonic Writes): 같은 클라이언트의 쓰기 요청이 시스템에 도달하는 순서가 보존됨을 보장합니다.
    • 인과적 일관성 (Causal Consistency): 인과 관계가 있는 쓰기(예: 댓글에 대한 답글)는 순서가 보존되지만, 인과 관계가 없는 쓰기는 순서가 바뀔 수 있습니다.

3. 코드 예제 2개 (Python)

CAP 이론의 개념을 직접적으로 코드로 완벽히 구현하는 것은 분산 시스템 프레임워크 수준의 작업이므로, 여기서는 CAP 이론의 각 속성이 애플리케이션에 미치는 영향을 아주 단순화된 형태로 시뮬레이션해 보겠습니다.

예제 1: CP 시스템 (일관성 유지, 가용성 저하) 시뮬레이션

이 예제는 분산 환경에서 "강한 일관성"을 유지하기 위해, 일부 노드가 통신 불가능할 때 쓰기 작업을 거부하는 시나리오를 보여줍니다.

import time
import random

class DistributedCounterCP:
    def __init__(self, node_ids, quorum_size):
        self.nodes = {node_id: 0 for node_id in node_ids}
        self.quorum_size = quorum_size
        self.active_nodes = set(node_ids) # 현재 활성화된 노드

    def simulate_partition(self, down_nodes):
        """특정 노드들을 비활성화하여 네트워크 분할을 시뮬레이션합니다."""
        for node_id in down_nodes:
            if node_id in self.active_nodes:
                self.active_nodes.remove(node_id)
                print(f"--- 노드 {node_id} 비활성화 (네트워크 분할 발생) ---")

    def simulate_recovery(self, recovered_nodes):
        """비활성화된 노드들을 복구합니다."""
        for node_id in recovered_nodes:
            if node_id not in self.active_nodes:
                self.active_nodes.add(node_id)
                print(f"+++ 노드 {node_id} 복구 (네트워크 분할 해소) +++")

    def increment(self, amount=1):
        """
        카운터를 증가시킵니다.
        정상적인 쓰기 작업을 위해 쿼럼 이상의 노드에 동기화되어야 합니다.
        """
        if len(self.active_nodes) < self.quorum_size:
            print(f"!!! 쓰기 실패: 활성 노드가 쿼럼({self.quorum_size})보다 적습니다. (가용성 저하)")
            return False

        # 쿼럼을 만족하는 활성 노드들에 쓰기 시도 (여기서는 모든 활성 노드에 쓰기 시도)
        successful_updates = 0
        for node_id in self.active_nodes:
            # 실제 분산 시스템에서는 복잡한 합의 프로토콜이 필요합니다.
            # 여기서는 단순히 업데이트를 시뮬레이션합니다.
            self.nodes[node_id] += amount
            successful_updates += 1
            print(f"  노드 {node_id}에 {amount} 증가: 현재 값 {self.nodes[node_id]}")

        print(f"✅ 카운터 {amount} 증가 완료. 현재 모든 노드 값: {self.get_current_values()}")
        return True

    def get_current_values(self):
        """모든 노드의 현재 값을 반환합니다. 일관성을 확인하기 위함."""
        return {node_id: self.nodes[node_id] for node_id in self.nodes}

# 시스템 설정
node_ids = ['node-1', 'node-2', 'node-3', 'node-4', 'node-5']
# 쿼럼: 전체 노드의 절반 이상. (N/2 + 1)이 일반적
quorum_size = (len(node_ids) // 2) + 1
print(f"총 {len(node_ids)}개 노드, 쿼럼 크기: {quorum_size}")

cp_system = DistributedCounterCP(node_ids, quorum_size)

print("\n--- 1. 정상 작동 (일관성 유지, 가용성 유지) ---")
cp_system.increment()
cp_system.increment(2)

print("\n--- 2. 네트워크 분할 시뮬레이션 (가용성 저하) ---")
# 2개 노드 다운 -> 3개 활성 -> 쿼럼(3) 만족 -> 아직 쓰기 가능
cp_system.simulate_partition(['node-1', 'node-2'])
cp_system.increment() # 쿼럼 만족, 쓰기 성공

# 3개 노드 다운 -> 2개 활성 -> 쿼럼(3) 미달 -> 쓰기 실패 (가용성 저하)
cp_system.simulate_partition(['node-3'])
cp_system.increment() # 쿼럼 미달, 쓰기 실패

print("\n--- 3. 네트워크 복구 후 ---")
cp_system.simulate_recovery(['node-1', 'node-2', 'node-3'])
cp_system.increment(5) # 쿼럼 만족, 쓰기 성공

print("\n최종 노드 값:", cp_system.get_current_values())

예제 2: AP 시스템 (가용성 유지, 일관성 저하) 시뮬레이션

이 예제는 "결과적 일관성"을 가진 AP 시스템에서 어떻게 가용성을 유지하면서 일시적인 데이터 불일치가 발생할 수 있는지 보여줍니다. 각 노드는 독립적으로 쓰기 작업을 처리하고, 나중에 동기화됩니다.

import time
import random

class KeyValueStoreAP:
    def __init__(self, node_ids):
        # 각 노드는 자체적인 키-값 저장소를 가집니다.
        self.nodes = {node_id: {} for node_id in node_ids}
        self.active_nodes = set(node_ids) # 현재 활성화된 노드

    def simulate_partition(self, down_nodes):
        """특정 노드들을 비활성화하여 네트워크 분할을 시뮬레이션합니다."""
        for node_id in down_nodes:
            if node_id in self.active_nodes:
                self.active_nodes.remove(node_id)
                print(f"--- 노드 {node_id} 비활성화 (네트워크 분할 발생) ---")

    def simulate_recovery(self, recovered_nodes):
        """비활성화된 노드들을 복구하고, 동기화를 시뮬레이션합니다."""
        for node_id in recovered_nodes:
            if node_id not in self.active_nodes:
                self.active_nodes.add(node_id)
                print(f"+++ 노드 {node_id} 복구 (네트워크 분할 해소) +++")
        self._synchronize_nodes() # 복구 시 동기화 시도

    def put(self, key, value):
        """
        키-값을 저장합니다. 모든 활성 노드에 쓰기를 시도하지만,
        각 노드는 독립적으로 쓰기를 성공시킵니다 (가용성 유지).
        """
        for node_id in self.active_nodes:
            self.nodes[node_id][key] = value
            print(f"  노드 {node_id}에 '{key}': '{value}' 쓰기 성공")
        print(f"✅ 키 '{key}'에 '{value}' 쓰기 요청 완료.")

    def get(self, key):
        """
        키에 해당하는 값을 읽습니다.
        가용성을 위해 활성 노드 중 하나에서 값을 가져옵니다.
        이 경우 일관성이 보장되지 않을 수 있습니다.
        """
        if not self.active_nodes:
            print("!!! 읽기 실패: 활성 노드가 없습니다. (시스템 전체 가용성 문제)")
            return None

        # 임의의 활성 노드에서 읽기 시도
        read_node_id = random.choice(list(self.active_nodes))
        value = self.nodes[read_node_id].get(key, "N/A")
        print(f"  노드 {read_node_id}에서 '{key}' 읽기: '{value}'")
        return value

    def _synchronize_nodes(self):
        """
        네트워크 복구 후 노드들 간의 데이터를 동기화합니다.
        여기서는 '최종 쓰기 승리 (Last Write Wins)' 전략을 사용합니다.
        실제 시스템에서는 더 복잡한 충돌 해결이 필요합니다.
        """
        print("\n--- 노드 동기화 시작 (충돌 해결: 최종 쓰기 승리) ---")
        all_keys = set()
        for node_data in self.nodes.values():
            all_keys.update(node_data.keys())

        for key in all_keys:
            # 모든 노드의 해당 키에 대한 값을 수집
            values_on_nodes = {node_id: self.nodes[node_id].get(key)
                               for node_id in self.nodes}

            # 값이 다른 노드가 있는지 확인
            unique_values = set(values_on_nodes.values())
            if len(unique_values) > 1 and None in unique_values: # None은 실제 값이 아니므로 제외
                unique_values.remove(None)
            
            if len(unique_values) > 1:
                # 충돌 발생: 가장 최근에 쓰인 값으로 통합 (여기서는 간단히 임의 선택)
                # 실제로는 타임스탬프 등을 비교하여 '최종 쓰기 승리'를 결정합니다.
                consistent_value = list(unique_values)[0] # 간단화를 위해 임의 선택
                print(f"  키 '{key}' 충돌 감지: {values_on_nodes} -> '{consistent_value}'로 통합")
                for node_id in self.nodes:
                    self.nodes[node_id][key] = consistent_value
            elif len(unique_values) == 1:
                print(f"  키 '{key}' 일관성 유지: '{list(unique_values)[0]}'")
            else:
                print(f"  키 '{key}' 존재하지 않음 또는 단일 값")

        print("--- 노드 동기화 완료 ---")

# 시스템 설정
node_ids = ['replica-a', 'replica-b', 'replica-c']
ap_system = KeyValueStoreAP(node_ids)

print("\n--- 1. 정상 작동 (가용성 유지, 일관성 유지) ---")
ap_system.put("product_stock", "100")
ap_system.get("product_stock")

print("\n--- 2. 네트워크 분할 시뮬레이션 (일관성 저하) ---")
# 'replica-c' 노드만 다른 노드와 통신 불가
ap_system.simulate_partition(['replica-c'])
time.sleep(0.1) # 시뮬레이션 지연

# 분할된 노드들이 독립적으로 쓰기 작업을 처리
ap_system.put("user_status", "online") # replica-a, b에만 반영
ap_system.nodes['replica-c']["user_status"] = "offline (partitioned)" # replica-c는 자기 혼자 업데이트

ap_system.get("user_status") # replica-a,b 중 하나에서 'online' 읽을 가능성
time.sleep(0.1)
ap_system.get("user_status") # 여전히 'online' (replica-c의 'offline'은 읽히지 않음)

print("\n--- 3. 네트워크 복구 후 ---")
ap_system.simulate_recovery(['replica-c']) # 복구 시 동기화 발생
ap_system.get("user_status") # 이제 모든 노드에서 'online' 또는 'offline (partitioned)' 중 하나로 통합됨 (여기서는 'online')
ap_system.get("product_stock")

print("\n최종 노드 상태:")
for node_id, data in ap_system.nodes.items():
    print(f"  {node_id}: {data}")

4. 실무 적용 사례

CAP 이론과 일관성 모델은 분산 시스템을 설계하고 운영하는 거의 모든 곳에서 마주하는 문제입니다.

  • 데이터베이스 선택:
    • CP (강한 일관성): 관계형 데이터베이스(RDBMS, 예: PostgreSQL, MySQL)의 분산 트랜잭션, Zookeeper, etcd와 같은 분산 코디네이션 시스템은 강한 일관성을 중요하게 여깁니다. 금융 거래, 재고 관리, 사용자 인증 등 데이터의 정확성이 최우선인 경우에 적합합니다.
    • AP (결과적 일관성): NoSQL 데이터베이스(예: Cassandra, DynamoDB, CouchDB)는 높은 가용성과 확장성을 위해 결과적 일관성을 선택하는 경우가 많습니다. 소셜 미디어 피드, 실시간 추천 시스템, IoT 데이터 수집, 대규모 사용자 프로필 저장 등 데이터의 일시적인 불일치가 허용되는 시나리오에 적합합니다.
  • 클라우드 서비스: AWS DynamoDB는 기본적으로 결과적 일관성 읽기(Eventual Consistent Reads)를 제공하지만, 필요에 따라 강한 일관성 읽기(Strongly Consistent Reads)를 선택할 수 있도록 옵션을 제공합니다. 이는 개발자가 애플리케이션의 요구사항에 따라 일관성 수준을 조절할 수 있도록 하는 좋은 예시입니다.
  • 마이크로서비스 아키텍처: 여러 마이크로서비스가 데이터를 공유할 때, 각 서비스가 어떤 일관성 수준을