들어가며

작년 말, IfKakao 2024에서 카카오페이에서 지연이체 개발기란 제목의 세미나가 공개됐습니다. Kafka를 활용해 지연이체 서비스를 설계한 과정이 담겨있었는데요. 개인적으로 굉장히 흥미롭게 봤기 때문에, 직접 설계 과정을 따라가보면 왜 카카오페이에서 세미나에 녹여냈던 선택들을 했는지 더 잘 이해할 수 있고 그 과정에서 공부가 많이 되겠다는 생각이 들어, 회사 동기들과 직접 Kafka를 활용해 예약이체 서비스를 사이드 프로젝트 수준에서 설계해봤습니다.

 

참고로 실제 이체 프로세스가 어떻게 정교하게 흘러가는지는 잘 몰랐기 때문에.. 이체 프로세스는 출금계좌의 돈을 빼고, 타행이체일 경우는 API호출을 가정하여 1초간 Thread.sleep()을 하는 수준으로만 가정하고 설계를 진행했습니다.

 

본 글은 다음 목차로 진행됩니다.

 

  1. 예약이체란?
  2. 개략적인 설계
  3. 세부 프로세스 설계
  4. 속도 높이기
  5. 소감
  6. Reference

 

1. 예약이체란?

 

이 글에선 은행 점검 시간에 송금 건을 예약하여 은행 점검 시간 이후 자동으로 예약된 송금 건이 실행되도록 하는 것을 말하도록 하겠습니다. 참고로 송금 예약을 눌렀을 때 어떻게 저장하는지는 여기서 다루지 않고, 저장된 예약이체 건을 실행하는 것에 대한 아키텍처와 프로세스만을 다뤘습니다.

 

2. 개략적인 설계

다음 내용들에 대한 개략적인 설계입니다.

 

  1. 데이터 스키마 설계
  2. API 설계
  3. 개략적인 아키텍처와 프로세스 

 

1) 데이터 스키마 설계

예약이체 테이블 (scheduled_transfer)

필드 설명 자료형
scheduled_transfer_id (PK) 예약이체 건 식별자 bigInt
from_account 출금계좌(source) varchar(20)
to_account 송금계좌(destination) varchar(20)
to_bank_code 송금은행코드 varchar(10)
transfer_amount 송금금액 decimal(15, 2)
transfer_available_dttm 송금가능일시 datetime
status 예약이체 건 상태 tinyInt
scheduled_dttm 예약일시 datetime

 

예약이체 건 상태값으론 0, 1, 2 등이 저장되며 각각 PENDING, COMPLETED, FAILED 등을 의미합니다. 신규 상태값이 추가될 수도 있음을 고려해 enum이 아닌 tinyInt타입을 사용하도록 했습니다. 마지막 수정일시 등의 컬럼도 필요하겠으나 위 표에서는 생략했습니다.

 

고객 정보, 계좌 정보, 은행 정보에 대한 스키마는 아주 간단한 수준(식별자, 고객명, 은행코드, 은행명 등만 있는 수준)으로만 설계했으므로 따로 작성하진 않겠습니다.

 

2) API 설계

앞서 말씀드렸듯 예약이체 건을 저장하는 프로세스는 다루지 않았기 때문에, 예약이체 건을 저장하는 API는 설계하지 않았습니다.

 

POST /v1/scheduled-transfers/:id/execute

예약 이체 건을 실행하는 API입니다. id는 예약이체 건의 식별자이며, API 호출 시 body에 담아 전달하는 인자들은 다음처럼 설계했습니다.

필드 설명 자료형
from_account 출금계좌(source) varchar(20)
to_account 송금계좌(destination) varchar(20)
to_bank_code 송금은행코드 varchar(10)
transfer_amount 송금금액 decimal(15, 2)

 

 

3) 개략적인 아키텍처와 프로세스

가장 초기에 구상한 아키텍처와 프로세스는 다음과 같습니다.

 

각 컴포넌트별 역할 및 프로세스

  1. 스케쥴러(Producer) : scheduled_transfer 테이블에 PENDING상태로 있는 예약이체 건들 중 송금시간이 다가온 것들(송금가능일시 <= 현재시간)을 5분 주기로 조회하여 Kafka로 발행하는 역할을 합니다.
  2. Kafka : 스케쥴러가 발행한 예약이체 건들에 대한 브로커 역할을 합니다. 예약이체 건들은 scheduled-transfer라는 토픽에 저장되며, 해당 토픽은 파티션 3개로 구성했습니다.
  3. Consumer : Kafka에 발행된 예약이체 건들을 가져와서 코어뱅킹 서버로 이체 실행 요청을 보내는 역할을 합니다. 각 Consumer들은 Kafka 파티션들과 1 : 1로 대응되도록 3개로 구성했습니다.
  4. 코어뱅킹 서버 : Consumer로부터 요청받은 예약 이체 건을 실행하는 내부 서버 역할을 합니다.

 

완료된 예약 이체 건의 상태 갱신은 누가 할까?

초기엔 단순하게 Consumer에서 코어뱅킹 서버로 이체 실행 요청을 보낸 후, 응답값에 따라 Consumer에서 예약 이체 건들의 상태를 갱신하는 프로세스를 구상했습니다. 그러나 다음과 같은 케이스들이 발생할 수 있었습니다.

 

  1. 코어뱅킹 서버에서 이체 실행은 완료됐으나 네트워크 오류 등으로 Consumer로 응답을 못 준 경우
  2. 코어뱅킹 서버로부터 응답은 왔으나 Consumer에서 예약 이체 건의 상태 갱신에 실패하는 경우
  3. 등등..

 

예약 이체 테이블(scheduled_transfer)과 계좌 테이블들이 같은 DB에 있었기 때문에, 코어뱅킹 서버에서 이체 실행과 예약 이체 건의 상태 갱신을 하나의 DB 트랜잭션에서 처리하도록 설계하여 위 문제를 해결할 수 있겠다는 생각이 들었습니다. 그러나 실무 상황이라면 예약 이체 테이블과 계좌 테이블이 다른 DB에 있다든가, 코어뱅킹 서버에서 예약 이체 테이블이 있는 DB로 접근할 수 없다든가 하는 제약들이 따를 수 있습니다. 따라서 코어뱅킹 서버에서 예약이체 건 식별자(scheduled_transfer_id)에 대한 멱등 처리를 통해 중복 이체 실행을 막고, 기 실행됐던 예약 이체 건에 대한 실행 요청을 받은 경우 이미 실행된 건임을 알리는 응답을 주도록 한 뒤 Consumer에서 해당 응답에 따라 예약 이체 건들의 상태를 갱신하도록 설계했습니다.

 

 

