AOP와 Spring Event를 활용한 결제 이력 추적 시스템 구현

실행 환경: Java 21, Spring Boot 3.3.3

배경 및 문제 정의

결제 복구 시스템을 구축하면서 결제 상태가 복잡해지면서 다양한 상태와 재시도 메커니즘이 추가되면서 상태의 추적이 필요해졌다. 이러한 상태 전환을 단순 로그로만 남길 경우 다음과 같은 한계가 존재했다.

  • 구조화되지 않은 로그 데이터로 인해 집계 및 분석이 어려움

  • 트랜잭션 단위로 상태 변경의 정합성을 보장할 수 없음

  • 수동 로그 해석에 따른 운영 효율성 저하

따라서, 구조화된 상태 변경 이력을 별도 테이블에 저장하고, 결제 이벤트를 시간순으로 추적할 수 있는 시스템을 구축할 필요가 있었다.

개발 목표 및 설계 원칙

이번 기능 개발에 있어, 아래의 항목들을 충족시키는 것이 핵심 목표였다.

  • 완전성: 모든 상태 전환이 누락 없이 기록

  • 추적 가능성: 단일 결제 건의 모든 상태 전환을 시간순으로 연결·조회 가능

  • 관심사 분리: 비즈니스 로직에 침투하지 않고 선언적으로 이력 추적 기능 추가

기술 검토 및 설계 결정

기술 대안 검토

대안
장점
한계

Repository 직접 호출

구현이 단순하고 직관적임

비즈니스 로직에 침투하여 결합도가 높아짐

메시지 큐 (Kafka 등)

비동기 확장성 및 시스템 독립성 확보 가능

단일 애플리케이션 내에서는 과도한 복잡도 및 운영 부담 발생

최종 선정: AOP + Spring Event 기반 설계

최종적으로 AOP + Spring Event을 사용하고, 커밋 직전(BEFORE_COMMIT)에 이력 저장을 수행하는 방안을 채택했다.

  • AOP (Aspect-Oriented Programming)

    • 선정 이유: 상태 변경 기록은 전형적인 횡단 관심사 -> 서비스 코드 변경 최소화 가능

    • 장점: 어노테이션 기반 선언적 프로그래밍으로 적용 누락 최소화

      • Repository 직접 호출 대비 결합도 낮음

  • Spring ApplicationEvent

    • 선정 이유: 프레임워크 내장 이벤트 시스템 활용 -> 외부 라이브러리 불필요

    • 장점: 이벤트 발행(서비스)과 처리(저장) 분리 -> 결합도 낮춤

      • 메시지 큐 대비 인프라 비용 추가 없이 구현 가능

  • TransactionPhase.BEFORE_COMMIT

    • 선정 이유: 커밋 직전에 실행하여 비즈니스 상태와 이력 간 정합성 확보

    • 장점: 로직이 성공한 경우에만 기록

      • AFTER_COMMIT은 커밋 후 저장 실패하더라도, 비즈니스 로직이 이미 커밋된 상태가 되어 불일치 발생하는 문제 발생

      • 히스토리 테이블의 변경 이력 == 실제 비즈니스 상태 변경을 보장

시스템 아키텍처

Service Method (@PublishPaymentHistory)

AOP Aspect

ApplicationEvent 발행

@TransactionalEventListener (BEFORE_COMMIT)

PaymentHistory 저장

Payment 상태 변경

구현 세부 사항

1. 이력 추적 어노테이션 적용

비즈니스 서비스 메서드에 @PublishPaymentHistory 어노테이션을 선언하여, 해당 메서드 실행 시 자동으로 상태 변경 이력을 발행하도록 설정했다.


@Transactional
@PublishPaymentHistory(action = "changed")
public PaymentEvent markPaymentAsFail(
        PaymentEvent paymentEvent,
        @Reason String failureReason // 이력에 기록할 사유
) {
    // 순수한 비즈니스 로직 수행
    paymentEvent.fail(failureReason);
    return paymentEventRepository.saveOrUpdate(paymentEvent);
}
  • @PublishPaymentHistory 어노테이션으로 이력 추적 대상 메서드 지정 및 action 속성으로 상태 변경 유형 전달

  • @Reason 어노테이션으로 이력 기록에 포함할 변경 사유 전달

2. AOP Aspect - 상태 변경 감지

AOP Aspect에서는 메서드 실행 전의 상태를 저장하고, 변경 이력을 담은 이벤트를 발행한다.


@Around("@annotation(publishHistory)")
public Object publishHistoryEvent(ProceedingJoinPoint joinPoint, PublishPaymentHistory publishHistory)
        throws Throwable {
    // 1. 실행 전 상태 캡처
    Optional<PaymentEvent> beforeEventOpt = findPaymentEvent(joinPoint.getArgs());
    PaymentEventStatus beforeStatus = beforeEventOpt.map(PaymentEvent::getStatus).orElse(null);

    // 2. 이력에 기록할 사유 추출
    String reason = findReasonParameter(joinPoint);

    try {
        // 3. 비즈니스 로직 실행
        Object result = joinPoint.proceed();

        // 4. 실행 결과 기반으로 상태 변경 이벤트 발행
        processResultAndPublishEvent(beforeStatus, result, reason, publishHistory);

        return result;
    } catch (Exception e) {
        log.error("Error occurred while processing payment: {}", e.getMessage(), e);
        throw e;
    }
}
  • findPaymentEvent 메서드로 현재 상태를 조회하여 이전 상태 저장

  • findReasonParameter로 이력에 포함할 변경 사유 추출

  • 비즈니스 로직 실행 후 action에 따라 적절한 이벤트 발행

3. 이벤트 리스너 - BEFORE_COMMIT 전략

Spring의 @TransactionalEventListener를 이용해, 트랜잭션 커밋 직전인 TransactionPhase.BEFORE_COMMIT 단계에서 이력 저장 로직을 실행한다.


@TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
public void handlePaymentHistoryEvent(PaymentHistoryEvent event) {
    paymentHistoryService.recordPaymentHistory(event);
}
  • BEFORE_COMMIT 단계는 트랜잭션이 성공적으로 커밋되기 직전에 실행되어, 비즈니스 상태 변경과 이력 기록이 동일 트랜잭션 내에서 처리되도록 보장

  • 상태 변경과 이력 저장 간 불일치 문제를 방지하며, 데이터 정합성과 원자성을 확보

결론

이번 프로젝트를 통해 AOP와 Spring Event를 활용한 결제 상태 이력 추적 시스템을 성공적으로 설계하고 구현함으로써, 다음과 같은 성과를 달성할 수 있었다.

  • 복잡한 결제 상태 변경의 체계적 관리: 다양한 결제 상태와 재시도 로직이 혼재하는 환경에서도 모든 상태 변경 내역을 누락 없이 기록하여 장애 분석 및 추적 가능

  • 비즈니스 로직의 순수성 및 유지보수성 향상: 이력 추적 기능이 AOP를 통한 선언적 방식으로 분리되어, 기존 비즈니스 서비스 코드에 침투하지 않고 기능 추가

  • 트랜잭션 정합성 확보 및 데이터 신뢰성 강화: 트랜잭션 커밋 직전 단계에서 이력 저장을 수행함으로써, 상태 변경과 이력 기록 간 불일치 문제 방지

Payment Event 히스토리 리스트 조회
Payment Event 상세 조회

Last updated

Was this helpful?