본문 바로가기

사이드 프로젝트/지갑 서비스

전자 지갑 시스템 - 1

안녕하세요.

이번 글에서는 전자 지갑 시스템를 구현하기 위한 내용을 학습한 것에 대해서 정리하도록 하겠습니다.

 

참고 했던 블로그 글은 아래와 같습니다.

https://jm-baek.tistory.com/339 

 

12장. 전자 지갑

결제 플랫폼은 일반적으로 고객에게 전자 지갑 서비스를 제공하여 고객으로 하여금 지갑에 돈을 넣어 두고 필요할 때 사용할 수 있도록 한다. 예를 들어, 은행 카드에서 전자 지갑에 돈을 이체

jm-baek.tistory.com

 

전자 지갑 시스템을 구현하기 위한 핵심 요구사항은 아래와 같다고 파악했습니다.

  1. 데이터베이스 선택 : 전자 지갑 시스템은 대규모 트래픽의 잔액 이체 서비스를 수용하기 위해 적절한 분산 DB를 채택해야 한다.
  2. 트랜잭션 처리 방안 : 이체 서비스란 서로 다른 사용자가 잔고를 주고 받는 행위를 의미한다. 잔고 차감, 증가 2가지가 이벤트가 발생한다. 분산 DB 환경에서 이체 서비스(잔고 차감 및 증가 이벤트)를 단일 DB 트랜잭션처럼 처리할 수 없다. 그러므로, 데이터 정합성을 지키며 이를 처리할 수 있는 방법이 필요하다.
  3. 로그 수집 : 외부 감사 등에 대응하기 위해 전자 지갑 이체 서비스의 모든 이력을 기록하여 저장해야 한다.

 

위 요구사항을 충족하기 위해 고려한 내용은 아래와 같습니다.

 

1. 데이터베이스 선택

인메모리 DB 보다는 전통적인 RDB를 선택했습니다. 대규모 트래픽을 처리하기 위해서 분산형 DB를 구성하고, RDB 또는 인메모리 DB를 선택할 수 있습니다. 비록 분산 데이터베이스 환경에서는 이체 서비스에서 발생하는 단위 서비스(잔고 차감, 증가)를 전역 트랜잭션으로 관리할 수 없지만, 이를 로컬 트랜잭션으로 처리할 수 있도록 RDB를 선택하는 것이 ACID를 이용할 수 있으므로 바람직하다고 판단했습니다.

 

2. 트랜잭션 처리 방안

분산 시스템에서의 트랜잭션 처리 방안으로 2PC, TCC(Try-Confirm-Cancel), Saga 패턴 3가지를 고려했습니다.

2PC는 분산 DB 환경에서의 전통적인 트랜잭션 처리 방법이지만, 특정 데이터베이스 기술에 대한 종속된다는 점과 코디네이터의 한계를 고려했을 때 해당 기술 보다는 TCC와 Saga 패턴을 중점적으로 비교했습니다. 

 

사용자 A의 잔고는 DB1, 그리고 사용자 B의 잔고는 DB2에 저장되어 있고, 사용자 A가 B에게 100을 이체하는 상황를 가정하겠습니다.

 

2-1. TCC(Try-Confirm-Cancel)

위의 상황을 TCC 기법을 적용하여 구현하면 아래와 같습니다.

 

Step 1. Try : 리소스 선점

INSERT
INTO DB1.예약테이블 (ID, 사용자, 금액, 상태)
VALUE (1234, A, -100, PENDING)

UPDATE DB1.잔고테이블
SET 금액 = 금액 - 100
WHERE 사용자 = A
INSERT
INTO DB2.예약테이블 (ID, 사용자, 금액, 상태)
VALUE (1234, B, +100, PENDING)

 

Step 2. Confirm : 확정

UPDATE DB1.예약테이블
SET 상태 = SUCCESS
WHERE ID = 1234
UPDATE DB2.잔고테이블
SET 잔고 = 잔고 + 100
WHERE 사용자 = B

UPDATE DB2.예약테이블
SET 상태 = SUCCESS
WHERE ID = 1234

 

 

동일한 DB에 수행되는 UPDATE 및 INSERT 쿼리는 하나의 로컬 트랜잭션으로 묶어서 처리합니다. 또한, 모든 단계에서의 로컬 트랜잭션은 서로 병렬로 실행될 수 있습니다.

