

기계 번역으로 제공되는 번역입니다. 제공된 번역과 원본 영어의 내용이 상충하는 경우에는 영어 버전이 우선합니다.

# 트랜잭션 아웃박스 패턴
<a name="transactional-outbox"></a>

## 의도
<a name="transactional-outbox-intent"></a>

트랜잭션 아웃박스 패턴은 단일 작업에 데이터베이스 쓰기 작업과 메시지 또는 이벤트 알림이 모두 포함된 경우에 분산 시스템에서 발생하는 이중 쓰기 작업 문제를 해결합니다. 이중 쓰기 작업은 애플리케이션이 서로 다른 두 시스템에 데이터를 쓸 때 발생합니다. 예를 들어 마이크로서비스가 데이터베이스에 데이터를 보관하고 메시지를 전송하여 다른 시스템에 알려야 하는 경우가 이에 해당합니다. 이러한 작업 중 하나에서 오류가 발생하면 데이터가 일치하지 않게 될 수 있습니다.

## 목적
<a name="transactional-outbox-motivation"></a>

데이터베이스 업데이트 후 마이크로서비스가 이벤트 알림을 보내는 경우 데이터 일관성과 신뢰성을 보장하기 위해 이 두 작업이 원자적으로 실행되어야 합니다.
+ 데이터베이스 업데이트는 성공했지만 이벤트 알림이 실패할 경우 다운스트림 서비스는 변경 사항을 인식하지 못해 시스템이 일관되지 않은 상태가 될 수 있습니다.
+ 데이터베이스 업데이트에 실패했지만 이벤트 알림이 전송되면 데이터가 손상되어 시스템의 신뢰성에 영향을 미칠 수 있습니다.

## 적용 가능성
<a name="transactional-outbox-applicability"></a>

다음과 같은 경우 트랜잭션 아웃박스 패턴을 사용합니다.
+ 데이터베이스 업데이트에서 이벤트 알림이 시작되는 이벤트 기반 애플리케이션을 구축하고 있습니다.
+ 두 서비스를 포함하는 작업에서 원자성을 보장하고자 합니다.
+ [이벤트 소싱 패턴](event-sourcing.md)을 구현하고자 합니다.

## 문제 및 고려 사항
<a name="transactional-outbox-issues"></a>
+ **중복 메시지**: 이벤트 처리 서비스에서 중복된 메시지나 이벤트를 보낼 수 있으므로, 처리된 메시지를 추적하여 소비하는 서비스를 멱등 상태로 만드는 것이 좋습니다.
+ **알림 순서**: 서비스에서 데이터베이스를 업데이트하는 순서와 동일한 순서로 메시지나 이벤트를 전송합니다. 이는 데이터 스토어의 시점 복구에 이벤트 스토어를 사용할 수 있는 이벤트 소싱 패턴에 있어 매우 중요합니다. 순서가 올바르지 않으면 데이터 품질이 저하될 수 있습니다. 알림 순서가 유지되지 않으면 최종 일관성과 데이터베이스 롤백으로 인해 문제가 복잡해질 수 있습니다.
+ **트랜잭션 롤백**: 트랜잭션이 롤백된 경우 이벤트 알림을 보내지 마세요.
+ **서비스 수준 트랜잭션 처리**: 여러 서비스에 걸친 트랜잭션에서 데이터 스토어 업데이트가 요구되는 경우, [Saga 오케스트레이션 패턴](saga-orchestration.md)을 사용하여 데이터 스토어 전체의 데이터 무결성을 유지합니다.

## 구현
<a name="transactional-outbox-implementation"></a>

### 전반적인 아키텍처
<a name="transactional-implementation-high-level-arch"></a>

다음 시퀀스 다이어그램은 이중 쓰기 작업 중에 발생하는 이벤트의 순서를 보여줍니다.

