‼️ 예제 소스코드는 이해 편의성을 위해 간략하게 요약했습니다.

들어가기 전

“미팅 참가 기능”을 RabbitMQ를 도입해 비동기 방식으로 전환하면서 성능을 개선할 수 있었습니다. 그러나 메시지 큐를 활용한 비동기 처리에서는 서비스 로직과 메시지 발행을 하나의 원자적 단위로 묶는 것이 중요합니다. 또 발행이 실패할 경우 이를 감지하고 책임 있게 재발행을 보장해야 합니다.

이번 글에서는 저희 프로젝트에서 비동기 전환 이후 트랜잭셔널 메시징을 구현한 방식을 소개하고자 합니다.

문제 파악

	@Transactional
	public ParticipantCreateResponseDto createParticipant(Long userId, Long meetingId) {
		
		// 1. 미팅 참가자 데이터 저장 
		MeetingParticipant participant = MeetingParticipant.createParticipant(meeting, userId);
		meeting.addMeetingParticipant(participant);

		// 2. RabbitMQ 메시지 발행
		rabbitTemplate.convertAndSend(...);

		// createParticipant 에서는 이벤트 발행 까지만 진행
		return new ParticipantCreateResponseDto("SUCCESS", "신청 완료");
	}

RabbitMQ로 비동기 전환 후, 메시지가 Message Broker에 전달되는 과정에서 유실되는 문제와 메시지 발행 이후 DB가 롤백되었음에도 불구하고 발행된 메시지를 되돌리지 못하는 문제를 확인했습니다. 이는 곧 데이터 정합성 문제로 이어집니다.

예를 들어, 메시지가 유실되면 사용자는 참가 신청을 완료했지만 결제 처리나 알림 전송이 누락될 수 있습니다. 반대로 DB는 롤백되었는데 메시지가 발행된 경우, 실제로는 참가자가 추가되지 않았음에도 결제가 완료되는 불일치가 발생할 수 있습니다.

문제 해결

이러한 문제를 해결하기 위해 총 세 가지 방식을 조합해 트랜잭셔널 메시징을 구현하였습니다.

  1. Spring Event의 AFTER_COMMIT
  2. Transactional Outbox 패턴
  3. RabbitMQ의 publisher Confirm

Spring Event의 AFTER_COMMIT

	@Transactional
	public ParticipantCreateResponseDto createParticipant(Long userId, Long meetingId) {
		
		// 1. 미팅 참가자 데이터 저장 
		MeetingParticipant participant = MeetingParticipant.createParticipant(meeting, userId);
		meeting.addMeetingParticipant(participant);

		// 2. 이벤트 발행
		eventPublisher.publishEvent(...);

		// createParticipant 에서는 이벤트 발행 까지만 진행
		return new ParticipantCreateResponseDto("SUCCESS", "신청 완료");
	}

	@TransactionalEventListener(AFTER_COIMMIT)
	public void listen(Dto dto){
		// 1. DB 커밋 이후 메시지 발행 시도
		rabbitTemplate.convertAndSend(dto);
	}

Spring EventAFTER_COMMIT 은 트랜잭션이 실제로 커밋된 이후에만 이벤트를 발행하도록 보장합니다.