Spring 은 @Transactional
이 붙어있다고 해서, 메서드 내 모든 코드가 "커밋 이후" 에 실행되는 것은 아니다.
스프링의 트랜잭션은
- 메서드 본문 전체를 하나의 트랜잭션 경계 안에서 실행
- 메서드가 정상 종료(return)되면 커밋
순으로 동작하기 때문에, 메서드 안에서 broadcast 호출을 바로 하면 커밋 전에 실행된다.
이 상태에서 커밋이 실패하거나 롤백되면, 이미 브로드캐스트된 메시지는 롤백되지 않아 데이터 불일치가 발생한다.
따라서 “DB 커밋이 정말 완료된 후에만 브로드캐스트를 보내라”는 보장을 하려면
TransactionSynchronizationManager.registerSynchronization(...)
ApplicationEventPublisher.publishEvent(...)
+ @TransactionalEventListener(phase = AFTER_COMMIT)
중 하나를 사용해야 한다.
@Transactional
은 “이 메서드가 하나의 트랜잭션 범위 안에서 실행” 만 보장TransactionSynchronization
을 등록하거나@TransactionalEventListener(phase = AFTER_COMMIT)
로 처리해야 함이렇게 해 두면, 브로드캐스트 이벤트가 트랜잭션 커밋 성공 시에만 실행되어서,
메시지와 DB 상태 간의 일관성이 완벽히 유지된다.
/** 퇴장(본인 나가기) */
@Transactional
public void leaveChatRoom(Long roomId, Long userId) {
ChatParticipant participant = chatParticipantRepository.findActiveByRoomAndUser(roomId, userId)
.orElseThrow(() -> new ChatException(ChatErrorCode.USER_NOT_IN_CHAT_ROOM));
participant.leave();
// 실시간 멤버 브로드캐스트
chatMemberEventService.broadcastMembers(roomId);
}
participant.leave()
호출 후 flush 되기 전에 바로 브로드캐스트하므로 위와 동일한 불일치 가능성 있음/**
* 퇴장(본인 나가기)
*
* @param groupId 모임 ID
* @param roomId 채팅방 ID
* @param userId 요청 사용자 ID
*/
public void leaveChatRoom(Long groupId, Long roomId, Long userId) {
// 1) 모임 멤버 여부 검증
groupValidator.validateMember(userId, groupId);
// 2) 채팅방 존재 여부 검증
ChatRoom room = chatValidator.validateRoom(roomId);
// 3) 해당 방의 참가자 여부 검증 및 조회
ChatParticipant participant =
chatValidator.validateParticipant(roomId, userId);
// 4) 참가 비활성 처리
participant.leave();
// 5) 트랜잭션 커밋 후 실시간 멤버 브로드캐스트
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCommit() {
chatMemberEventService.broadcastMembers(roomId);
}
});
}
TransactionSynchronization
직접 구현하는 방법 사용
TransactionSynchronization
인터페이스에 기본 메서드(default method)가 구현되어 있어, 어댑터 클래스를 상속할 필요 없이 바로 익명 구현으로 쓸 수 있음