리소스 낭비 방지를 위한 Consumer 단 필터링 프로세스 추가

스케쥴러가 예약 이체 건들을 kafka로 5분 주기로 발행해주므로, 이전에 발행한 건이 아직 실행되지 않았다면 스케쥴러가 동일한 예약 이체 건을 Kafka로 여러 번 발행할 수 있습니다. 물론 코어뱅킹 서버에서 예약 이체 건 식별자(scheduled_transfer_id)에 대한 멱등 처리를 해줘서 실제로 이체가 중복으로 발생하지 않도록 처리했으나, 처리될 필요가 없는 예약 이체 건도 코어 뱅킹 서버로 전달되어 리소스를 낭비하게 되는 상황은 발생 가능하다고 생각했습니다. 만약 코어뱅킹 서버에서 멱등 처리가 제대로 기능하지 않게 된다면 이는 리소스 낭비에서 끝나지 않고 중복 이체 발생으로도 이어질 수 있습니다. 따라서 Consumer에서 Kafka에서 가져온 예약 이체 건들의 PENDING 여부를 확인 후 필터링된 건들만 이체 실행 요청을 보내도록 했습니다.

 

정리하면 다음과 같은 프로세스를 설계하게 됐습니다.

 

 

3. 세부 프로세스 설계

1) Consumer 단 필터링을 거쳐도 리소스 낭비가 가능했다

스케쥴러가 Kafka로 동일한 예약 이체 건을 중복으로 쌓아두는 상황은 여전히 가능한 상황이었습니다. Consumer에서 상태가 PENDING인지 체크하는 과정을 추가하여 처리할 필요가 없는 예약 이체 건에 대한 리소스 낭비를 막고자 했지만, 만약 동일한 예약이체 건들이 서로 다른 Consumer에서 동시에 실행된다면 다음과 같이 여전히 리소스 낭비가 발생할 수 있었습니다. 

 

위 그림에선 코어뱅킹 서버에서의 멱등 처리로 인해 한 쪽에서는 이체가 실패하여 리소스 낭비에서 끝나게 됐지만, 앞서 설명했듯 만약 코어뱅킹 서버에서 멱등 처리가 제대로 기능하지 않게 된다면 리소스 낭비에서 끝나지 않고 중복 이체 발생으로 이어질 수 있습니다. 이 문제를 해결하려면 동일한 예약 이체 건 여러 개가 동시에 실행되는 것을 제어할 수 있어야 한다고 생각했습니다.

 

구체적인 방법으로는 다음 방법들을 구상했습니다.

 

방법 1. 하나의 예약 이체 건을 정확히 한 번만 Kafka로 발행하자

애당초 동일한 예약 이체 건이 Kafka에 여러 번 중복으로 발행되지 않는다면 동일한 예약 이체 건들이 동시에 실행되는 상황 자체가 벌어지지 않는다는 점을 활용한 해결 방법입니다. 대표적인 구현 방법으로는 스케쥴러가 PENDING 상태인 예약 이체 건들을 조회해서 Kafka에 발행해줄 때, 해당 예약 이체 건들의 상태를 PUBLISHED로 바꿔주는 방법이 있습니다.

 

이 방법은 

 

  1. 스케쥴러가 DB에 해당 예약 이체 건의 상태를 PUBLISHED로 갱신하는 작업
  2. 스케쥴러가 Kafka에 해당 예약 이체 건을 발행하는 작업

 

으로 구분할 수 있으며, 하나의 예약 이체 건을 정확히 한 번만 Kafka로 발행한다는 제약을 지키려면 두 작업의 원자성이 보장되어야 합니다. DB 트랜잭션을 통해 예약 이체 건의 상태를 PUBLISHED로 갱신한 다음 Kafka로 예약 이체 건을 발행하는 작업의 성공/실패 여부에 따라 트랜잭션을 커밋 or 롤백하면 된다고 생각했으나, 다음과 같은 상황도 충분히 발생할 수 있었습니다.

 

 

즉 하나의 DB 트랜잭션만으로는 두 작업의 원자성을 완벽하게 보장할 수 없었습니다. 2 Phase Commit이나 트랜잭셔널 아웃박스 등을 활용하는 방법이 있겠지만, 결국 "어떤 상황이 닥쳐도 하나의 예약 이체 건을 정확히 한 번만 발행해준다"라는 것을 구현하기는 굉장히 어렵겠다는 생각이 들었습니다. 또한 어찌저찌 해서 정확히 한 번 발행에 성공한다고 해도 다음과 같은 상황들을 고려해야 했습니다.

 

  1. 정확히 한 번 발행된 예약 이체 건을 실행하다가 중간에 실패하면 재시도는 어떻게 할 것인가? 다시 예약 이체 건의 상태를 PENDING으로 바꾸게 할 것인가?
  2. 정확히 한 번 발행된 예약 이체 건을 Consumer에서 중복으로 consume하게 되는 상황이 오면 어떻게 할 것인가? 정확히 한 번 발행됐어도, 해당 예약 이체 건을 실행하는 것도 정확히 한 번만 실행되는 것을 어떻게 처리해줄 것인가?
  3. 등등..

 

즉 정확히 한 번 발행에 성공해도 논리적인 관점 등에서 데이터의 일관성이 깨질 수 있는 여러 샛길들이 많다고 생각했습니다. 따라서 하나의 예약 이체 건을 정확히 한 번만 발행한다는 제약 조건을 지키기 위해 그런 샛길들을 모두 고려하며 구현하는 것보다는, 같은 이체 건 여러 개가 동시에 실행되는 것을 제어할 수 있는 다른 방법을 적용하는 것이 더 낫다고 판단했습니다.

 

 

방법 2. 계좌 락을 걸어보자

두 번째로 구상한 방법은 동시성 제어에 보편적으로 많이 활용되는 "락"을 활용하는 것이었습니다. Consumer에서 예약 이체 건의 상태를 체크하는 부분부터 Consumer에서 예약 이체 건의 상태를 갱신하는 부분에 락을 걸어 같은 이체 건이 동시에 실행하는 것을 제어하자는 아이디어였습니다. 예약 이체 건의 상태 체크와 상태 갱신 모두 Consumer에서 수행되고 있기 때문에, Consumer에서 예약 이체 테이블(scheduled-transfer)에 SELECT FOR UPDATE를 통해 배타 락을 거는 방법을 우선적으로 고려했습니다. 그러나 이 방법은 락을 걸고 코어뱅킹 서버로 요청한 이체 실행의 응답이 오기까지 DB 커넥션이 늘어질 여지가 있었기 때문에, 다른 형태로 락을 걸어보자고 생각했습니다. 그 결과 예약 이체 건에 계좌 락을 거는 형태를 고려하게 됐습니다.

 