![\[이중 쓰기 작업 중 이벤트 순서\]](http://docs.aws.amazon.com/ko_kr/prescriptive-guidance/latest/cloud-design-patterns/images/outbox-1.png)


1. 항공편 서비스가 데이터베이스에 데이터를 쓰고 결제 서비스에 이벤트 알림을 보냅니다.

1. 메시지 브로커가 메시지와 이벤트를 결제 서비스에 전달합니다. 메시지 브로커에 장애가 발생하면 결제 서비스가 업데이트를 수신할 수 없습니다.

항공편 데이터베이스 업데이트에 실패했지만 알림은 전송될 경우 결제 서비스는 이벤트 알림에 따라 결제를 처리합니다. 이로 인해 다운스트림 데이터 불일치가 발생할 수 있습니다.

### AWS 서비스를 사용한 구현
<a name="transactional-implementation-aws-services"></a>

시퀀스 다이어그램의 패턴을 설명하기 위해, 다음 다이어그램에 나와 있는 것처럼 다음 AWS 서비스를 사용하겠습니다.
+ 마이크로서비스는 [AWS Lambda](https://aws.amazon.com/lambda/)를 사용하여 구현됩니다.
+ 기본 데이터베이스는 [Amazon Relational Database Service(RDS)](https://aws.amazon.com/rds/)에서 관리됩니다.
+ [Amazon Simple Queue Service(Amazon SQS)](https://aws.amazon.com/sqs/)는 이벤트 알림을 수신하는 메시지 브로커 역할을 합니다.

![\[, AWS Lambda Amazon RDS 및 Amazon SQS를 사용한 트랜잭션 아웃박스 패턴\]](http://docs.aws.amazon.com/ko_kr/prescriptive-guidance/latest/cloud-design-patterns/images/outbox-2.png)


거래를 커밋한 후 항공편 서비스에 장애가 발생하면 이벤트 알림이 전송되지 않을 수 있습니다.

![\[커밋 작업 후 트랜잭션 실패\]](http://docs.aws.amazon.com/ko_kr/prescriptive-guidance/latest/cloud-design-patterns/images/outbox-3.png)


하지만 트랜잭션이 실패하고 롤백되더라도 이벤트 알림이 계속 전송되어 결제 서비스가 결제를 처리하게 될 수 있습니다.

![\[커밋 작업 후 트랜잭션 실패와 롤백\]](http://docs.aws.amazon.com/ko_kr/prescriptive-guidance/latest/cloud-design-patterns/images/outbox-4.png)


이 문제를 해결하려면 아웃박스 테이블을 사용하거나 변경 데이터 캡처(CDC) 기능을 사용할 수 있습니다. 다음 섹션에서는 이 두 가지 옵션과 AWS 서비스를 사용하여 이를 구현하는 방법을 설명합니다.

#### 관계형 데이터베이스에 아웃박스 테이블 사용
<a name="transactional-implementation-rdb"></a>

아웃박스 테이블에는 항공편 서비스의 모든 이벤트가 타임스탬프 및 시퀀스 번호와 함께 저장됩니다.

항공편 테이블이 업데이트되면 아웃박스 테이블도 동일한 트랜잭션에서 업데이트됩니다. 또 다른 서비스(예: 이벤트 처리 서비스)가 아웃박스 테이블에서 데이터를 읽고 Amazon SQS로 이벤트를 전송합니다. Amazon SQS는 추가 처리를 위해 이벤트 관련 메시지를 결제 서비스에 전송합니다. [Amazon SQS 표준 대기열](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/standard-queues.html)은 메시지가 한 번 이상 전송되고 손실되지 않도록 보장합니다. 하지만 Amazon SQS 표준 대기열을 사용하는 경우 동일한 메시지 또는 이벤트가 두 번 이상 전송될 수 있으므로, 이벤트 알림 서비스가 멱등적인지 확인해야 합니다(즉, 동일한 메시지를 여러 번 처리해도 부작용이 없어야 함). 메시지 순서 지정과 함께 메시지를 정확히 한 번 처리해야 하는 경우 [Amazon SQS 선입선출(FIFO) 대기열을 사용할 수 있습니다](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-fifo-queues.html).

항공편 테이블 업데이트가 실패하거나 아웃박스 테이블 업데이트가 실패할 경우, 전체 트랜잭션이 롤백되므로 다운스트림 데이터 불일치가 발생하지 않습니다.

![\[다운스트림 데이터 불일치가 없는 롤백\]](http://docs.aws.amazon.com/ko_kr/prescriptive-guidance/latest/cloud-design-patterns/images/outbox-5.png)


다음 다이어그램에서는 Amazon RDS 데이터베이스를 사용하여 트랜잭션 아웃박스 아키텍처를 구현했습니다. 이벤트 처리 서비스는 아웃박스 테이블을 읽을 때 커밋된 (성공적인) 트랜잭션에 포함된 행만 인식한 다음, 해당 이벤트에 대한 메시지를 SQS 대기열에 추가합니다. 결제 서비스가 추가 처리를 위해 이 대기열을 읽습니다. 이 설계 방식은 이중 쓰기 작업 문제를 해결하고 타임스탬프와 시퀀스 번호를 사용하여 메시지와 이벤트의 순서를 유지합니다.

![\[이중 쓰기 작업 문제를 해결하는 설계\]](http://docs.aws.amazon.com/ko_kr/prescriptive-guidance/latest/cloud-design-patterns/images/outbox-6.png)


#### 변경 데이터 캡처(CDC) 사용
<a name="transactional-implementation-cdc"></a>

일부 데이터베이스는 변경된 데이터를 캡처하기 위한 항목 수준 수정 게시 기능을 지원합니다. 변경된 항목을 식별하고 그에 따라 이벤트 알림을 보낼 수 있습니다. 이렇게 하면 업데이트를 추적하기 위해 다른 테이블을 만드는 데 따른 오버헤드를 줄일 수 있습니다. 항공편 서비스가 시작한 이벤트는 동일한 항목의 다른 속성에 저장됩니다.

[Amazon DynamoDB](https://aws.amazon.com/dynamodb/)는 CDC 업데이트를 지원하는 키-값 NoSQL 데이터베이스입니다. 다음 시퀀스 다이어그램에서 DynamoDB는 Amazon DynamoDB Streams에 항목 수준 수정 사항을 게시합니다. 이벤트 처리 서비스는 스트림에서 데이터를 읽고 추가 처리를 위해 이벤트 알림을 결제 서비스에 게시합니다.

![\[DynamoDB 및 DynamoDB Streams를 사용한 트랜잭션 아웃박스\]](http://docs.aws.amazon.com/ko_kr/prescriptive-guidance/latest/cloud-design-patterns/images/outbox-7.png)


DynamoDB Streams는 시간 순서에 따라 DynamoDB 테이블의 항목 수준 변경 사항과 관련된 정보의 흐름을 캡처합니다.

DynamoDB 테이블에서 스트림을 활성화하여 트랜잭션 아웃박스 패턴을 구현할 수 있습니다. 이벤트 처리 서비스를 위한 Lambda 함수가 이러한 스트림과 연결됩니다.
+ 항공편 테이블이 업데이트되면 DynamoDB Streams가 변경된 데이터를 캡처하고 이벤트 처리 서비스가 스트림을 폴링하여 새 레코드를 찾습니다.
+ 새 스트림 레코드를 사용할 수 있게 되면 Lambda 함수는 추가 처리를 위해 이벤트에 대한 메시지를 동기식으로 SQS 대기열에 추가합니다. DynamoDB 항목에 속성을 추가하여 필요에 따라 타임스탬프와 시퀀스 번호를 캡처함으로써 구현의 안정성을 개선할 수 있습니다.

![\[CDC를 사용한 트랜잭션 아웃박스\]](http://docs.aws.amazon.com/ko_kr/prescriptive-guidance/latest/cloud-design-patterns/images/outbox-8.png)


## 샘플 코드
<a name="transactional-implementation-code"></a>

### 아웃박스 테이블 사용
<a name="transactional-implementation-code-outbox"></a>

이 섹션의 샘플 코드는 아웃박스 테이블을 사용하여 트랜잭션 아웃박스 패턴을 구현하는 방법을 보여줍니다. 전체 코드를 보려면 이 예의 [GitHub](https://github.com/aws-samples/transactional-outbox-pattern) 리포지토리를 참조하세요.

다음 코드 조각은 데이터베이스의 `Flight` 엔터티와 `Flight` 이벤트를 단일 트랜잭션 내의 각 테이블에 저장합니다.

```
@PostMapping("/flights")
    @Transactional
    public Flight createFlight(@Valid @RequestBody Flight flight) {
        Flight savedFlight = flightRepository.save(flight);
        JsonNode flightPayload = objectMapper.convertValue(flight, JsonNode.class);
        FlightOutbox outboxEvent = new FlightOutbox(flight.getId().toString(), FlightOutbox.EventType.FLIGHT_BOOKED,
                flightPayload);
        outboxRepository.save(outboxEvent);
        return savedFlight;
    }
```

아웃박스 테이블에서 새 이벤트를 주기적으로 스캔하여 Amazon SQS로 전송하고, Amazon SQS가 성공적으로 응답하면 테이블에서 해당 이벤트를 삭제하는 작업은 별도의 서비스가 담당합니다. 폴링 속도는 `application.properties` 파일에서 구성할 수 있습니다.

```
@Scheduled(fixedDelayString = "${sqs.polling_ms}")
    public void forwardEventsToSQS() {
        List<FlightOutbox> entities = outboxRepository.findAllByOrderByIdAsc(Pageable.ofSize(batchSize)).toList();
        if (!entities.isEmpty()) {
            GetQueueUrlRequest getQueueRequest = GetQueueUrlRequest.builder()
                    .queueName(sqsQueueName)
                    .build();
            String queueUrl = this.sqsClient.getQueueUrl(getQueueRequest).queueUrl();
            List<SendMessageBatchRequestEntry> messageEntries = new ArrayList<>();
            entities.forEach(entity -> messageEntries.add(SendMessageBatchRequestEntry.builder()
                    .id(entity.getId().toString())
                    .messageGroupId(entity.getAggregateId())
                    .messageDeduplicationId(entity.getId().toString())
                    .messageBody(entity.getPayload().toString())
                    .build())
            );
            SendMessageBatchRequest sendMessageBatchRequest = SendMessageBatchRequest.builder()
                    .queueUrl(queueUrl)
                    .entries(messageEntries)
                    .build();
            sqsClient.sendMessageBatch(sendMessageBatchRequest);
            outboxRepository.deleteAllInBatch(entities);
        }
    }
```

### 변경 데이터 캡처(CDC) 사용
<a name="transactional-implementation-code-cdc"></a>

이 섹션의 샘플 코드에서는 DynamoDB의 변경 데이터 캡처(CDC) 기능을 사용하여 트랜잭션 기반 아웃박스 패턴을 구현하는 방법을 보여줍니다. 전체 코드를 보려면 이 예의 [GitHub](https://github.com/aws-samples/transactional-outbox-pattern) 리포지토리를 참조하세요.

다음 AWS Cloud Development Kit (AWS CDK) 코드 조각은 DynamoDB 항공편 테이블과 Amazon Kinesis 데이터 스트림(`cdcStream`)을 생성하고 모든 업데이트를 스트림으로 전송하도록 항공편 테이블을 구성합니다.

```
Const cdcStream = new kinesis.Stream(this, 'flightsCDCStream', {
    streamName: 'flightsCDCStream'
})

const flightTable = new dynamodb.Table(this, 'flight', {
    tableName: 'flight',
    kinesisStream: cdcStream,
    partitionKey: {
        name: 'id',
        type: dynamodb.AttributeType.STRING,
    }

});
```

다음 코드 조각 및 구성에서는 Kinesis 스트림에서 업데이트를 선택하고 추가 처리를 위해 이러한 이벤트를 SQS 대기열에 전달하는 스프링 클라우드 스트림 함수를 정의합니다.

```
applications.properties
spring.cloud.stream.bindings.sendToSQS-in-0.destination=${kinesisstreamname}
spring.cloud.stream.bindings.sendToSQS-in-0.content-type=application/ddb
    
QueueService.java
@Bean
public Consumer<Flight> sendToSQS() {
    return this::forwardEventsToSQS;
}

public void forwardEventsToSQS(Flight flight) {
    GetQueueUrlRequest getQueueRequest = GetQueueUrlRequest.builder()
            .queueName(sqsQueueName)
            .build();
    String queueUrl = this.sqsClient.getQueueUrl(getQueueRequest).queueUrl();
    try {
        SendMessageRequest send_msg_request = SendMessageRequest.builder()
                .queueUrl(queueUrl)
                .messageBody(objectMapper.writeValueAsString(flight))
                .messageGroupId("1")
                .messageDeduplicationId(flight.getId().toString())
                .build();
        sqsClient.sendMessage(send_msg_request);
    } catch (IOException | AmazonServiceException e) {
        logger.error("Error sending message to SQS", e);
    }
}
```

## GitHub 리포지토리
<a name="transactional-implementation-github-repo"></a>

이 패턴의 샘플 아키텍처를 완전히 구현하려면 [https://github.com/aws-samples/transactional-outbox-pattern](https://github.com/aws-samples/transactional-outbox-pattern)에서 GitHub 리포지토리를 참조하세요.