각 단계에서의 모든 로컬 트랜잭션이 완료되면 다음 단계로 진행합니다. 만약 Try 및 Confirm 각 단계에서 하나의 로컬 트랜잭션이라도 실패하면, Cancel 작업을 실행합니다.

 

Try 단계에서 하나의 로컬 트랜잭션이라도 장애가 발생한다면, 아래의 작업을 실행합니다.

UPDATE DB1.잔고테이블
SET 잔고 = 잔고 + 100
WHERE 사용자 = A

UPDATE DB1.예약테이블
SET 상태 = CANCELED
WHERE ID = 1234

 

UPDATE DB2.예약테이블
SET 상태 = CANCELED
WHERE ID = 1234

 

물론, 네트워크 오류 등의 이유로 Try와 Cancel 명령의 순서가 뒤바뀐 채로 명령이 실행될 수 있습니다. 이러한 경우에 잔고 테이블에 데이터 정합성에 문제가 발생할 수 있습니다. 이에 대응하기 위하여 예약 테이블의 상태(PENDING, SUCCESS, CANCELED)를 고려하여 잔고 테이블에 DML 쿼리를 수행할지 말지를 결정해야 합니다.

 

마찬가지로 Confirm 단계에서도 하나의 로컬 트랜잭션이 실패하더라도 Cancel 작업을 실행하고, 동일하게 순서가 어긋나 데이터 정합성이 깨지는 문제를 방지하기 위해 예약 테이블의 상태 칼럼을 참고하여 처리해야 합니다.

 

2-2. Saga

위의 사례는 결국 (사용자 A 잔고 -100), (사용자 B 잔고 +100) 2가지 이벤트로 나눌 수 있고, 두 이벤트를 반드시 순차적으로 처리할 수 있다면 데이터 정합성을 지킬 수 있습니다. 

 

이처럼, 서비스를 이벤트 단위로 나누어 순차적으로 처리하는 방식을 Saga 패턴이라고 합니다. 순차적으로 처리하기 위해 대표적인 이벤트 스트림인 Kafka를 활용할 수 있습니다.

 

2-3. 두 기술의 비교

TCC와 Saga 방식을 성능 관점에서만 비교를 해보면, Saga 패턴은 순차 처리를 위한 이벤트 스트림의 오버헤드가 발생할 수 있고 TCC는 서로 다른 DB에 Try-Confirm 등의 작업을 병렬로 실행할 수 있기 때문에 TCC가 더 나은 선택으로 보입니다.

하지만, TCC는 예약 테이블을 활용하여 데이터 정합성을 지키는 것이 핵심이기 때문에 이것이 한계가 될 수 있습니다. 위의 예시에서는 서로 다른 두 데이터베이스가 모두 예약 테이블을 생성할 수 있다는 것을 가정했지만, 서로 다른 전자 지갑 서비스 간의 송금이 필요하고 한쪽 전자 지갑 서비스가 예약 테이블을 사용할 수 없는 상황이면 TCC는 사용할 수 없는 해결책이 됩니다.

그러므로, 성능 자체 만을 고려 했을 때는 TCC가 좋은 선택지이나, 여러 시스템과의 확장성을 고려 했을 때는 Saga 패턴도 합리적인 선택이라고 볼 수 있습니다.

 

3. 로그 기록

로그 수집은 외부 감사 등으로 인하여 모든 전자 지갑 이체 이력을 보관하기 위해 필요한 요구사항입니다.

이체 서비스와 관련된 모든 기록을 Kafka와 같은 중앙형 이벤트 스트림에 저장하도록 하고, 전자 지갑 시스템이 아닌 외부 시스템에서 해당 데이터를 활용하여 이체 이력을 관리하는 방향으로 구현할 수 있습니다. 

물론, 중앙형 이벤트 스트림에 저장하지 않고 로컬에 이벤트를 Append-only 파일 형태로 기록하는 방법도 존재합니다.

 

 

다음 글에서는 위 내용을 바탕으로 전자 지갑 시뮬레이션 시스템을 구현한 내용에 대해서 공유하도록 하겠습니다.

감사합니다.