계좌 이체 문제
은행에서 돈을 다른 계좌로 이체할 때, 안전하게 이체하는 것은 어려운 일이다.
단순하게 생각하면 내 통장에서 돈을 2000원 감소시키고, 상대방 통장의 돈을 2000원 증가시키면 될 것 같다.
하지만 나와 상대방이 둘 다 같은 액수에 대해 동시에 돈을 감소시키고 증가시켜야 한다는 점을 생각하면 쉽지 않다는 것을 알 수 있다.
온라인으로 돈을 보내는 것은 비동기적인 통신이다.
나는 돈을 보냈다고 생각해서 2000원을 감소시켰는데, 상대방은 돈을 받았다는 신호를 못 받아서 돈을 증가시키지 않았다면 나의 잔액만 2000원이 줄어들 뿐이다.
돈의 액수에 대한 원만한 '합의' 를 위해서 사이에 중간자를 두는 방법을 생각할 수 있다.
우리나라로 치면 금융결제원 같은 중간자를 두는 것이다.
안전성과 라이브니스
분산 시스템은 '안정성 (safety)' 과 '라이브니스 (liveness)' 가 반드시 필요하다.
안정성 = 기능적으로 원래 동작해야 할 동작이 올바르게 동작하는 것을 말한다.
라이브니스 = 시도한 일이 결국에 반드시 일어나는 것을 말한다.
예를 들어 계좌이체 시스템이라면
안정성 관점에서 내가 돈을 보내서 내 계좌에서 돈이 빠졌는데, 상대방 계좌에는 돈이 안들어가는 '잘못된 일이 발생하면' 안된다.
라이브니스 관점에서 내가 돈을 보냈는데, 내 계좌에서 돈이 빠지지도 않고, 상대방 계좌에 돈이 늘어나지도 않으면 안된다. 즉, 아무일도 발생하지 않으면 안된다.
2-단계 커밋 (2-Phase Commit)
지난 글에서 정리한 '두 장군 이야기' 에서 두 장군이 직접 소통하려고 하는 상황에서는 절대 합의에 이룰 수 없기에 '간주' 하는 정도로 타협을 봐야 했음을 알 수 있었다.
따라서 분산 시스템에는 '합의' 를 이루기 위해서 두 개체 사이에 중간자를 두는 방법을 사용하는데, 이것이 분산 시스템의 제일 기본적인 프로토콜인 2단계 커밋이다.
2단계 커밋 프로토콜에서는 중간자로 트랜잭션 코디네이터 (TC, Transaction Coordinator) 라는 개념을 제시한다.
트랜잭션 코디네이터는 서로 합의를 보려는 두 개체 A, B 로부터 트랜잭션 요청을 받는다.
요청은 '트랜잭션' 이기 때문에 원자성을 갖는다. (즉, 트랜잭션 요청사항은 일어나지 않거나, 요청대로 온전히 일어난다.)
스트로우맨 프로토콜
이제 계좌 이체 문제를 한번 해결해보자.
이 문제를 해결하는 제일 간단한 프로토콜로는 스트로우맨(허수아비) 프로토콜이 있다.
돈을 송금하려는 A 는 TC에게 송금을 요청한다.
TC 는 K 은행에서 1000원을 빼고, S 은행에서 1000원을 입금한다.
이렇게 보면 아무런 문제가 없어보인다.
하지만 이렇게 문제를 해결하려면 TC, K, S 은행이 모두 살아이었어야 한다는 전제가 필요하다.
실제로는 수시로 서비스에 장애가 발생하여 요청을 처리하지 못할 수도 있다.
따라서 만약 K 는 장애가 발생하고, S 만 정상적인 상황이었다면, 출금은 되지 않고 S 의 잔고만 늘어나는 상황도 발생할 수 있다.
이러면 'K은행에서 S은행으로 1000원을 송금한다' 라는 트랜잭션 요청은 온전히 실행된 것이 아니므로 원자성이 깨지게 된다.
원자적 커밋 프로토콜 (Atomic Commit Protocol)
이 문제를 해결한 방법이 바로 원자적 커밋 프로토콜이다.
위 스트로우맨 프로토콜의 문제의 원인은 '시스템의 장애가 발생했는지 알 수 없다' 는 데에서 발생했다.
그러면 해결책은 간단하다. 시스템에 장애가 발생했는지 확인하고 나서 요청을 처리하면 된다.
A 로부터 송금 요청을 받은 TC 는 K, S 은행이 살아있는지 물어본다.
만약 두 컴퓨터가 모두 살아있다고 응답을 보냈다면, TC는 위에서 본 것처럼 실제 이체 작업을 실행시킨다.
만약 한 컴퓨터라도 살아있다는 응답을 보내지 않았다면, TC는 이 트랜잭션 요청을 처리하지 않는다.
하지만 이렇게만 하면 여전히 문제가 남아있다.
분산 시스템에서는 안정성과 라이브니스를 보장하는 것이 중요하다.
만약 은행 K, S 가 모두 '예' 를 보낸 상태에서 TC 에 장애가 생겨서 다운되면 어떻게 될까?
이럴 때는 트랜잭션을 커밋하기 전에 미리 로그를 써둠으로써 해결할 수 있다. (Write Ahead Log, WAL)
TC 는 두 은행으로부터 모두 '예' 를 받은 뒤, 로그를 작성하여 '송금 작업을 처리해야 한다' 는 것을 기록한다.
만약 이 상태에서 TC에 장애가 생겨 다운되더라도, 다시 복구된 후에는 로그를 보고 송금 작업을 이어서 처리할 수 있다.
만약 K 에서는 출금을 했는데, S 에 입금을 하기 전에 다운되었다면 어떨까?
이때도 마찬가지로 '출금하기 전에 로그 작성', '입금 하기 전에 로그 작성' 을 통해서 내가 하려는 작업을 수행하기 전에 로그를 작성해둠으로써 해결한다. 만약 출금을 하고나서 다운이 되었다면, 로그를 보고 출금까지는 했으니 입금만 이어서 진행하면 된다는 것을 알 수 있다.
그리고 이로서 '잘못된 결과가 발생하는 것' 을 막아, 안정성을 지킬 수 있다.
그렇다면 라이브니스는 어떻게 보장할 수 있을까?
간단하다. 만약 두 은행으로부터 모두 응답을 받은 뒤라면, 은행에는 커밋한 내용을 수행해야 한다는 로그가 남아있게 된다.
따라서 은행이 다운되었더라도 나중에 복구되었을 때 로그를 확인하고 '결국에는' 원하는 결과대로 계좌 응답을 처리하게 된다.
즉, 장애가 일어난 장비가 '복구만 된다면' 결국에는 원하는 결과가 실행된다.
이제 이 상황에서 발생할 수 있는 다양한 예외 생황들을 생각해보자.
1. K 은행은 준비가 되었으나, S 은행은 TC로부터 준비 메세지를 받았으나 장애가 발생하여 응답을 보내지 못한 상황
TC 는 S 은행으로부터 일정 시간동안 응답을 받지 못하면, 준비를 취소하라는 요청을 K 은행에게 보낸다.
K 은행은 준비를 취소하고 ACK 를 보내며, 나중에 S 은행이 복구되어 뒤늦게 확인 응답을 보내더라도 TC 는 이미 취소 처리를 햇으므로 다시 S 은행에게 취소하라는 요청을 보내어 아무 일도 일어나지 않도록 만든다.
2. K, S 모두 준비가 되었으나, 이후에 S 은행이 다운되어 커밋을 처리하지 못하는 상황
준비가 되었다는 응답을 받은 TC 는 일단 수행해야 할 작업을 K, S 로 전송한다.
만약 S가 중단되어 이를 받지 못하더라도, 일단 TC 로부터 준비가 되었다는 대답은 한 상태이므로, 나중에 복구되어 작업 요청사항을 받게 되면 이를 그대로 수행만 하면 된다.
3. K, S 모두 준비가 되었으나, 네트워크 에러로 S은행의 준비가 되었다는 응답 패킷이 유실된 상황
응답을 보낸 시점에서 S은행은 로그로 준비가 된 상태임을 기록해 둔 상태이다.
하지만 TC 입장에서는 S은행이 준비가 되었다는 응답을 받지 못했으므로, 준비를 취소하라는 요청을 두 은행에 보낸다.
따라서 S 은행은 준비를 취소하고 아무일도 일어나지 않는다.
이처럼 분산 시스템에서는 장애가 언제나 일어날 수 있음을 고려해야 한다.
따라서 어떻게 해야 장애 내성(Fault-Tolerance)을 가지고, 고가용성 시스템을 구현할 수 있을지 고민해야 한다.
'CS > 분산시스템특론' 카테고리의 다른 글
[분산시스템특론] 5. CAP 이론 & FLP 정리 (0) | 2025.10.22 |
---|---|
[분산시스템특론] 4. 시간 동기화 문제와 램포트 시계 (0) | 2025.10.21 |
[분산시스템특론] 2. 두 장군 이야기 (3) | 2025.10.20 |
[분산시스템특론] 1. AI Agents vs. Agentic AI (0) | 2025.10.20 |