계좌 락을 얻는 방법으론 계좌 락을 위한 테이블을 별도로 만들어 활용하는 방법과 분산 락을 활용하는 방법이 있었습니다. 전자의 경우 예약 이체 테이블(scheduled-transfer)에 배타 락을 거는 방법과 동일하게 락을 걸고 코어뱅킹 서버로 요청한 이체 실행의 응답이 오기까지 DB 커넥션이 늘어질 여지가 있다고 판단했습니다. 또한  출금계좌에 락을 거는 것은 결국 전체 시스템에서 해당 계좌로 접근하는 스레드를 하나로 제한하기 위함이기도 한데, 실무에서는 계좌 정보를 하나의 DB에서 관리하지 않고 여러 시스템 또는 모듈들이 독립적인 DB를 사용 중일 수 있고 하나의 서버가 여러 서버로 분리될 수도 있기 때문에 테이블 기반의 계좌 락은 전체 시스템에서 해당 계좌로 접근하는 스레드를 하나로 제한하기엔 확장성이 낮다는 생각이 들었습니다. 따라서 분산 락을 활용해 계좌 락을 구현하는 것으로 결정했습니다.

 

분산 락을 통해 같은 이체 건들이 동시에 실행되는 것을 제어하게 되는 과정을 나타내면 다음과 같게 됩니다.

 

 

 

 

 

2) 예약 이체 건이 송금가능일시가 지나도 장기간동안 실행되지 않는 이슈가 발생

하지만 이렇게까지 해놓고 테스트를 돌려보니, 특정 예약 이체 건들이 송금가능일시가 지나도 1 ~ 2시간 이상 실행되지 않는 이슈가 발생했습니다. 

 

원인은 위 흐름도와 같이 출금계좌는 같지만 엄연히 서로 다른 이체 건들이 동시에 실행될 때 한 쪽에선 분산 락 획득을 실패하여 이체가 실행되지 않던 상황이 생길 수 있었다는 것이었습니다. 락을 획득할 때까지 대기시키기는 방법 등이 떠올랐으나 그렇게 되면 현재 구조에선 뒤에 쌓이는 예약 이체 건들이 밀릴 수 있었고, 대기시키는 방법도 이체 실행이 지연되는 건 마찬가지이니 근본적인 해결법은 아니라고 생각했습니다. 따라서 락 경합 자체를 줄일 수 있는 방법들을 고려하게 됐습니다.

 

 

방법 1. 분산 락 키를 바꾸자

현재는 출금계좌를 키로 해서 분산 락을 잡고 있었는데, 다른 값을 키로 잡으면 되지 않을까라는 아이디어였습니다.

 

출금계좌는 각 예약 이체 건들의 고유값이 아니기 때문에 출금계좌를 분산 락 키로 잡으면 서로 다른 이체 건들끼리도 락 경합이 발생 가능합니다. 반면 예약 이체 건 식별자(scheduled_transfer_id)는 각 예약 이체 건들의 고유값이므로 여기에 분산 락 키를 잡아주게 되면 같은 예약 이체 건들이 동시에 실행되는 상황에 한해서만 락 경합이 발생됩니다. 따라서 락 경합을 줄이려는 목적을 달성할 수 있다는 생각이 들었습니다.

 

다만 앞서 말했듯 출금계좌에 락을 거는 것은 시스템 전체에서 해당 계좌로 접근하는 스레드를 하나로 제한하는 효과를 줍니다. 예약 이체 건 식별자에 대해 락을 걸면 락 경합은 분명 줄어들겠지만 해당 계좌로 접근하는 스레드가 여러 개가 될 수 있어 예상치 못한 동시성 관련 문제를 안겨줄 수 있습니다. 이체는 결국 오류없이 수행되는 것이 가장 중요하다고 생각했기 때문에 분산 락 키는 출금계좌를 그대로 쓰게 하여 해당 계좌로 접근하는 스레드를 하나로 제한시켜 안전성을 높이고, 대신 락 경합을 줄일 수 있는 다른 방법을 고려해보기로 했습니다.

 

 

방법 2. 예약 이체 건들을 출금계좌별로 파티셔닝하자

두 번째 방법은 예약 이체 건들을 출금계좌별로 같은 파티션에 가게끔 설정하는 아이디어입니다. 같은 출금계좌를 가진 서로 다른 예약 이체 건들이 동시에 실행되는 이유는 이들이 서로 다른 Consumer에서 실행될 수 있기 때문인데요. 현재 구조(3개 파티션, 3개 Consumer)에서는 각 Consumer들이 서로 다른 파티션을 담당하고 있기 때문(Kafka는 기본적으로 파티션 하나에 같은 Consumer 그룹 내에선 단일 Consumer 스레드가 할당됨)에 같은 출금계좌를 갖는 예약 이체 건들을 같은 파티션으로 발행하게 되면 하나의 Consumer에서만 예약 이체 건들을 처리하게 됩니다. 또한 현재 하나의 Consumer 스레드에서는 담당하는 파티션에 발행된 예약 이체 건들을 순서대로 처리 중이므로 이 경우 같은 출금계좌를 갖는 예약 이체 건들이 동시에 실행되는 것이 방지되는 효과를 가져올 수 있습니다.

 

Kafka Producer(스케쥴러)에서 메시지를 발행할 때 어떤 파티션으로 발행할지는 Partitioner라는 컴포넌트가 담당하는데요. kafka-clients 3.6.2 기준으로, 메시지 발행 시 키를 지정하지 않는다면 다음 그림과 같이 Sticky partitioning전략을 통해 메시지를 파티션으로 발행하게 됩니다.

 

Sticky partitioning은 하나의 파티션을 유지하며 메시지를 발행하다가 특정 조건(배치 크기 초과, 시간 초과 등)이 충족되면 새로운 파티션을 선택하여 메시지를 발행하는 전략으로, 같은 파티션에 연속해서 메시지를 발행하므로 배치 최적화가 가능해지고 불필요한 파티션 변경을 줄여 네트워크 처리 비용 등을 감소할 수 있다는 장점이 있습니다. 다만 현재 설계 중인 예약 이체 시스템과 같이 동시에 실행되면 안 좋은 메시지들(ex: 같은 출금계좌를 가진 서로 다른 예약 이체 건들)이 각기 다른 파티션에 분배되어 동시에 실행되는 문제를 가져다 줄 수 있습니다.

 

