Good Code - 트랜잭션과 MQ
Good Code - 트랜잭션과 MQ
사실, 이건 Good Code 라기보다는 100% 사고날 수 있는 위험한? 코드이다.
@Transactional
public void something() {
var entity = ...;
entityRepository.save(entity);
kafkaproducer.send(new EntityCreatedMessage(...));
}@KafkaListener(
...
)
public void consume(EntityCreatedMessage message) {
var entity = entityRepository.findById(message.getId()).orElseThrow();
...트랜잭션 내에서 메시지를 직접 발행하는 코드가 있다고 해보자. 이유라면?
- 요청과 메시지 발행을 트랜잭션으로 보장하고 싶다.
- 같은 맥락으로, 사용자에게 응답을 할 때 메시지가 발행된 걸 보장하고 싶다.
사용자가 응답할 때, 메시지를 발행하는걸 보장하지 않으면 귀찮아진다...
무조건, 메시지가 발행하는걸 보장하게 스케줄러에 스케줄러를 구현한다던가, 아웃박스 패턴을 따르다던가, 상대방이 알아차릴 수 있게 핸들링이 필요하던가
등이 있을수 있다.
하지만? 상당한 안티 코드다. 메시지는 발행하면, 바로 Consume 을 할 수 있다.
즉, 트랜잭션이 끝나기 전에 메시지를 받아서 반영이 되지 않은 데이터를 수정할 수 있다.
EX) 엔티티 저장 로직이라면 not found 가 뜰수도, 수정 로직이라면 수정이 안된 상태를 조회할수도 있다.
해결 방법
TransactionalEventListener + EventPublisher
서비스 로직에서 메시지를 바로 발행하는게 아니라
eventPublisher.publishEvent(new EntityCreatedEvent());@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void onEntityCreatedEvent(EntityCreatedEvent event) {
kafkaTemplate.send(...);이벤트 발행 후, 커밋이 된 후 이벤트를 수신해서 발행하자.
즉, 메시지가 발행되는걸 트랜잭션이 끝난 이후 발행하는걸 보장한 것.
Async 를 사용하지 않으면, 위와 동일하게 발행 로직도 동기로 진행되며 메시지를 보장할 수 있다.
No Transactional
트랜잭션 어노테이션을 사용하지 않는 것이다! ☠️
코드의 상황에 따라 다르겠지만?
- 단순 조회
- 조회 및 단일 INSERT or UPDATE
라면, 트랜잭션을 선언하지 않고도 트랜잭션과 동일하게 동작하는걸 기대할 수 있다.
단, 여러 번 조회할 때 데이터 일관성이 깨지면 안되는 요소라면 트랜잭션을 빼기 어려울 수 있다.
트랜잭션이 없으니, save 구문에서 바로 쿼리가 나가서 DB 에 반영이 되고,
그 다음 메시지가 MQ 에 바로 반영이 된다.
메소드에 습관적으로 트랜잭션 어노테이션을 붙일 수 있지만 꼭 그럴 필요는 없다.
자기가 의식적으로 제어해서 사용해 위와같은 상황에서 현명하게 처리할 수도 있다.

- 참고 아티클 : 카카오페이 기술 블로그
일반적으로, 고트래픽을 기대한다면 당연하게도
Outbox Pattern + 비동기를 기반으로 메시지를 발행해서 사용할 수 있을것이다. (아마..?)
그러지 않고, 단순하게 or 지금 인프라에 맞게 해야한다면 위 패턴을 주의하자!