이번 주차에는 5장 하나만 나가기로 했다.

 

비동기 연동

비동기 방식을 사용해도 되는 경우

  • 연동에 약간의 시차가 생겨도 문제가 되지 않음
  • 실패했을 때 재시도가 가능함
  • 실패했을 때 나중에 수동으로 처리 가능
  • 실패했을 때 무시해도 괜찮음

비동기 연동을 구현하기 위해 아래에 5가지 방식 소개

 

별도 스레드로 실행

Thread를 생성하는 방법

java.lang.Thread를 이용해 스레드 객체를 직접 생성한다.

// Runnable 함수형 인터페이스를 구현한 인스턴스를 파라미터로
new Thread(**Runnable** instance).start();

// 함수형 인터페이스 람다식으로 변경 가능
new Thread(() -> code).start();

스레드 풀 사용

ExecutorService 를 이용

각 워커 스레드는 OS 네이티브 스레드 1개와 매핑된다.

new Thread(...) 를 하면 → JVM이 OS에 네이티브 스레드 생성을 요청한다.

고려해아 하는 부분

  • 메모리 — 각 스레드마다 스택 메모리
  • 컨텍스트 스위칭 비용
  • OS 스레드 제한 — 프로세스당 생성 가능한 스레드 수

@Async 어노테이션

스프링 프레임워크에서 비동기 처리를 위해 제공하는 어노테이션

비동기 작업은 다른 스레드에서 실행되므로 호출자에게 예외가 자동 전파되지 않고, 스프링 트랜잭션(@Transactional)도 스레드 로컬 기반이라 호출자의 트랜잭션 범위에 포함되지 않는다. 따라서 비동기 쪽에서 별도의 오류 처리 설계가 필요하다.

Thrashing

스레드를 너무 많이 생성하면 쓰레싱 현상이 생길 수 있다.

OS에서 페이지 교체(page fault) 때문에 CPU가 일을 못 하고 디스크 스왑만 계속되는 상태

 

메시징 시스템

사용 목적

  • 비동기 처리
  • 서비스 간 느슨한 결합
  • 백프레셔 (Backpressure) 핸들링 — pull 방식으로 소비자가 자신만의 속도로 처리
  • 이벤트 기반 아키텍처
  • 이벤트 소싱

소비자? 구독자?

  • 소비자 : 메시지를 가져와 처리하는 주체. 처리하고 ack하여 큐에서 없애는 느낌
  • 구독자 : 특정 이벤트/토픽을 구독해서 알림을 받는 주체.

메시지 종류

  • 이벤트 : 이미 발생한 사건/상태 변화 알림 (보통 1:N)
  • 커맨드 : 수행해야 할 행위가 담긴 메시지 (보통 1:1)

주요 메시징 시스템 주요 특징

  • Kafka
    • 수평 확장 용이 → 브로커, 파티션, 소비자를 늘린다.
    • 메시지 파일 보관 → 유실되지 않음
    • 하나의 토픽에 여러 파티션 가능. 파티션 단위로는 순서 보장하지만, 토픽 수준에서는 순서 보장 X
    • 소비자는 메시지 언제든지 재처리 가능 → 소비자는 파티션마다 offset / committed offset을 가지고 있다. offset을 과거로 되돌려서 다시 읽는다는 의미
    • pull 모델
  • RabbitMQ
    • 메모리에만 메시지 보관하는 큐 설정 사용시 메시지 유실 가능
    • 큐에 등록된 순서대로 소비자에 전송 → push 모델
    • ACK 기능 제공
    • AMQP, STOMP 등 여러 프로토콜 지원
    • 게시/구독 패턴, 요청/응답 패턴, Point-to-Point 패턴 지원
    • 우선순위 지정해서 처리 순서 변경 가능
  • Redis pub/sub
    • 메모리 사용 → 지연 시간 짧고, 처리량 높음
    • 구독자 없으면 메시지 유실
    • 기본적으로 영구 메시지 지원 X
    • 모델 단순
       

글로벌 트랜잭션(분산 트랜잭션)

하나의 논리적 작업을 여러 시스템/DB에 걸쳐 한 번에 성공 또는 한 번에 실패(원자성)처럼 보장하려는 트랜잭션

다음 요소들로 인해 만들기 어렵고 비용도 크다.

  • 네트워크 지연/단절
  • 부분 실패
  • 롤백 불가능한 외부 시스템

2PC (Two-Phase Commit)

구성 요소

  • Coordinator : 전체 트랜잭션 관리
  • Participants : 트랜잭션에 참여하는 DB나 시스템

Phase 1: Prepare

  1. 코디네이터가 참여자들에게 커밋할 준비가 되었는지 메시지 전송
  2. 참여자들은 락을 잡고 커밋 가능한지 Y/N 를 응답