반면 Kafka Producer(스케쥴러)에서 메시지를 발행할 때 키를 지정하게 된다면, Partitioner는 다음과 같이 키를 기반으로 파티션을 지정하여 메시지를 발행하게 됩니다.

 

이 경우 같은 출금계좌를 갖는 예약 이체건들은 같은 파티션에 할당되므로, 이들이 동시에 실행되는 상황이 방지되게 됩니다.

 

이렇게 해두고 보니.. 스케쥴러에서 동일한 예약 이체 건을 발행해도 이들은 동일한 파티션으로 할당되고(물론 리밸런싱 등이 발생하면 다른 파티션에 발행될 순 있음), 앞서 말씀드렸듯이 현재 하나의 Consumer 스레드에서는 담당하는 파티션에 발행된 예약 이체 건들을 순서대로 처리 중이므로 동일한 예약 이체 건들이 서로 다른 Consumer에서 동시에 실행되는 상황도 없어지게 됩니다. 즉, 동시성 문제 해결을 위해 분산 락을 사용할 필요가 없어지게 됩니다. 분산 락을 획득하고 해제하는 것도 네트워크 I/O가 수반되는 것이므로 "그러면 이제 분산 락을 굳이 할 필요가 없어졌는데?"라고 생각했으나, 역시나 앞서 말씀드렸듯이 출금계좌에 락을 거는 것은 시스템 전체에서 해당 계좌로 접근하는 스레드를 하나로 제한하는 효과를 줍니다. 예약 이체 시스템이 아닌 다른 시스템에서도 해당 계좌에 접근하는 로직 등이 있을 수 있고, 이들도 분산 락을 통해 계좌 락을 걸어주고 있다면 예약 이체 시스템을 넘어서 시스템 전체에서 해당 계좌로 접근하는 스레드가 하나가 된다는 얘기입니다. 따라서 동시성 문제를 제어할 필요성이 없어지긴 했어도 시스템 전체에서의 안전성을 높이기 위해 여전히 분산 락을 잡아주는 것은 유효하다고 판단하고, 계속해서 분산 락을 사용해도 된다고 생각하게 됐습니다.

 

 

4. 속도 높이기

1) Batch Read 설정

현재 설정은 Consumer들이 각자가 담당하는 파티션에 발행된 예약 이체 건들을 하나 하나 가져와서 처리하고 있는데요. 이런 상황에서 처리량을 높이고 싶을 땐 대표적으로 "일괄 처리"를 도입해볼 수 있고 Consumer에서도 Batch read를 설정하여 파티션에 발행된 예약 이체 건들을 일괄로 가져와서 처리할 수 있습니다.

 

다만 Consumer에서 특정 개수만큼 예약 이체 건들을 모을 때까지 기다렸다가 일괄로 가져가는 형태가 되기 때문에 각 예약 이체 건의 입장에서 보면 실시간성이 떨어질 수 있음을 고려해야 합니다. 예약 이체 시스템의 경우 송금 가능 일시가 되자마자 즉각적인 이체가 발생되어야 하는 서비스는 아니라고 판단하고 Batch Read를 도입해도 된다고 판단했습니다.

 

2) 병렬 처리 및 출금계좌별 스레드 지정

Batch Read로 예약 이체 여러 건을 한 번에 가져와도 기본적으로는 하나의 Consumer 스레드가 이들을 처리합니다. 따라서 처리량을 높이기 위해 복수의 스레드로 이들을 병렬 처리하는 방식을 도입할 수 있습니다.

 

다만 이 경우 같은 출금계좌를 가진 예약 이체 건들이 서로 다른 스레드에서 동시에 실행될 수 있고 이는 잦은 락 경합으로 이어질 수 있습니다. 이전에 출금계좌를 기준으로 특정 파티션에 발행되도록 설정했듯이, Consumer에서 출금계좌별로 처리할 스레드를 따로 지정주어 이 문제를 해결할 수 있습니다.

 

 

5. 소감

저랑 동기들이 설계했던 부분은 여기까지입니다. 막상 돌이켜보니 사실상 카카오페이 세미나에서 발표된 내용과 큰 차이가 없긴 합니다만.. 그래도 특정 문제(카카오페이에서 맞닥뜨린)가 생겼을 때 "이런 방법도 있었을 텐데 왜 이건 쓰지 않았을까?"라는 고민들이 드는 지점이 많았는데 직접 설계를 해보니 "아 이래서 그랬구나"라고 더 이해가 잘 되는 부분들이 많았던 것 같습니다. 예를 들면 "예약 이체 건들을 계좌별로 파티셔닝되게 했다면 분산 락이 필요없어진 것 같은데 왜 안 뺐을까? -> 아 안전성을 위해서였구나!" 등등.. 뭔가 다른 사람들이 설계하는 과정을 따라가보는 것은 처음이었는데 그 안에서 배운 것들이 굉장히 많네요. 또한 스스로 아직 많이 부족함을 느끼게 된 것 같기도 합니다.

 

준비하는 과정에서도 여러 레퍼런스(개인이 올린 블로그 글들은 다 비슷한 감들이 많아.. 테크블로그에서 발행한 컨텐츠들을 최대한 참고하려고 했습니다)들을 봤습니다. 신뢰성 보장을 위해 여러 고민 끝에 기술을 도입하고, 그 기술의 도입으로 인한 리스크들도 기술적인 고민들을 가미하며 해결하는 과정들이 상당히 흥미로웠습니다. 저도 그런 엔지니어가.. 그런 전문성을 가진 사람이 되고 싶다는 생각이 드네요. 더욱,, 정진해야겠습니다.

 

 

6. Reference

1. 지연이체 서비스 개발기 (카카오페이)

https://youtu.be/LECTNX8WDHo?si=8cn67Fbr4CbDtkaA

 

2. 분산 시스템에서 메시지 안전하게 처리하기 (강남언니)

https://blog.gangnamunni.com/post/transactional-outbox/

 

3. 풀필먼트 입고 서비스에서 분산락을 사용하는 방법 (컬리)

https://helloworld.kurly.com/blog/distributed-redisson-lock/

 

4. Kafka 메시지 중복 및 유실 케이스별 해결 방법 (올리브영)

