문제 상황
실시간 경매 시스템에서 메인 서버가 경매를 생성하고, WebSocket 서버에 경매방을 만들도록 요청하는 구조였다.
그런데 다음과 같은 문제가 발생했다:
❗ 메인 서버에서 경매 생성이 실패했는데도, WebSocket 서버에는 방이 생성돼버린다.
- 경매를 생성할 때 유효성 검증 오류 등으로 DB에 저장이 되지 않았음
- 하지만 그 시점에서 이미 WebSocket 서버에 경매방 생성 요청이 전달되어 실행됨
- 이후 다시 요청을 보내면 auctionId가 달라져 두 서버 간 auctionId 불일치 문제 발생
원인 분석
문제의 핵심은 단순했다.
Auction saveAuction = auctionRepository.save(auction);
// ❌ 이 시점엔 아직 트랜잭션 커밋되지 않았음
webSocketClient.createAuctionRoom(...);
즉, @Transactional 메서드 내부에서 DB 커밋이 완료되기도 전에
WebSocket 서버에 방 생성 요청을 보내버리는 구조였다.
만약 DB 저장이 실패하거나 롤백되면? → WebSocket 서버에는 유령 경매방이 생겨버리는 상황
해결 방법 : @TransactionalEventListener 활용
Spring에서는 트랜잭션 커밋이 완료된 이후에만 실행되는 후처리 이벤트 리스너를 제공한다.
이를 활용해 다음과 같은 구조로 리팩토링했다.
이벤트 클래스 정의
@Getter
public class AuctionCreatedEvent {
private final Auction auction;
public AuctionCreatedEvent(Auction auction) {
this.auction = auction;
}
}
경매 생성 완료 시 Auction 객체를 담아 이벤트로 전달할 수 있도록 만든 단순 DTO
경매 생성 서비스에서 이벤트 발행 (AuctionService)
@Transactional
public AuctionSaveResponse createAuction(...) {
Auction savedAuction = auctionRepository.save(auction);
// ✅ 트랜잭션 커밋 후 처리될 이벤트 발행
eventPublisher.publishEvent(new AuctionCreatedEvent(savedAuction));
return new AuctionSaveResponse(...);
}
기존에 들어있던 webSocketClient.createAuctionRoom()호출은 제거하고,
대신 이벤트를 발행함.
이 시점에는 아직 트랜잭션 커밋 전이므로, 실제 요청은 바로 나가지 않음.
트랜잭션 커밋 후 실행되는 리스너 작성
@Component
@RequiredArgsConstructor
public class AuctionEventListener {
private final WebSocketClient webSocketClient;
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handleAuctionCreated(AuctionCreatedEvent event) {
Auction auction = event.getAuction();
log.info("✅ 트랜잭션 커밋 완료 → 웹소켓 서버에 경매방 생성 요청");
webSocketClient.createAuctionRoom(
new WebSocketAuctionCreateRequest(
auction.getId(),
auction.getProduct().getProductName(),
auction.getMinPrice(),
auction.getStartTime(),
auction.getEndTime()
)
);
}
}
이 리스너는DB에 경매가 진짜 저장된 후에만 동작하므로,
유령 경매방이 생성되는 일을 완전히 방지할 수 있다.
WebSocketClient 클래스는 기존 그대로 사용
public void createAuctionRoom(WebSocketAuctionCreateRequest request) {
webClient.post()
.uri(websocketUrl + "/internal/auction/join")
.bodyValue(request)
.retrieve()
.bodyToMono(Void.class)
.block(); // 동기로 요청
}
WebSocket 서버에 HTTP 요청을 보내 경매방을 생성하는 로직
전체 흐름 요약
[메인 서버에서 경매 생성 요청]
↓
AuctionService.createAuction()
↓
DB에 저장 (아직 커밋 전)
↓
이벤트 발행 (publishEvent)
↓
트랜잭션 커밋 성공
↓
@TransactionEventListener 실행
↓
WebSocket 서버에 방 생성 요청
회고
- 분산 구조에서 한 서버의 결과를 다른 서버에 반영해야 할 때는
- 트랜잭션 커밋 전후를 구분하는 것이 매우 중요하다.
- Spring의 @TransactionalEventListener는 이런 케이스에서 매우 유용하며,
- 시스템 안정성을 높이고 코드의 관심사 분리도 가능하게 한다.
'내일배움캠프' 카테고리의 다른 글
| Redis를 활용한 스케줄링 서버 구현 (0) | 2025.04.30 |
|---|---|
| WebSocket을 활용한 실시간 경매 서버 구현 (0) | 2025.04.29 |