Spring Boot

[Spring] Transaction 분리를 통한 성능 개선

kittity 2024. 12. 27. 23:54
목차
1. Transaction 분리의 필요성
2. Transaction 분리로 발생할 수 있는 문제와 해결방안
3. 콘서트 예약 서비스에 적용해보기
    3.1 Transaction 분리1. 좌석예약
    3.2 Transaction 분리2. 결제

 

1. Transaction 분리의 필요성

Transaction의 범위에 따라 다음과 같이 성능에 영향을 줄 수 있다. 따라서 Transaction을 분리할 필요가 있다.

1. Transaction 범위가 불필요하게 많거나 느린 쿼리가 포함되어 있는 경우

  • 트랜잭션 내에서 Lock을 갖고 있다면 다른 사용자는 트랜잭션이 끝날때까지 대기해야한다. 그리고 데드락이 발생할 수도 있다.
  • 다수의 느린 쿼리가 포함된 작업은 요청 처리에 영향을 줄 수 있다.
  • 긴 생명 주기의 Transaction 의 경우, 오랜 시간은 소요되나 후속 작업에 의해 전체 트랜잭션이 실패할 수 있다.

 

2. Transaction 범위 내에서 DB 와 무관한 작업이 포함된 경우

  • DB의 작업은 완료되거나 오래 걸리지 않는데 API 요청과 같은 DB 외적인 작업이 오래 걸리게 되면 Transaction 이 길어지는 문제가 있다.
  • 외부 API 호출에 실패할 경우 롤백된다. 그러나 이미 완료 되었기 때문에 롤백할 필요가 없다.
  • 예를 들어, 좌석 예약, 결제 등이 완료 되었을 때 데이터 플랫폼에 전달하거나 이력을 저장하는 경우가 있다.

 

이러한 케이스를 트랜잭션을 분리하고 Transactional Outbox Pattern을 적용하고 이벤트를 발행하도록하여 개선할 수 있으며, 다음과 같은 방법들을 사용할 수 있다.

  • ApplicationEventPublisher & EventListner / TransactionEventListner
  • Kafka 비동기 메시지 통신

 

2. Transaction 분리로 발생할 수 있는 문제와 해결방안

1. 이벤트에 의한 작업이 실패가 영향을 미치는 경우

  • 만약 이벤트에 의한 작업이 실패할 경우 원본 작업도 실패 처리를 해야한다면 이를 위한 처리가 필요히다.
  • 이를 위해 SAGA 패턴과 보상 트랜잭션을 통해 처리할 수 있으며, 이를 통해 분산환경에서도 처리가 가능하다.


2. 트랜잭션에서 이벤트 발행도록 적용한 후 이벤트 발행시 처리하는 로직이 실패할 경우

  • 이벤트 발행시 처리되는 로직이 실패할 경우, 만약 반드시 수행해야하는 로직이라면 이에 대한 처리가 필요하다.

 

3. 콘서트 예약 서비스에 적용해보기

콘서트 예약 서비스에 Transaction을 분리할 수 있는 부분을 파악하고 개선해 보았습니다.

 

3.1 Transaction 분리1. 좌석예약

좌석예약의 경우 예약이 완료될 경우 알림톡 등을 전송할 수 있습니다. 이 경우 외부 API를 통해 예약 완료 메시지를 전달해야하므로 다음과 같이 개선할 수 있습니다.

 

  • 기존 로직
    ReservationFacade {
    	reservationService.분산락을_이용한_예약 ()
    }
    
    ReservationService {
        분산락을_이용한_예약 () {
            락_획득 ()
            concertReservationService.좌석_예약 ()
        } 
    }
    
    @Transactional
    ConcertReservationService {
        좌석_예약 () {
            1. seatId로 좌석 조회 (낙관적락)
            2. 좌석 상태 변경 저장 (AVAILABLE -> TEMPORARY_RESERVED)
            3. 예약 내역 저장 (Reservation)
        }
    }

 

  • 개선 로직
    ReservationFacade {
    	reservationService.분산락을_이용한_예약 ()
    }
    
    ReservationService {
        분산락을_이용한_예약 () {
            락_획득 ()
            concertReservationService.좌석_예약 ()
        } 
    }
    
    @Transactional
    ConcertReservationService {
        좌석_예약 () {
            1. 좌석_조회 (낙관적락)
            2. 좌석_상태변경 (AVAILABLE -> TEMPORARY_RESERVED)
            3. 예약내역_저장 (Reservation)
            4. 예약완료_이벤트발행 ()
        }
    }
    
    @Component
    ReservationEventPublisher {
        successReservation(ReservationInfo reservationInfo){
    		applicationEventPublisher.publishEvent(reservationInfo);
        }
    }
    
    @Component
    ReservationEventListener {
        @Async
        @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
        reservationSuccessHandler(ReservationInfo reservationInfo) {
    		// 예약 완료 메시지 전달 외부 API 
        }
    }

 

  • 로직 변경 후, 테스트 코드 실행 결과

 

3.2 Transaction 분리2. 결제

  • 기존 로직
    @Transactional 
    PaymentFacade {
        reservationService.예약정보_조회_및_검증 ()
        pointService.결제처리_비관적락_사용 ()
        paymentService.결제상태_완료처리 ()
        reservationService.예약상태_완료처리 ()
        concertService.좌석상태_완료처리 ()
        waitingQueueService.토큰_만료 ()
    }
    
    ReservationService {
        예약정보_조회_및_검증 ()
    
        @Transactional
        예약상태_완료처리 ()
    }
    
    PointService {
        @Transactional
        결제처리_비관적락_사용 () {
        }
    }
    
    PaymentService {
        @Transactional
        결제상태_완료처리 ()
    }
    
    ConcertService {
        @Transactional
        좌석상태_완료처리 ()
    }
    
    WaitingQueueService {
        토큰_만료 ()
    }

 

  • 개선 로직
    PaymentFacade {
    	paymentService.결제 ()
    }
    
    PaymentService {
        @Transactional
        결재() {
            reservationService.예약정보_조회_및_검증 ()
            pointService.결제처리_비관적락_사용 ()
    
            paymentService.결제상태_완료처리 ()
            reservationService.예약상태_완료처리 ()
            concertService.좌석상태_완료처리 ()
    
            결제완료_이벤트발행 ()
    
            waitingQueueService.토큰_만료 ()
        }
    
        @Transactional
        결제상태_완료처리 ()
    }
    
    @Component
    PaymentEventPublisher {
        successPayment(PaymentInfo paymentInfo){
        	applicationEventPublisher.publishEvent(paymentInfo);
        }
    }
    
    @Component
    PaymentEventListener {
        @Async
        @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
        paymentSuccessHandler(PaymentInfo paymentInfo) {
        	// 결제 완료 메시지 전달 외부 API 
        }
    }
    
    ReservationService {
        예약정보_조회_및_검증 ()
    
        @Transactional
        예약상태_완료처리 ()
    }
    
    PointService {
        @Transactional
        결제처리_비관적락_사용 () {
        }
    }
    
    ConcertService {
        @Transactional
        좌석상태_완료처리 ()
    }
    
    WaitingQueueService {
        토큰_만료 ()
    }

 

  • 로직 변경 후, 테스트 코드 실행 결과

 

 

관련 코드는 아래 GitHub에서 보실 수 있습니다.

 

 

GitHub - leehanna602/hhplus-server-construction

Contribute to leehanna602/hhplus-server-construction development by creating an account on GitHub.

github.com

 

728x90