https://oliveyoung.tech/2024-10-16/oliveyoung-scm-oms-kafka/

 

5. SLASH 22 - 애플 한 주가 고객에게 전달 되기까지 (토스증권)

https://youtu.be/UOWy6zdsD-c?si=iTtbPbEDpFaWdPt8

 

 

 

 가상 면접 사례로 배우는 대규모 시스템 설계 기초 2권 - 챕터 4의 내용과 개인적으로 추가 공부한 내용들을 정리한 글입니다.

 

들어가며


현대적인 소프트웨어 아키텍쳐들은 각자만의 인터페이스를 갖고 분리된 여러 컴포넌트들로 구성됩니다. 메시지큐는 이 컴포넌트 사이에서의 통신을 담당하는 컴포넌트로, 각 컴포넌트들이 기존에 sync 방식으로 통신하던 것을 async 방식으로 쉽게 통신할 수 있도록 돕는 역할을 합니다. 메시지 큐 사용 시 다음과 같은 이점들이 있습니다.

 

  1. 결합도 완화 : 각 컴포넌트 사이에 통신을 담당하는 컴포넌트를 두는 것이므로, 기존 컴포넌트 간의 결합도가 낮아지게 됩니다. 이를 통해 좀 더 유연한 설계 및 각 컴포넌트의 독립적인 갱신이 가능합니다.
  2. 확장성 증가 : 메시지 큐를 사용하는 컴포넌트는 데이터를 생산하는 생산자(producer)와 소비자(consumer)로 분류 가능합니다. 결합도가 완화된 것을 통해, 시스템 부하에 맞춰 각 컴포넌트의 규모를 독립적으로 늘릴 수 있습니다. (트래픽이 더 몰려 더 많은 데이터가 생산되면 소비자를 증설 등)
  3. 가용성 개선 : 특정 컴포넌트에 장애가 생겨도 다른 컴포넌트는 메시지 큐와 상호작용 가능합니다.
  4. 성능 개선 : 메시지 큐는 각 컴포넌트들이 async 통신을 쉽게 할 수 있도록 합니다. 생산자는 소비자가 어떻게 처리하든 메시지큐에 데이터를 밀어넣어주면 되고, 소비자는 메시지큐에 있는 데이터를 긁어가서 처리하면 됩니다. 이를 통해 sync 방식에서 생기던 문제점들(ex : 자원을 비효율적으로 사용 등)을 개선할 수 있습니다

 

물론 장점들만 있는 건 아닙니다. 메시지 큐라는 컴포넌트를 추가 운영하면서 생기는 공수도 있을 것이고, 컴포넌트 간의 통신이 메시지 큐라는 컴포넌트를 거쳐 발생하므로 네트워크 문제로 인한 추가 지연이 발생 가능한 것 등의 단점들도 있습니다. 그럼에도 메시지큐는 기존 컴포넌트들간의 결합도를 낮추면서 얻을 수 있는 이점들이 많기 때문에 많은 시스템에서 사용하는 컴포넌트이기도 합니다(해외에서는 메시지큐를 이미 성숙된 기술이라고도 표현합니다).

 

메시지큐는 대표적으로 RabbitMQ등의 서비스가 있으며 Apache Kafka 등의 이벤트 브로커도 많이 쓰입니다. 이 글에서는 분산 메시지 큐를 설계하는 방법과 필요한 지식 등을 소개하겠습니다.

 

※ 참고 : 메시지 큐 vs 메시지 브로커 vs 이벤트 브로커

1) 메시지 큐

  • 컴포넌트 간의 메시지를 전달하기 위한 컴포넌트
  • 메시지를 저장하는 역할

2) 메시지 브로커

  • 메시지 큐의 기능을 포함
  • 추가적으로 메시지 라우팅(pub/sub 패턴 등을 통해..), 메시지 변환 등도 해줌

3) 이벤트 브로커

  • 메시지 브로커의 기능을 포함
  • 추가적으로 메시지(이벤트) 보관 등의 기능을 할 수 있음

 

※ 설계 범위

이 설계에서 메시지는 텍스트만 가정하며, KB수준으로 가정합니다.

 

1) 기능적 요구사항

  • 생산자는 메시지 큐에 메시지를 보낼 수 있어야 합니다.
  • 소비자는 메시지 큐에서 메시지를 소비할 수 있어야 합니다.
  • 메시지는 생산된 순서대로 소비자에게 전달되어야 합니다.
  • 메시지 큐에 넣어진 메시지는 설정에 따라 소비자가 반복적으로 소비할 수도 있고, 한 번만 소비할 수도 있어야 합니다.
  • 메시지는 2주까지만 보관합니다.
  • 메시지 전달은 최소 한 번, 최대 한 번, 정확히 한 번 중에 설정 가능해야 합니다.

 

2) 비기능적 요구사항

  • 높은 대역폭 제공 / 낮은 전송 지연 중 하나를 설정할 수 있어야 합니다.
  • 메시지 양이 급증해도 처리할 수 있도록 확장성있는 설계가 필요합니다.
  • 데이터는 디스크에 지속적으로 보관해야 하며 여러 노드에 복제되어야 합니다.

 

참고) 아래에서 좀 더 설명하겠지만, 높은 대역폭을 제공하려면 메시지를 버퍼링하다가 일괄로 처리해야 하나 이는 메시지들이 즉시 전달되는게 아니므로 전송 지연이 높아지게 되므로 높은 대역폭과 낮은 전송 지연을 만족시키는 것은 힘듭니다.

 

 

메시지큐의 구성 요소와 개략적인 설계


메시지 큐의 핵심 구성 요소와 간략한 흐름도는 다음과 같습니다.

 

 

  1. 생산자가 메시지를 메시지큐에 발행
  2. 소비자는 큐를 구독하고 있고, 구독한 메시지를 소비하게 됨

 

이를 기반으로 한 메시지 모델들은 다음과 같습니다.

 

메시지 모델

1) 일대일 모델

메시지를 소비하는 소비자가 여러 명일 수 있는데, 일대일 모델은 생산자가 메시지 큐에 발행한 메시지는 오직 한 소비자만 가져갈 수 있는 모델을 말합니다.

 

 

2) 발행-구독 모델

관련있는 메시지들을 토픽이라는 논리적인 그룹으로 묶어 메시지들을 주제별로 관리하게 한 다음, 이 토픽으로 메시지를 보내고 받는 모델입니다. 토픽에 전달된 메시지는 해당 토픽을 구독하는 모든 구독자들에게 전달됩니다. 

 

 

 

