RECENT POSTS

최근에 작성한 글

01 / 03

전체 글

전체 보기

[구현] 환불 5분 정책 구현하기(1)

주문 결제 후 5분 이내 환불 정책을 어떻게 구현할지 고민했다.

처음에는 환불 가능 여부를 시간이 지나면 자동으로 변경하기 위해 Spring Scheduler를 사용하는 방식을 생각했다.

예를 들면

  • 1분마다 Scheduler 실행
  • 환불 가능 시간이 지난 Payment 조회
  • availableRefund = false 변경

과 같은 방식이다.

하지만 이 방식에는 몇 가지 마음에 걸리는 점이 있었다.

  • 실제로 환불 요청이 없어도 계속 Scheduler가 실행된다는 것.
  • 모든 데이터를 주기적으로 검사해야 한다는 것.
  • 데이터가 많아질수록 불필요한 조회가 발생한다는 것.

그래서 다음으로는 Redis + 분산락을 사용하는 방법도 고민했다. (로그아웃 과정에서 Redis에 저장한 RefreshToken을 지우는 구조이기때문에 Redis를 사용하니까 그럼 이 방법도 가능하지 않나? 라는 생각..)

환불 요청이 동시에 들어오는 상황에서 하나의 결제 데이터만 수정되도록 분산락을 사용하면 되지 않을까 생각했다.

하지만 코치님께서 이런 피드백을 주셨다.

분산락은 같은 자원을 여러 서버에서 동시에 수정할 가능성이 있을 때 사용하는 기술이다.

지금 프로젝트에서는 오히려 Redis와 분산락을 사용하는 것이 오버엔지니어링이 될 수 있다.

DB Lock으로도 충분히 해결할 수 있다.

이 이야기를 듣고 단순 계산 로직으로 구현하는 것에 대해 생각해봤다...

현재시간 - 결제시간 <= 5분

만 계산하면 된다.

즉, 별도로 Scheduler를 돌려서 상태를 변경할 필요도 없고 Redis에 만료 시간을 저장할 필요도 없다.

환불 API가 호출되는 순간

  1. Payment 조회
  2. 결제 시간 확인
  3. 5분 이내인지 계산
  4. 가능하면 환불 진행
  5. 아니면 예외 발생

이 흐름이면 충분하다.

다만 동시에 여러 사용자가 같은 결제를 환불하려고 하면 중복 환불 문제가 발생할 수 있다.

이 부분은 DB Lock으로 해결하는 것이 더 적절해 보였다.

후보는 두 가지이다.

1. 비관적 락(Pessimistic Lock)

  • 조회와 동시에 Lock을 획득
  • 다른 트랜잭션은 대기
  • 동시 환불을 확실하게 막을 수 있음

장점

  • 구현이 단순하다.
  • 데이터 정합성이 높다.

단점

  • 락으로 인해 대기 시간이 발생할 수 있다.

2. 낙관적 락(Optimistic Lock)

  • version 컬럼을 이용
  • 마지막 저장 시 version 검사
  • 충돌이 발생하면 예외 처리

장점

  • 평소에는 락을 잡지 않아 성능이 좋다.

단점

  • 충돌 발생 시 재시도 로직이 필요하다.

현재 생각하는 구현 방향

현재 프로젝트에서는

  • 환불 요청이 매우 빈번하지 않고
  • 동일 주문에 대해 동시에 환불 요청이 들어오는 경우도 거의 없으며
  • 구현 난이도도 고려해야 한다.

따라서

환불 요청 시 결제 시간을 계산하여 환불 가능 여부를 판단하고, 동일 결제에 대한 중복 환불은 DB Lock(비관적 락 또는 낙관적 락)으로 처리하는 방식이 가장 적절하다고 판단했다.

 

이렇게 되면 테이블 구조에서는 availableRefund 같은 컬럼도 굳이 둘 필요가 없다..

boolean refundable =
    payment.getStatus() == PaymentStatus.SUCCESS &&
    payment.getPaidAt().plusMinutes(5).isAfter(LocalDateTime.now());

 

처럼 결제 시각(paidAt)과 현재 시간을 비교해서 계산하는 방식이 더 일관성을 유지하기 쉽우며 스케줄러로 상태를 변경할 필요도 없고, 별도의 상태 동기화 문제도 발생하지 않을 것 같다.