Загружаем каталог…
Загружаем каталог…
1. 목표 이 글을 읽고 나면 다음을 설명할 수 있다. RDBMS의 @Transactional 과 DynamoDB Transaction의 차이 RDS(MySQL)와 DynamoDB에서 트랜잭션을 처리하는 방식 MySQL과 DynamoDB를 함께 사용할 때 하나의 트랜잭션으로 묶기 어려운 이유 Saga와 이벤트를 이용해 서로 다른 서비스의 작업을 연결하고, Outbox와 Retry 등을 통해 데이터 일관성을 보완하는 방법 2. 개념 2-1. @Transactional 은 어떻게 동작하는가 @Transactional 은 트랜잭션 자체를 구현하는 기능이라기보다 메서드의 실행 범위를 하나의 트랜잭션으로 처리하도록 Spring에 알려주는 역할 을 한다. Spring 동작방식 Transaction Manager를 통해 데이터베이스 Connection의 트랜잭션을 시작하고, 메서드가 정상적으로 끝나면 Commit, 예외가 발생하면 Rollback한다. @Transactional 메서드 호출 ↓ Spring ↓ Transaction Manager ↓ JDBC Connection ↓ BEGIN ↓ MySQL ┌──┴──┐ 정상 종료 예외 발생 ↓ ↓ COMMIT ROLLBACK 실제로 데이터를 Commit하거나 Rollback하는 주체는 데이터베이스다. Spring은 Transaction Manager를 통해 하나의 Connection에서 여러 SQL 작업이 동일한 트랜잭션으로 처리되도록 연결한다. DynamoDB 동작방식 DynamoDB는 JDBC Connection을 사용하는 방식이 아니라 AWS API 요청 단위로 동작 한다. 예를 들어 다음과 같이 여러 번 save() 를 호출하면 각각 별도의 요청이 발생한다. save(orderInfo) → 요청 1 → 저장 save(item #1) → 요청 2 → 저장 save(item #2) → 요청 3 → 실패 이 경우 1, 2의 작업은 이미 DynamoDB에 반영되었기 때문에 3이 실패해도 앞선 작업까지 자동으로 되돌아가지 않는다. 따라서 일반적인 Spring @Transactional 만 사용하는 것으로 DynamoDB의 여러 save() 를 하나의 트랜잭션으로 묶을 수 없다. 2-2. RDS(MySQL)와 DynamoDB의 차이 RDS에서 @Transactional 이 동작하는 이유는 RDS에서 사용하는 데이터베이스가 MySQL 같은 RDBMS이기 때문 이다. 구분 RDS / MySQL DynamoDB 데이터베이스 RDBMS NoSQL 접근 방식 JDBC Connection AWS API 요청 트랜잭션 경계 Connection 내부 Transaction API 요청 Spring @Transactional 사용 가능 일반적인 방식으로는 적용되지 않음 여러 작업 묶기 DB Transaction TransactWriteItems 따라서 로컬에서 실행하는 MySQL과 AWS RDS의 MySQL은 애플리케이션 입장에서는 접속 대상만 다를 뿐, Spring이 JDBC/JPA를 통해 트랜잭션을 처리하는 방식은 동일하다. ★ 차이점: "트랜잭션 경계를 어디에서 관리하는가" RDBMS → 하나의 Connection에서 여러 SQL을 하나의 트랜잭션으로 처리 DynamoDB → DynamoDB Transaction API를 통해 여러 작업을 하나의 요청으로 묶음 2-3. DynamoDB Transaction DynamoDB는 여러 아이템에 대한 작업을 하나의 트랜잭션으로 처리할 수 있는 API를 제공한다. API 역할 TransactWriteItems 여러 Put , Update , Delete 를 하나의 원자적 작업으로 처리 TransactGetItems 여러 아이템을 트랜잭션 단위로 읽기 이 글에서는 쓰기 작업에 사용하는 TransactWriteItems 를 기준으로 살펴본다. 일반적인 save() 와 비교하면 차이가 명확하다. [일반 save()] orderInfo 저장 ✔ ↓ item #1 저장 ✔ ↓ item #2 저장 ✘ → orderInfo와 item #1은 남아 있음 [TransactWriteItems] orderInfo + item #1 + item #2 ↓ 하나의 요청 ↓ item #2 실패 ↓ 전체 취소 AWS SDK v2의 Enhanced Client를 사용하면 다음과 같이 여러 작업을 하나의 요청에 담을 수 있다. public void createOrder( ShopData orderInfo, List<ShopData> itemEntities ) { TransactWriteItemsEnhancedRequest.Builder builder = TransactWriteItemsEnhancedRequest.builder() .addPutItem(shopDataTable, orderInfo); itemEntities.forEach( item -> builder.addPutItem(shopDataTable, item) ); try { enhancedClient.transactWriteItems(builder.build()); } catch (TransactionCanceledException e) { throw new RuntimeException("주문 저장 트랜잭션 실패", e); } } 코드설명: addPutItem() 으로 필요한 작업을 하나의 요청에 담은 뒤 transactWriteItems() 로 실행 하나의 작업이라도 트랜잭션 조건을 만족하지 못하면 전체 작업이 취소된다. 사용 시 고려 사항: 한 트랜잭션에 최대 100개 작업 총 요청 크기 최대 4MB 동일 아이템을 하나의 트랜잭션에서 중복 처리할 수 없음 일반 쓰기보다 추가적인 용량이 사용되므로 원자성이 필요한 작업에 사용하는 것이 적절함 여기까지는 하나의 저장소 내부에서 트랜잭션을 처리하는 방법 이다. 문제는 MySQL과 DynamoDB처럼 서로 다른 저장소를 함께 사용할 때 발생한다. 3. MySQL + DynamoDB 3-1. 다른 데이터베이스를 하나의 트랜잭션으로 묶는 방법 각 저장소 내부에서는 트랜잭션을 사용할 수 있지만, 두 저장소의 트랜잭션을 단순한 @Transactional 하나로 묶을 수는 없다. MySQL과 DynamoDB는 각각 자신의 트랜잭션을 관리하기 때문이다. Spring ├── MySQL │ └── @Transactional │ └── DynamoDB └── TransactWriteItems 두 트랜잭션은 서로의 상태를 알지 못하기 때문에, 발생하는 문제점으로는 MySQL 저장(COMMIT)은 성공했으나, DynamoDB 저장이 실패할 수 있다. MySQL 저장 ✔ DynamoDB 저장 ✔ ↓ <COMMIT 완료> ↓ DynamoDB 저장 ✘ MySQL Commit ✘ 이 경우, 다른 DBMS 실패를 이유로 기존에 COMMIT된 DBMS를 자동으로 Rollback 할 수 없다. 이론적으로는 2PC(Two-Phase Commit) 같은 분산 트랜잭션 방식이 있지만, 구현 복잡도와 성능 부담이 있고 DynamoDB를 전통적인 XA 기반 분산 트랜잭션에 참여시키는 방식도 일반적이지 않다. 추천하는 방법은, MySQL과 DynamoDB를 함께 사용하는 경우 각 저장소의 로컬 트랜잭션을 사용하고, 이벤트로 작업을 연결하는 방식 이다. 이때 등장하는 대표적인 패턴이 Saga 다. 3-2. Saga와 Choreography Saga는 하나의 큰 트랜잭션 대신 각 서비스의 로컬 트랜잭션을 순차적으로 실행하고, 실패하면 보상 작업을 수행하는 방식 이다. Service A │ │ 로컬 트랜잭션 ↓ 성공 이벤트 ↓ Message Broker ↓ Service B │ │ 로컬 트랜잭션 ↓ 성공 이벤트 ↓ Message Broker ↓ Service C 중간 단계에서 실패하면 이전 작업을 ROLLBACK하는 것이 아니라 보상 트랜잭션(Compensating Transaction) 을 별도의 작업으로 실행한다. 예를 들어 주문을 취소해야 한다면 이미 Commit된 주문 데이터를 DB Rollback으로 되돌리는 것이 아니라 주문 상태를 CANCELED 로 변경하는 식이다. Saga의 조율 방식은 크게 두 가지다. 구분 Choreography Orchestration 조율 주체 없음 중앙 Orchestrator 다음 작업 이벤트를 받은 서비스가 판단 Orchestrator가 지시 서비스 간 결합 이벤트 기반 Orchestrator에 의존 전체 흐름 파악 상대적으로 어려움 상대적으로 쉬움 이번에는 Choreography 를 기준으로 살펴본다. 주문 서비스 ↓ 주문 생성 이벤트 ↓ Message Broker ↓ 상세 서비스 ↓ 상세 데이터 저장 각 서비스는 자신의 데이터베이스에 대한 로컬 트랜잭션만 관리한다. 따라서 모든 데이터가 동시에 변경되는 것이 아니라 이벤트가 전달되는 과정에서 잠시 차이가 발생할 수 있다. 이를 최종적 일관성(Eventual Consistency) 이라고 한다. 4. 이벤트 전달의 신뢰성 Saga에서는 이벤트가 다음 서비스로 전달되어야 다음 로컬 트랜잭션이 실행된다. 그런데 여기서 또 하나의 문제가 생긴다. DB에 데이터를 Commit하는 작업과 메시지 브로커에 이벤트를 발행하는 작업 역시 서로 다른 시스템에서 수행되기 때문이다. 메시지 브로커 종류 및 특징 기술 방식 특징 RabbitMQ Queue / Exchange 메시지 라우팅과 작업 큐에 적합 Apache Kafka Topic / Partition 대규모 이벤트 스트리밍, 메시지 보관 및 재처리에 적합 Amazon SQS Queue AWS 관리형 큐, 서버 운영 없이 사용 가능 4-1. 메시지 브로커 장애 주문 DB 저장 ✔ ↓ 이벤트 발행 ↓ Message Broker 장애 ✘ ↓ 상세 서비스 이벤트 수신 실패 주문 데이터는 이미 Commit됐지만 후속 작업은 실행되지 않을 수 있다. 즉, Saga를 사용한다고 해서 데이터 일관성 문제가 모두 해결되는 것은 아니다. DB 저장 ↓ ? 이벤트 발행 DB와 메시지 브로커 사이에서도 "한쪽은 성공했는데 다른 쪽은 실패하는 문제" 가 발생한다. 4-2. Outbox Pattern 이 문제를 해결하는 대표적인 방법이 Transactional Outbox Pattern 이다. 핵심은 이벤트를 바로 브로커에 보내지 않고, 비즈니스 데이터와 이벤트 정보를 같은 DB에 먼저 저장하는 것 이다. ┌──────────────────────┐ │ MySQL │ │ │ │ Order │ │ Outbox Event │ └──────────┬───────────┘ │ 같은 Transaction ↓ COMMIT │ ↓ Relay / Publisher │ ↓ Message Broker │ ↓ Consumer 예를 들어 주문 생성과 이벤트 기록을 같은 트랜잭션으로 처리한다. MySQL Transaction ├── Order 저장 └── Outbox Event 저장 ↓ COMMIT 둘 다 성공하거나 둘 다 실패한다. Commit 이후 별도의 Relay가 Outbox를 읽어 메시지 브로커로 이벤트를 발행한다. 브로커가 일시적으로 장애가 발생해도 이벤트 자체는 DB에 남아 있기 때문에 복구 후 다시 발행할 수 있다. DynamoDB를 사용하는 경우에도 같은 개념을 적용할 수 있다. DynamoDB Transaction ├── Order Item 저장 └── Outbox Item 저장 ↓ TransactWriteItems ↓ COMMIT 즉, 비즈니스 데이터와 이벤트를 같은 트랜잭션 경계 안에 넣어 이벤트 유실 가능성을 줄이는 것 이다. 4-3. Retry, DLQ, Idempotency Outbox를 사용한다고 장애가 없을 것 같으냐,. 아니다. 이 무슨. 이벤트를 전달하고 처리하는 과정에서도 실패와 중복이 발생할 수 있다. 방법 역할 Outbox 이벤트 유실 방지 Retry 일시적인 전송·처리 실패 재시도 DLQ 반복적으로 실패하는 메시지 격리 Idempotency 동일 메시지가 여러 번 처리되어도 결과가 중복되지 않도록 방어 Broker HA 브로커 장애 가능성을 낮춤 Outbox를 사용했을 때 발생하는 가장 큰 주의점은 Retry와 Outbox를 사용하면 중복 메시지 가 발생할 수 있다는 것이다. 이벤트 발행 ↓ Consumer 처리 성공 ↓ 응답 전달 실패 ↓ Retry ↓ 같은 이벤트 다시 전달 따라서 Consumer는 같은 이벤트를 여러 번 받아도 최종 결과가 한 번 처리한 것과 같도록 멱등성(Idempotency) 을 고려해야 한다. 계속 처리에 실패하는 메시지는 DLQ로 보내 별도로 확인하고 재처리할 수 있다. Broker HA는 브로커 자체의 장애 가능성을 줄이는 역할을 하지만 장애를 완전히 없애는 것은 아니다. 각 방법이 해결하는 문제는 다음과 같이 구분할 수 있다. Outbox ↓ 이벤트 유실 방지 Retry ↓ 일시적 실패 대응 ↓ 중복 가능 ↓ Idempotency Retry 반복 실패 ↓ DLQ Broker HA ↓ 브로커 장애 가능성 감소 5. 정리 트랜잭션을 어디까지 하나로 묶을 수 있는지는 데이터 저장소와 시스템의 경계 에 따라 달라진다. 범위 일관성을 관리하는 방법 단일 RDBMS @Transactional + DB Transaction 단일 DynamoDB TransactWriteItems 서로 다른 서비스·저장소 Saga + 이벤트 DB ↔ Message Broker Outbox + Retry / DLQ / Idempotency RDS의 MySQL에서는 Spring의 @Transactional 과 DB Transaction을 이용해 여러 SQL 작업을 하나로 묶을 수 있다. DynamoDB 내부에서 여러 아이템을 원자적으로 처리해야 한다면 TransactWriteItems 를 사용한다. MySQL과 DynamoDB처럼 서로 다른 저장소의 작업을 하나의 트랜잭션으로 묶으려면 각 저장소의 로컬 트랜잭션을 사용하고 Saga와 이벤트를 통해 서비스 간 작업을 연결할 수 있다. 이벤트 기반 구조에서는 DB와 메시지 브로커 사이에서도 동일한 문제가 발생한다. 이를 보완하기 위해 Outbox, Retry, DLQ, Idempotency 등을 함께 사용한다. 모든 작업을 하나의 트랜잭션으로 묶는 것이 아니라, 각 경계에서 어떤 방식으로 일관성을 보장할 것인지 결정하는 것 이 이번 글의 핵심이다. 참고 자료 Spring Framework — Transaction Management AWS — DynamoDB Transactions AWS SDK for Java 2.x — DynamoDB Enhanced Client Transactions AWS — DynamoDB TransactWriteItems API Reference RabbitMQ — RabbitMQ vs Kafka Apache Kafka — Introduction AWS — What is Amazon SQS?
То, что RADAR обнаружил и классифицировал для этой возможности. Это опубликованный источником текст, а не подтверждение, что предложение ещё действует.
[DynamoDB #1] Transaction과 데이터 일관성. 1. 목표 이 글을 읽고 나면 다음을 설명할 수 있다. RDBMS의 @Transactional 과 DynamoDB Transaction의 차이 RDS(MySQL)와 DynamoDB에서 트랜잭션을 처리하는 방식 MySQL과 DynamoDB를 함께 사용할 때 하나의 트랜잭션으로 묶기 어려운 이유 Saga와 이벤트를 이용해 서로 다른 서비스의 작업을 연결하고, Outbox와 Retry 등을 통해 데이터 일관성을 보완하는 방법 2. 개념 2-1. @Transactional 은 어떻게 동작하는가 @Transactional 은 트랜잭션 자체를 구현하는 기능이라기보다 메서드의 실행 범위를 하나의 트랜잭션으로 처리하도록 Spring에 알려주는 역할 을…
Открыть источник