메시지 큐 컴포넌트의 구성 요소들은 다음과 같습니다.

 

토픽, 파티션, 브로커

  • 토픽 : 메시지 큐에서 데이터를 주고받는 주제 또는 채널의 개념으로, 관련있는 메시지들을 토픽으로 묶어 주제별로 관리합니다.
  • 파티션 : 토픽을 샤딩하여 만든 분할된 각 단위를 파티션이라 부릅니다. 토픽의 확장성과 병렬성을 위해 사용하며, 덕분에 토픽에 데이터가 몰려서 부하가 커지는 것을 방지할 수 있습니다. 파티션은 다수의 메시지 큐 컴포넌트들이 구성하는 클러스터에 고르게 분산 배치됩니다.
  • 브로커 : 파티션들을 유지하는, 즉 파티션들이 있는 서버를 브로커라고 부릅니다. 

참고 : 파티션 내의 메시지 위치는 오프셋이라 부름

 

생산자가 보내는 메시지는 보내질 토픽의 파티션 중 하나로 보내지며, 메시지에 키 값을 설정하여 같은 키 값을 가진 메시지들은 같은 파티션으로만 보내지도록 설정할 수도 있습니다.

 

같은 토픽을 구독하는 소비자가 여러 명이면 각 구독자는 해당 토픽의 파티션들을 분담해서 처리하게 됩니다. 즉 이 경우 모든 소비자들에 구독한 토픽에 발행되는 모든 메시지를 처리하는 게 아니라 서로가 분담해서 처리하는 것으로 이해할 수 있습니다. 이 소비자들을 소비자 그룹이라고 부릅니다.

 

소비자 그룹

소비자 그룹의 소비자들은 앞서 설명했듯 특정 토픽의 메시지들을 서로 분담해서 처리합니다(즉 서로 협력하는 구조). 같은 토픽에 여러 소비자 그룹이 있을 수 있고, 하나의 소비자 그룹이 여러 토픽을 처리하고 있을 수도 있습니다.

 

ex 1) 주문 토픽을 구독하면서 결제를 담당하는 그룹과 재고 차감을 담당하는 그룹이 있을 수 있습니다.

ex 2) 사용자 활동 토픽, 주문 토픽를 둘 다 구독하고 있는 분석용 그룹이 있을 수 있습니다.

 

또한 다음 그림과 같이 특정 소비자 그룹 내의 소비자들이 같은 파티션에 붙으면 어떻게 될지도 생각해볼 필요가 있습니다. 

 

 

 

이 경우, 같은 파티션에 있는 메시지들을 두 개 이상의 소비자가 병렬로 읽게 되므로 대역폭 측면에선 좋겠으나 메시지를 순서대로 소비하는 것이 보장되지 않습니다(병렬처리이므로 순서보장이 어려움). 이 문제는 직관적으로 "한 파티션은 한 소비자 그룹에선 한 소비자만 담당하게 하기"로 해소가 가능합니다. 다만 파티션 수 < 소비자 수 일 때 일부 소비자들은 유휴 현상이 발생 가능하다는 단점이 있습니다. 

 

개략적 설계

설명드린 것들을 바탕으로, 분산 메시지 큐 시스템을 개략적으로 다음처럼 설계 가능합니다.

 

  • 생산자 : 메시지를 특정 토픽으로 보내는 역할
  • 소비자(소비자 그룹) : 토픽을 구독하고 발행된 메시지를 소비하는 역할
  • 브로커 : 토픽을 샤딩(즉 분할)한 단위인 파티션들이 있는 서버
  • 상태 저장소 : 각 브로커별로 갖고 있는 파티션의 상태 정보들(각 소비자 그룹이 마지막으로 가져간 메시지의 오프셋 등)을 저장
  • 메타데이터 저장소 : 토픽 설정과 속성 정보들(토픽별 파티션 수, 메시지 보관 기간 등).. 즉 브로커들이 공통적으로 참조하는 메타데이터들을 저장
  • 조정 서비스(Coordination Service) : 브로커들의 상태 모니터링, 리더 선출(분산 시스템들이 master - slave 구조일 때 master가 다운되면 누가 master가 될지를 선출하는 것) 등을 담당

 

 

상세 설계


파티션(데이터 저장소) 설계

1) 파티션에서의 메시지 저장 방식

메시지들은 토픽에 저장되며, 토픽은 파티션이라는 단위로 샤딩된다고 말씀드렸습니다. 이 파티션에 데이터를 어떻게 저장해볼지를 생각해봐야 하는데요, 대표적으로 DB에 저장하는 것과 파일 형태로 디스크에 저장하는 방식을 생각해볼 수 있습니다. 분산 메시지 큐는 시스템 특성 상 메시지라는 데이터를 write(생산)하고 read(소비)하는 연산이 빈번하게 발생하므로 DB에 저장해 관리하는 것은 한계가 있습니다(대규모의 읽기와 쓰기가 모두 능한 DB는 설계가 어려움). 근데 메시지를 생산하고 소비하는 과정을 보면 읽기/쓰기 모두 메시지에 대한 액세스 패턴이 순차적이므로, 로그파일 형태로 디스크에 저장하도록 설계하면 좋은 효과를 기대할 수 있습니다.

 

하드 디스크는 플래터가 회전하며 읽기/쓰기가 이루어지므로, 데이터 액세스 패턴이 순차적일 때 효과적

 

2) 파티션 복제 (파티션 사본)

또한 파티션들을 복제하여 높은 가용성을 제공하도록 할 수 있습니다. 토픽을 여러 파티션으로 분할한 뒤, 각 브로터들이 다음과 같이 파티션들의 사본도 갖도록 구성할 수 있습니다.

 

 

 

DB도 master에만 write를 하면 사본들로 복제가 되는 것처럼, 생산자가 메시지를 발행할 때도 1번 파티션에 발행해야 한다면 리더역할을 하는 파티션에 발행하도록 한 뒤  사본 파티션들은 리더에서 지속적으로 메시지들을 가져와 동기화하도록 구성할 수 있습니다. 이때 파티션 별로 사본을 어떻게 분산할 지에 대해 기술하는 것을 사본 분산 계획(replica distribution plan)이라 부르며 다음과 같이 요약할 수 있습니다.

 

  • 주문 토픽의 1번 파티션 사본 분산 계획 : 파티션 총 2개. 리더는 1번 브로커, 사본은 2번 브로커에 배치
  • 주문 토픽의 2번 파티션 사본 분산 계획 : 파티션 총 2개. 리더는 2번 브로커, 사본은 1번 브로커에 배치

 

