CAP 이론
CAP 이론은 분산 데이터 저장소가 일관성(Consistency), 가용성(Availability), 분할 내성(Partition tolerance)을 모두 동시에 보장할 수는 없다는 이론입니다. 네트워크 분할이 일어나는 동안에는 일관성과 가용성 가운데 하나를 골라야 한다는 뜻으로 많이 설명합니다.
에릭 브루어(Eric Brewer)가 2000년 PODC 기조 강연에서 발표한 추측을 세스 길버트(Seth Gilbert)와 낸시 린치(Nancy Lynch)가 2002년에 증명했습니다. 이 토픽에서는 두 노드 사이의 네트워크가 끊겼을 때 무슨 일이 일어나는지, CP와 AP가 각각 무엇을 포기하는지를 시각화로 확인합니다.
핵심 개념
일관성 (C)
쓰기가 끝난 뒤 시작한 읽기가 그 쓰기의 값(또는 이후 쓰기의 값)을 돌려주는 성질입니다. 길버트와 린치의 증명에서는 단일 복사본처럼 보이는 원자적(선형화 가능) 일관성을 뜻합니다. ACID의 일관성과는 다른 뜻입니다.
핵심 개념
가용성 (A)
장애가 나지 않은 노드가 받은 모든 요청에 응답하는 성질입니다. 형식 정의에는 응답 시간 제한이 없지만 실무에서는 타임아웃을 넘기면 가용성을 잃은 것으로 봅니다. 응답이 최신값이라는 보장은 포함하지 않습니다.
핵심 개념
분할 내성 (P)
노드 사이 메시지가 유실되거나 지연되어 네트워크가 갈라져도 시스템이 계속 동작하는 성질입니다.
핵심 개념
CP 시스템
분할 중에 일관성을 지키려고 일부 요청을 거절하거나 지연시킵니다. 응답할 때는 최신값만 돌려줍니다.
핵심 개념
AP 시스템
분할 중에도 모든 요청에 응답합니다. 오래된 값을 줄 수 있고, 복구 뒤에 갈라진 값을 합쳐야 합니다.
핵심 개념
최종적 일관성
더 이상 쓰기가 없으면 시간이 지나 모든 복제본이 같은 값으로 수렴한다는 성질입니다. AP 시스템이 보통 제공하는 보장입니다.
CAP 이론의 배경
브루어는 2000년 분산 컴퓨팅 학회(PODC)의 기조 강연에서 웹 서비스처럼 확장되는 분산 시스템이 일관성, 가용성, 분할 내성 가운데 두 가지만 가질 수 있다는 추측을 내놓았습니다. 브루어 본인의 설명으로는 이 생각이 1998년 가을에 처음 나왔습니다. 길버트와 린치는 2002년 논문에서 이 추측을 비동기 네트워크 모델에서 형식적으로 증명해 이론이 되었습니다.
이후 NoSQL 데이터베이스를 설명하는 틀로 널리 쓰였지만 정의가 엄밀하지 않게 쓰이는 일이 많았습니다. 브루어 본인도 2012년 글에서 규칙이 어떻게 달라졌는지 다시 설명하며, 분할 중에도 일관성과 가용성을 상황에 따라 세밀하게 조절할 수 있다고 말했습니다.
숫자로 확인하기
자주 하는 오해
면접에서 이렇게 물어요
면접 질문
CAP 이론의 세 요소는 무엇인가요?
일관성(C), 가용성(A), 분할 내성(P)입니다. 분산 저장소는 이 셋을 모두 동시에 보장할 수 없습니다.
면접 질문
네트워크 분할이 일어나면 어떤 선택을 하나요?
일관성을 지킬지(CP) 가용성을 지킬지(AP) 고릅니다. CP는 일부 요청을 거절하거나 지연시키고, AP는 오래된 값으로 응답할 수 있습니다.
면접 질문
CP와 AP는 각각 어떤 데이터에 맞나요?
잔액과 재고처럼 틀린 값의 피해가 큰 데이터는 일관성을 우선(CP)하는 경우가 많고, 좋아요 수나 장바구니처럼 잠시 틀려도 되고 항상 응답하는 것이 중요한 데이터는 가용성을 우선(AP)하는 경우가 많습니다. 정해진 답은 아니고 서비스 요구에 따라 다릅니다.
면접 질문
최종적 일관성이란 무엇인가요?
새 쓰기가 없다면 시간이 지나 모든 복제본이 같은 값으로 수렴한다는 보장입니다. 그 사이에는 복제본마다 다른 값을 읽을 수 있습니다.
활용 분야
함께 보면 좋은 토픽
참고 자료
SQL — PostgreSQL 동기 복제와 커밋 대기
-- 동기 복제: 대기 서버의 확인을 받아야 커밋이 끝난다 (내구성 쪽) ALTER SYSTEM SET synchronous_standby_names = 'FIRST 1 (standby1, standby2)'; ALTER SYSTEM SET synchronous_commit = 'on'; SELECT pg_reload_conf(); SELECT application_name, state, sync_state FROM pg_stat_replication; -- sync_state: sync, potential, async, quorum -- 이 트랜잭션만 대기 서버를 기다리지 않는다 (응답 속도 쪽) BEGIN; SET LOCAL synchronous_commit = 'local'; UPDATE account SET balance = balance - 100 WHERE id = 1; COMMIT;
PostgreSQL에서 synchronous_standby_names를 비워 두지 않고 synchronous_commit이 on이면, 커밋은 동기 대기 서버가 커밋 기록을 받아 디스크에 flush했다는 확인을 받을 때까지 기다립니다. 필요한 수만큼 동기 대기 서버를 채울 수 없으면 커밋이 계속 기다리게 되므로, 데이터를 잃을 가능성을 크게 줄이는 대신 쓰기 요청이 멈출 수 있습니다. 이는 CAP이 말하는 분할 상황과는 다릅니다. 분할이 없어도 대기 서버를 기다릴지에 따라 내구성과 응답 속도가 갈리는 평상시의 균형에 가깝고, 대기 서버에 닿지 못해 쓰기가 멈추는 경우는 분할 중에 일관성을 우선하는 모습과 닮았습니다. SET LOCAL synchronous_commit = 'local'로 바꾸면 그 트랜잭션은 대기 서버를 기다리지 않고 로컬 디스크에 기록된 뒤 끝납니다. 응답은 빨라지고 대기 서버가 죽거나 끊겨도 쓰기가 계속되지만, 장애가 나면 대기 서버에 아직 없는 변경이 생길 수 있습니다. 서버 이름과 설정값은 환경마다 다릅니다.
Python — 분할 중 CP와 AP의 읽기 차이
class Node: def __init__(self): self.x = 1 class Cluster: def __init__(self, mode): self.a, self.b, self.link, self.mode = Node(), Node(), True, mode def write_a(self, v): self.a.x = v if self.link: self.b.x = v # 연결이 있으면 B로 복제 def read_b(self): if self.link or self.mode == 'AP': return self.b.x # AP는 가진 값으로 응답 raise TimeoutError('최신값 확인 불가') # CP는 응답을 거절 for mode in ('CP', 'AP'): c = Cluster(mode) c.write_a(2) c.link = False # 네트워크 분할 c.write_a(3) try: print(mode, '분할 중 읽기:', c.read_b()) except TimeoutError as e: print(mode, '분할 중 읽기:', e) c.link = True # 복구 후 B가 따라잡음 c.b.x = c.a.x print(mode, '복구 후 읽기:', c.read_b()) writes = [(1, 'A', 3), (2, 'B', 5)] # (시각, 노드, 값) print('LWW 결과:', max(writes)[2]) # CP 분할 중 읽기: 최신값 확인 불가 / CP 복구 후 읽기: 3 # AP 분할 중 읽기: 2 / AP 복구 후 읽기: 3 / LWW 결과: 5
같은 상황에서 두 방식의 읽기를 비교하는 장난감 모델입니다. 이 예시에서 A는 쓰기를 받는 쪽(리더에 해당)이라고 가정하며, 실제 CP 시스템은 복제를 확인하지 못하는 쪽의 쓰기도 거절하는 경우가 많습니다. A에 x = 2를 쓰고 연결을 끊은 뒤 x = 3을 쓰면 B는 2에 머뭅니다. 이때 CP 모드의 B는 최신값을 확인할 수 없다며 읽기를 거절하고, AP 모드의 B는 오래된 값 2를 돌려줍니다. 복구 후에는 B가 3으로 따라잡아 두 모드 모두 3을 읽습니다. 시각화의 CP 선택, AP 선택 탭과 같은 결과입니다. 마지막 줄은 분할 중 양쪽에서 쓴 값을 시각으로 비교하는 마지막 쓰기 우선(LWW) 규칙으로, 시각 2의 x = 5가 남고 x = 3 쓰기는 사라집니다. 실제 시스템은 노드 사이 시계가 어긋날 수 있어 시각만으로 순서를 정하기가 훨씬 까다롭습니다.
Java — 쿼럼 크기와 읽기 가능 여부
static boolean overlaps(int n, int w, int r) { return w + r > n; } // 쓰기와 읽기 쿼럼이 겹치는가 static boolean canServe(int reachable, int need) { return reachable >= need; } int n = 3; for (int[] q : new int[][]{{2, 2}, {1, 1}, {3, 1}}) { int w = q[0], r = q[1]; System.out.println("W=" + w + " R=" + r + " overlap=" + overlaps(n, w, r) + " 소수쪽(1대) 읽기=" + canServe(1, r) + " 다수쪽(2대) 읽기=" + canServe(2, r)); } // W=2 R=2 overlap=true 소수쪽(1대) 읽기=false 다수쪽(2대) 읽기=true // W=1 R=1 overlap=false 소수쪽(1대) 읽기=true 다수쪽(2대) 읽기=true // W=3 R=1 overlap=true 소수쪽(1대) 읽기=true 다수쪽(2대) 읽기=true
복제본이 N개일 때 쓰기에 W개, 읽기에 R개의 응답을 받는 쿼럼 방식에서 W + R이 N보다 크면 읽기 쿼럼과 쓰기 쿼럼이 반드시 겹쳐 읽기가 최근 쓰기를 볼 수 있습니다(겹침 조건일 뿐 선형화를 보장하는 것은 아닙니다). N = 3에서 W = 2, R = 2는 이 조건을 만족하고, W = 1, R = 1은 만족하지 못합니다. 이 조건은 시각화의 CP와 AP 사이의 선택을 숫자로 보여 주는 한 가지 방법입니다. 복제본 3대가 2대와 1대로 갈라졌을 때, R = 2이면 1대쪽은 읽기 쿼럼을 채우지 못해 읽기를 거절하고(CP 성향), R = 1이면 어느 쪽이든 읽지만 오래된 값을 읽을 수 있습니다(AP 성향). W = 3, R = 1은 읽기가 항상 가능하지만 쓰기는 3대가 모두 응답해야 하므로 분할 중에는 쓰기를 받지 못합니다. 개념 설명용 계산이며 실제 시스템은 쿼럼 외에도 여러 장치를 함께 씁니다.
Q1
CAP 정리의 세 가지 속성은?
Q2
네트워크 분단(Partition)이 실제로 발생했을 때 분산 시스템이 택해야 하는 것은?
Q3
CP 시스템의 특성으로 알맞은 것은?