Phase 2: Commit/Rollback

  • 모두 YES → 코디네이터가 커밋하라는 메시지 전송
  • 하나라도 NO → 코디네이터가 롤백하라는 메시지 전송

한계

  • 코디네이터가 죽어버리면..? 참여자가 커밋인지 롤백인지 메시지를 못 받아서 락을 잡고 기다릴 수 있음
  • 성능 저하 문제

Saga 패턴

2PC 처럼 강한 원자성을 만들지 않고, 단계별로 커밋하되 실패하면 보상을 줌으로써(보상 트랜잭션) 최종 일관성을 가지게 하는 것.

(최종 일관성 : 두 데이터 저장소 간의 일관성을 보장하지만, 즉시가 아닌 일정 시간 후에 일관성이 맞춰진다. 일시적으로 저장소 간의 불일치 발생 가능)

2가지 구현 방법

  • Choreography-Based (이벤트 기반)
    • 서비스들이 이벤트를 발행 및 구독하면서 다음 단계를 이어나간다.
  • Orchestration-Based
    • 지휘자가 있어, 지휘자가 단계나 상태를 관리함

고려해야 하는 부분

  • 중복 처리 (멱등성)
  • 보상 트랜잭션은 완전한 되돌리기가 아닐 수 있다. (예를 들면, 배송 시작 → 반품 프로세스 진행)

참고자료

SSAFY 특화 프로젝트에서 코어 뱅킹 시스템 만들었을 때 헥사고날 아키텍처를 적용했는데, 그 때 이체 기능(계정 잔고 증감, 원장 기입이 포함된)에 아래 자료를 참고해서 사가 패턴과 보상 트랜잭션을 적용함

토스ㅣSLASH 24 - 보상 트랜잭션으로 분산 환경에서도 안전하게 환전하기

 

트랜잭션 아웃박스 패턴

  1. 비즈니스 데이터 변경과 발행할 메시지를 같은 DB 트랜잭션으로 outbox table에 함께 기록해 원자성을 확보하고
  2. 별도 퍼블리셔가 outbox table 을 읽어(폴링/CDC) 메시징 시스템으로 전송함으로써 네트워크/브로커 장애로 인한 메시지 유실과 불일치를 방지하는 패턴

보통 outbox 테이블에 많이 들어가는 것들

  • id (PK)
  • message_id : 메시지 아이디
  • message_type : 메시지 타입
  • payload (json/text) : 이벤트 데이터
  • status : 발행 상태
  • retry_count : 재시도 횟수
  • next_retry_at : 다음 재시도 시각
  • created_at : 생성 시각
  • updated_at : 상태 변경/재시도 시각
  • published_at : 발행 시각
  • last_error : 마지막 실패 원인

중복 발생/중복 소비 가능성은 충분히 생길 수 있다.

  • 메시지 발행 성공했지만, 어떤 문제로 인해 상태 업데이트 실패 가능성
  • 그래서 보통 at-least-once + 멱등성 처리
  • exactly-once는 네트워크가 껴있는 이상, 이론적으로 매우매우매우 어려움
     

CDC(Capture Data Change)

DB에서 발생하는 데이터 변경(INSERT/UPDATE/DELETE)을 감지하고 이 이벤트를 다른 시스템에 전달 및 동기화 하는 기법

구현 방식

  • Log-based CDC — 트랜잭션 로그를 읽어서 변경 감지
  • Trigger-based CDC — 데이터 변경 사항을 DB 내 별도 로그 테이블에 저장해 관리
  • Query-based CDC — DB에 쿼리 날려 변동 사항을 조회
  • Debezium
    • CDC를 위한 오픈소스 플랫폼 (대표적인 log-based cdc)
    • Kafka Connect위에서 돌아가는 대표적인 Source Connector

스터디를 하면서…

  • 멀티 프로세스? 멀티쓰레드?
    • 멀티 프로세스 — 여러 프로세스를 띄움
      • 메모리 공간: 서로 분리되어 있음
      • 통신: IPC
      • 장점
        • 격리/안정성. 한 프로세스가 죽어도 다른 프로세스에 영향이 적다
        • 보안/권한 분리
      • 단점
        • 프로세스 생성 및 컨텍스트 스위칭 비용
        • 메모리 중복이 생길 수 있음
    • 멀티쓰레드 — 한 프로세스 안에 여러 스레드가 실행
      • 메모리 공간: 스택 제외 데이터 공유
      • 통신: 메모리 공유. 동기화 필요
      • 장점
        • 생성 및 전환 비용이 프로세스보다 상대적으로 작음
        • 메모리 공유로 데이터 전달
      • 단점
        • 공유 자원으로 인한 race / dead-lock 등 위험 요소
        • 한 스레드로 인해 프로세스 전체에 영향을 미칠 수 있다
  • CompletableFuture — 추가적인 공부 필요