파티션의 리더는 조정 서비스를 통해 브로커 노드 중 하나가 선출되도록 합니다. 해당 브로커가 사본 분산 계획을 만들고 메타데이터 저장소로 저장하도록 합니다. (조정 서비스에 대한 내용은 밑에서 소개하겠습니다).

 

3) 파티션 사본 동기화

DB에서 복제본을 구성할 때, 동기식 / 비동기식 / 반동기식으로 복제 방법을 구성할 수 있습니다. 동기식은 master에 데이터가 쓰인 후 복제본까지 데이터가 쓰인 후에 ack를 보내는 거고, 비동기는 master노드에만 데이터를 쓴 뒤 ack를 보내는 거고, 반동기는 master에 데이터를 쓴 후 복제본으로 데이터 변경에 대한 로그 작성이 끝난 것만 확인한 뒤 ack를 보내는 것을 의미합니다. 앞서 설명한 것처럼 파티션의 동기화도 리더 파티션에 발행된 메시지를 사본 파티션들이 가져가게끔 구성하는데, 메시지 발행에 대한 ack를 어떻게 보내는지 설정해주는 것을 통해 영속성의 정도를 조절할 수 있습니다.

 

이 때 ISR(In-Sync Replicas)이란 개념이 사용됩니다. 리더 파티션과 동기화된 사본 파티션을 일컫는 말이며, 이때 "동기화됐다"의 기준은 토픽 설정에 따라 달라집니다. 리더 파티션의 메시지 개수와 사본 파티션의 메시지 개수는 차이가 날 수도 있는데, 설정된 특정 개수 이하로 차이난다면 "동기화됐다"라고 취급할 수 있습니다. 만약 메시지 차이가 2개 이하면 ISR로 취급한다고 할 경우, 다음 상황에선 사본 1과 2가 ISR입니다.

 

 

리더 파티션에 새로운 메시지가 발행된 후, ISR 상태인 파티션들에게까지 메시지가 전달된 뒤 ack를 보내는 것을 ACK=all이라 합니다. 리더 파티션에 메시지가 발행된 후(즉 메시지가 저장된 후) ack를 보내면 ACK=1, 리더 파티션에 메시지를 전달한 후 ack를 보내는 것을 ACK=0(즉 메시지가 저장됐는지는 관심사가 아님)이라고 합니다. 설정이 높을수록 영속성을 높게 가져가는 것이고, 설정이 낮을수록 낮은 지연을 기대할 수 있습니다.

 

생산자, 소비자 작업 흐름 설계 및 일괄 처리

보통 반복되는 작업을 일괄 처리(batching)하면 네트워크 I/O 또는 디스크 I/O를 줄이게 되며 성능을 개선시킬 수 있습니다(다만 데이터들을 모았다가 한 번에 처리하는 것이므로 개별 데이터의 지연은 높아질 수 있음). 분산 메시지 큐에서는 다음과 같이 생산자와 소비자의 작업 흐름을 구성하며 일괄 처리 도입도 설계해볼 수 있습니다.

 

1) 생산자에서의 작업 흐름 및 일괄 처리 설계

우선 일괄 처리를 배제하고 생각해보겠습니다. 생산자가 새 메시지를 발행할 경우 리더 파티션이 있는 브로커로 메시지를 보내야 합니다. 이때 해당 파티션을 유지하는 브로커로 연결하기 위해 별도의 라우팅용 컴포넌트를 두는 방안을 고려할 수 있으나, 다음과 같이 라우팅 계층을 생산자 내부로 편입시키는 구조를 설계해볼 수 있습니다.

 

 

라우팅 컴포넌트를 별도로 두면 그에 따른 네트워크 지연을 감수해야 하지만, 이렇게하면 거치는 컴포넌트가 하나 더 적으니 전송 지연이 줄어드는 효과를 줍니다. 또한 생산자 입장에서는 전송할 메시지를 버퍼에 모으다 목적지로 일괄 전송하게끔 처리하여 대역폭을 높일 수 있다는 장점을 가져올 수 있습니다.

 

 

2) 소비자에서의 작업 흐름 및 일괄 처리 설계

소비자의 작업 흐름 설계 시, 데이터를 Push / Pull 중 어떤 방식으로 가져올 것인지를 고려해야 합니다.

 

  • Push : 브로커에서 소비자로 직접 데이터를 밀어주는 방식. 메시지가 발행되자마자 소비자에게 밀어줄 수 있으니 지연이 낮다는 장점이 있으나, 데이터 공급의 주도권이 브로커에게 있는 만큼 소비자가 감당 가능한 양 이상으로 데이터를 넣어줄 수 있는 단점이 있습니다.
  • Pull : 소비자가 브로커에서 직접 데이터를 가져오는 방식. 데이터 공급의 주도권이 소비자에게 있으니 자신의 속도에 맞게 브로커로부터 데이터를 가져올 수 있고, 일괄 처리에 적합하다는 장점이 있습니다. 반면 브로커에 메시지가 없어도 소비자가 데이터를 가져가려 시도할 수 있으며 이는 컴퓨팅 자원의 낭비(하지 않아도 되는데 하는 것이므로)가 된다는 단점이 있습니다.

 

참고로 레디스 pub / sub은 Push 방식을, kafka는 Pull 방식을 사용합니다. Pull 방식 사용 시 컴퓨팅 자원이 낭비될 수 있는 부분은 롱 폴링(서버에 요청을 보내고 서버가 새로운 데이터가 있을 때까지 응답을 지연시키는 방식)을 통해 어느 정도 해소가 가능해 대부분의 메시지 큐는 Pull 방식을 많이 지원합니다. 해당 방식의 작업 흐름을 도식화하면 다음과 같습니다.

 

 

소비자 입장에서는 내가 어느 파티션에 붙어서 데이터를 읽고 써야 할지를 알아야 하는데, 이걸 알려주는 역할을 해당 소비자 그룹의 코디네이터 브로커가 해주며 소비자 그룹 이름의 해시값에 매핑되는 브로커가 담당합니다. 즉 서로 다른 소비자 그룹에 대해 같은 브로커가 두 그룹의 코디네이터 역할을 할 수도 아닐 수도 있습니다. 코디네이터 브로커를 통해 어떤 파티션에 붙어야 하는지를 알게 됐다면, 해당 파티션의 오프셋으로부터 메시지를 묶어서 가져오는 식으로 일괄 처리를 설계할 수 있습니다.

 

