트랜잭션 아웃박스 패턴 - AWS 규범적 지침

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

트랜잭션 아웃박스 패턴

의도

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

목적

데이터베이스 업데이트 후 마이크로서비스가 이벤트 알림을 보내는 경우 데이터 일관성과 신뢰성을 보장하기 위해 이 두 작업이 원자적으로 실행되어야 합니다.

  • 데이터베이스 업데이트는 성공했지만 이벤트 알림이 실패할 경우 다운스트림 서비스는 변경 사항을 인식하지 못해 시스템이 일관되지 않은 상태가 될 수 있습니다.

  • 데이터베이스 업데이트에 실패했지만 이벤트 알림이 전송되면 데이터가 손상되어 시스템의 신뢰성에 영향을 미칠 수 있습니다.

적용 가능성

다음과 같은 경우 트랜잭션 아웃박스 패턴을 사용합니다.

  • 데이터베이스 업데이트에서 이벤트 알림이 시작되는 이벤트 기반 애플리케이션을 구축하고 있습니다.

  • 두 서비스를 포함하는 작업에서 원자성을 보장하고자 합니다.

  • 이벤트 소싱 패턴을 구현하고자 합니다.

문제 및 고려 사항

  • 중복 메시지: 이벤트 처리 서비스에서 중복된 메시지나 이벤트를 보낼 수 있으므로, 처리된 메시지를 추적하여 소비하는 서비스를 멱등 상태로 만드는 것이 좋습니다.

  • 알림 순서: 서비스에서 데이터베이스를 업데이트하는 순서와 동일한 순서로 메시지나 이벤트를 전송합니다. 이는 이벤트 저장소를 사용하여 데이터 저장소를 point-in-time 복구할 수 있는 이벤트 소싱 패턴에 매우 중요합니다. 순서가 올바르지 않으면 데이터 품질이 저하될 수 있습니다. 알림 순서가 유지되지 않으면 최종 일관성과 데이터베이스 롤백으로 인해 문제가 복잡해질 수 있습니다.

  • 트랜잭션 롤백: 트랜잭션이 롤백된 경우 이벤트 알림을 보내지 마세요.

  • 서비스 수준 트랜잭션 처리: 여러 서비스에 걸친 트랜잭션에서 데이터 스토어 업데이트가 요구되는 경우, Saga 오케스트레이션 패턴을 사용하여 데이터 스토어 전체의 데이터 무결성을 유지합니다.

구현

전반적인 아키텍처

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

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

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

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

AWS 서비스를 사용한 구현

시퀀스 다이어그램에서 패턴을 설명하기 위해 다음 다이어그램에 표시된 대로 다음 AWS 서비스를 사용합니다.

Amazon RDS 및 AWS Lambda Amazon SQS를 사용한 트랜잭션 아웃박스 패턴

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

커밋 작업 후 트랜잭션 실패

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

커밋 작업 후 트랜잭션 실패와 롤백

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

관계형 데이터베이스에 아웃박스 테이블 사용

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

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

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

다운스트림 데이터 불일치가 없는 롤백

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

이중 쓰기 작업 문제를 해결하는 설계

변경 데이터 캡처 (CDC) 사용

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

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

DynamoDB 및 DynamoDB Streams를 사용한 트랜잭션 아웃박스

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

DynamoDB 테이블에서 스트림을 활성화하여 트랜잭션 아웃박스 패턴을 구현할 수 있습니다. 이벤트 처리 서비스를 위한 Lambda 함수가 이러한 스트림과 연결됩니다.

  • 항공편 테이블이 업데이트되면 DynamoDB Streams가 변경된 데이터를 캡처하고 이벤트 처리 서비스가 스트림을 폴링하여 새 레코드를 찾습니다.

  • 새 스트림 레코드를 사용할 수 있게 되면 Lambda 함수는 추가 처리를 위해 이벤트에 대한 메시지를 동기식으로 SQS 대기열에 추가합니다. DynamoDB 항목에 속성을 추가하여 필요에 따라 타임스탬프와 시퀀스 번호를 캡처함으로써 구현의 안정성을 개선할 수 있습니다.

CDC를 사용한 트랜잭션 아웃박스

샘플 코드

아웃박스 테이블 사용

이 섹션의 샘플 코드는 아웃박스 테이블을 사용하여 트랜잭션 아웃박스 패턴을 구현하는 방법을 보여줍니다. 전체 코드를 보려면 이 예제의 GitHub리포지토리를 참조하십시오.

다음 코드 조각은 데이터베이스의 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) 사용

이 섹션의 샘플 코드는 DynamoDB의 변경 데이터 캡처 (CDC) 기능을 사용하여 트랜잭션 아웃박스 패턴을 구현하는 방법을 보여줍니다. 전체 코드를 보려면 이 예제의 리포지토리를 참조하십시오. GitHub

다음 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 리포지토리

이 패턴의 샘플 아키텍처를 완전히 구현하려면 https://github.com/aws-samples/ GitHub 리포지토리를 참조하십시오transactional-outbox-pattern.