참고로 소비자 그룹의 코디네이터 브로커는 신규 소비자에 대한 파티션 지정 외에도 그룹 내 소비자들의 상태 감시(heart beat를 통해), 소비자 탈퇴 및 장애 발생 시 파티션 재분배 등의 역할도 담당합니다.

 

상태 저장소 & 메타데이터 저장소 & 조정 서비스 설계

브로커가 유저하는 타피션에 대한 상태 정보를 관리하는 저장소와 토픽의 메타데이터를 관리하는 저장소, 그리고 조정 서비스를 어떻게 설계할 지도 살펴봐야 합니다. 이때 Apache Zookeeper라는 서비스를 사용해볼 수 있습니다. 분산 메시지 큐를 비롯한 여러 분산 시스템에선 각 시스템들의 상태를 모니터링할 수 있는 환경, 범용적으로 사용 가능한 중앙 집중식 데이터 저장소가 등이 필요합니다. 물론 이를 직접 구현할 수 있겠으나, Apache에서 분산 시스템들이 공통적으로 가지는 요구사항들을 해결하기 위해 Zookeeper라는 서비스를 개발했다고 이해할 수 있습니다. 대표적으로 다음 기능들을 제공합니다.

 

  1. 시스템들이 공유하는 공유 상태의 저장 & 관리
  2. 각 시스템 상태 모니터링
  3. 리더 선출 (분산 시스템들이 master - slave 구조일 때 leader가 다운되면 누가 leader가 될지를 선출하는 거. 이거 위해서 주키퍼는 노드 수를 홀수로 맞춘다고 합니다)

 

이를 통해 상태 저장소, 메타데이터 저장소, 조정 서비스의 역할을 Zookeeper가 담당하면서 다음 형태의 설계가 가능합니다.

 

 

 

 

분산 시스템의 설계 이론을 말하는 것으로, 시스템이 Consistency (일관성), Availability (가용성), Partition Tolerance (분할 내성) 이 세 가지를 모두 동시에 완벽히 충족할 수 없다는 것을 말합니다. 이 3가지를 모두 만족시킬 수는 없으니, 시스템이 요구하는 것이 무엇인지에 따라 한 가지는 어느 정도 포기해야 한다는 이론입니다.

 

각 항목별 구체적인 설명은 다음과 같습니다.

 

1. 일관성 (Consistency)

  • 어떤 노드에 연결하든 모든 클라이언트가 동시에 동일한 데이터를 볼 수 있어야 함을 의미합니다. (일종의 정확성)
  • 어떤 데이터가 갱신됐다고 치면, 해당 데이터를 조회할 때 어떤 노드에서 조회하든 모든 노드가 동일한 값을 반환해야 합니다.
  • 데이터가 한 노드에 기록될 때마다 쓰기가 '성공'된 것으로 간주되기 전에 다른 모든 노드에 데이터를 즉시 전달하거나 복제해야 합니다. 

 

2. 가용성 (Availability)

  • 모든 요청(읽기 및 쓰기)에 대해 항상 응답할 수 있어야 함을 의미합니다. 즉 언제든지 서비스가 가용해야 한다는 뜻입니다.
  • 데이터를 요청하는 모든 클라이언트가 하나 이상의 노드가 다운된 경우에도 응답을 받을 수 있어야 합니다.
  • 즉, 분산 시스템의 모든 작업 노드가 예외 없이 모든 요청에 대해 유효한 응답을 반환해야 합니다.
  • 시스템 일부가 장애를 겪더라도 다른 노드가 요청을 처리하여 응답해야 합니다.

 

3. 분할 내성(파티션 허용성이라고도 부름, Partition Tolerance)

  • 시스템에 분할이 생겨도 여전히 시스템은 동작해야 함을 의미합니다.
  • 즉 네트워크 단절(Network Partition, 예를 들면 일부 노드 간 통신 불가) 상황에서도 시스템은 계속 작동할 수 있어야 합니다.

 

 

 

 

단일 노드 시스템이라면 분할 내성은 고려하지 않아도 됩니다. 하지만 분산 시스템은 시스템의 요구사항이 어떻든 네트워크 장애가 언제든 발생할 수 있으므로, 분할 내성은 반드시 챙겨가야 합니다. 따라서 일관성과 가용성 중 시스템에 요구사항에 맞춰 하나를 고르는 형태로 시스템을 설계하게 됩니다. 

 

그럼 이때 왜 일관성과 가용성을 모두 챙길 수 없다는 것일까요? 일관성을 지켜려면 데이터 복제를 기다리는 것이 필요한데, 이 부분이 가용성을 해치는 요소가 되기 때문입니다. 예를 들어 네트워크 장애가 생겼을 때 데이터 일관성(어떤 노드에서 조회하든 같은 데이터가 조회되는 것)을 지키는 것은 상당히 어렵습니다. 이 때 일관성을 챙기겠다고 하면 잠시 요청 처리를 중단하고 중단된 노드가 재실행될 때까지 기다릴 수 있는데, 그 시간 동안 서비스를 정상적으로 이용하지 못 하는 것이니 가용성은 희생됐다고 볼 수 있습니다. 반면 가용성을 챙기겠다고 하면 정상적으로 요청은 처리하게 되지만 정확한 데이터가 전달된다는 보장은 없으니 데이터의 일관성은 희생하게 된 것이라고 볼 수 있겠습니다.

 

형태에 따라 CP, AP 시스템으로 다음처럼 분류 가능합니다.

 

1. CP 시스템 (Consistency + Partition Tolerance)

  • 일관성과 분할 내성은 허용, 가용성은 희생하는 구조
  • 만약 데이터 쓰기 중 네트워크 장애가 발생하면, 모든 노드가 동일한 데이터를 반환할 때가지 쓰기나 읽기를 지연시킴
  • 즉 최신 데이터를 보장하나, 특정 시점엔 요청을 처리하지 못하는 상황(가용성 희생에 따른..)이 발생 가능

 

2. AP 시스템 (Availability + Partition Tolerance)

  • 가용성과 분할 내성은 허용, 일관성은 희생
  • 네트워크 장애 시에도 시스템은 항상 가용하나, 최신 데이터가 아닌 오래된 데이터를 반환할 수도 있음
  • 네트워크 장애 시에도 쓰기/읽기 요청을 허용하며, 노드 간 동기화는 나중에 처리

 

 

 

 

참고한 레퍼런스

https://www.ibm.com/kr-ko/topics/cap-theorem

https://onduway.tistory.com/106

+ Recent posts