왜 결제했는데 적용이 안돼요?

혁콩·2024년 8월 29일

모두의 음악

목록 보기
14/17
post-thumbnail

들어가기에 앞서

이전 두 포스트에선 MSA 환경에서 결제를 담당하는 payment-service 에 결제를 요청, 처리하는 과정에 대해 알아봤어요.

이번 포스트에선 결제 처리 과정에서 발생할 수 있는 실패 시나리오를 생각해보고, 어떤 방식으로 해결할지에 대해 적어볼까 해요.

시나리오

1. 결제 요청 실패

group-service 가 어떠한 이유로 결제 요청 이벤트 발행에 실패했을 경우입니다. Kafka에 문제가 생겼거나, group-service 내부 로직에서 예외가 발생하는 등 메시지가 미들웨어에 도착하기 전 문제가 발생한 경우죠.

이 경우, 크리티컬한 문제가 아니라고 생각했습니다. 결제 프로세스 자체가 진행되지 않을 뿐더러, group-service 에서 재시도 로직을 진행하던가, 진행되지 않았다고 알려주기만 한다면 사용자가 얼마든지 재시도할 수 있을 거예요.

물론 UX에는 안좋겠지만..!

2. 결제 실패


결제 요청 이벤트 가 성공적으로 발행된 후 실패한 경우입니다. 아래 상황들을 고려했어요.

1. 이벤트를 받지 못함

먼저, 이벤트 컨슘에 문제가 생긴 경우를 생각했어요. 데이터 포맷 이 다르다던지, 컨슈머에 장애가 발생하는 등의 상황이 존재할 것 같아요.

데이터 포맷 에서 문제가 생길 경우, 이 요청은 절대로 실행되지 않을거예요. 다행히도, Kafka에는 이벤트 컨슘 실패 시 재시도 로직이 존재합니다. 또한 Spring Kafka 는 ErrorHandler를 통해 Dead Letter Queue 를 간편하게 사용할 수 있도록 지원합니다.

2. 내부 로직에서 예외가 발생

애플리케이션 로직 혹은 결제 중 문제가 발생한 경우입니다. 잔액 부족 등으로 인해 결제가 실패한 경우죠.

3. 선택

위 상황들을 포함해 결제 실패 라는 상황 자체는 크리티컬하지 않다고 생각했어요. 서비스 이용에 불편하겠지만, 결국 금전적인 손해는 일어나지 않았기 때문이예요.

이 경우에도, 1번 상황의 결제 요청 실패와 동일하게 재시도 로직, 실패 알람 등으로 해결 가능할 것이라 생각했습니다.

3. 결제 성공 이벤트 발행 실패


이번 포스트에서 주로 다룰 케이스예요.

결제 성공 후 어떠한 이유에 의해 이벤트 발행이 실패한 경우예요. 돈은 나갔는데, 피드백이 없는 경우죠. 이 케이스가 가장 크리티컬하다고 생각했습니다.

4. 결제 성공 이벤트 수신 실패


2번의 결제 실패, 그 중 1. 이벤트를 받지 못함과 유사한 상황이라고 생각해요. 다만 결제가 이미 진행됐기에, 보다 빠르게 피드백이 이뤄질 필요가 있어보여요.

실패 처리

이제 위 시나리오들을 순서대로 고려하며 실패에 대한 처리를 진행해볼게요.

1. 재시도

가장 먼저 고려한 방법입니다. 실패한 요청에 대해 반복하여 시도해보는 가장 단순하면서도 직관적인 방법이라고 생각해요.

이 방법의 경우, 1. 결제 요청 실패 케이스에서 발생하는 문제를 대부분 해결할 수 있을 것 같아요. 다만 아래의 두 케이스에 대해선 동일한 작업을 반복해도 영원히 실패할 수 밖에 없겠죠.

1) 잘못된 계좌 번호가 들어옴

2) 잘못된 포맷의 이벤트 발행

2. Dead Letter Queue(DLQ)

잘못된 계좌 번호가 들어옴 등, 사용자의 요청 자체가 잘못된 경우엔 사용자로부터 제대로 된 요청을 다시 입력받아야겠죠.

1) 사용자에게 정상 데이터 요청

그렇다면 잘못된 포맷의 이벤트 발행은 어떨까요? 이는 어플리케이션에서 발생하는 문제예요. 자체적으로 재시도를 하던가, 사용자로부터 다시 정보를 입력받는 방법으로는 절대로 성공할 수 없을 것이라 생각해요.

위 케이스를 해결하기 위해선 어플리케이션 로직 수정이 필요해요.
여유롭게 수정하고 처리할 수 있다면 정말 좋겠지만, 해당 이벤트 이후에 발급된 정상적인 이벤트들 또한 처리되지 않고 무한정 대기하게 되겠죠.

다행히도, 이러한 문제를 해결하기 위해 Dead Letter Queue 라는 방법이 존재합니다. 반복적으로 실패하는 이벤트를 별도의 실패 토픽에 옮기는 방식이예요. 이를 통해 잘못된 이벤트 이후의 정상 데이터를 처리할 수 있게 되겠죠.

2) 실패한 이벤트를 저장, 문제 해결 후 재시도

3. 이벤트 저장

이벤트 처리가 불가능한 케이스에 대한 처리를 진행했어요. 이제 문제 없이 동작할까요? 아쉽게도 여전히 문제가 존재하네요.

결제 성공 후 이벤트 발행 실패 케이스를 다시 봐볼까요?

결제가 성공해 이미 고객의 돈은 빠져나갔습니다. 하지만, 이후의 어떠한 로직이 실패했고, 결국 결제 성공 이벤트 발행에 실패했네요. 이 경우에도 반복되지 않는 문제라면 단순한 재시도를 통해 해결할 수 있을테고, 반복되는 문제라면 DLQ를 통해 해결할 수 있지 않을까요?

정말 슬프게도 그렇게 되지 않았습니다. 결제 성공 후 동일한 이벤트를 재시도 할 경우, "결제 자체가 다시 실행된다", 즉 중복 결제가 발생한다는거죠.


이러한 문제를 해결하기 위해 이벤트를 저장, 추적 하는 방식을 사용했습니다.

실패 처리 흐름

1. 도식

중복 결제를 방지하기 위한 흐름은 아래와 같습니다.

1) 이벤트 정보 저장

이벤트 발생 시 발생한 정보를 저장합니다. 저는 RDBMS를 사용해 저장했어요.

2) 이벤트 커밋

이벤트를 저장하고 난 뒤 곧바로 해당 이벤트를 커밋합니다. 저장된 후의 이벤트는 RDBMS가 책임지지, Kafka의 책임이 아니니까요.

커밋되지 않는다면 도중에 실패한 이벤트를 다시 꺼내와서 처리할테고, 이는 결국 중복 결제로 이어지겠죠.

3) 결제 진행 및 결제 결과 기록

결제를 진행한 후, 결과를 기록합니다.

다만, 프로젝트엔 해당 부분이 실제로 구현되어있진 않습니다. :)

4) 결제 성공 이벤트 발행

결제에 성공했을 시 결제 성공 이벤트를 발행합니다. 실패했을 경우엔 해당 단계는 건너뜁니다.

5) 이벤트 처리 시점 기록

모든 로직이 처리된 후의 시점을 기록합니다. 결제 성공 여부와는 관계없이 모든 이벤트에 동일하게 적용되는 부분이예요.

2. 구현

1) 이벤트 정보 저장 & 커밋

acknowledge() 호출 시 즉시 커밋을 진행하는 MANUAL_IMMEDIATE 설정을 사용했어요.
MANUAL 설정의 경우, 다음 poll() 동작이 진행될 때 커밋되므로 즉시 커밋해야하는 지금과 같은 상황엔 MANUAL_IMMEDIATE 설정이 필요합니다.

이벤트를 컨슘한 후, 곧바로 이벤트의 정보를 저장합니다. 이후 이벤트를 소비했음 을 의미하는 커밋을 진행해요.

2) 이벤트 처리

이벤트를 처리하는 실질적인 부분입니다. 저장된 이벤트를 찾아 결제를 진행하고, 이벤트 결과를 기록합니다. 나머지 로직을 진행하고, 이벤트가 완전 처리된 시점을 기록합니다.

3. 효과

이러한 방식을 통해 어떤 효과를 얻을 수 있을까요? 바로바로... 이벤트가 어디까지 진행됐는지를 추적할 수 있다는 거예요!

1) 이벤트 컨슘 실패


이벤트의 책임이 아직 Kafka에 존재합니다. 여기서 실패할 경우, 재시도 혹은 DLQ를 이용해 시패 처리를 진행하면 되겠죠.

2) 결제 여부 저장 안됨


데이터베이스에 결제 성공 여부가 저장되지 않은 경우입니다. 이는 결제가 진행되며 문제가 발생했음을 의미하며, 결제 자체를 재시도해야겠죠.

3) 이벤트 처리 시점 저장 안됨


데이터베이스에 이벤트 최종 처리 시점이 저장되지 않은 경우입니다. 이는 결제가 진행됐으며, 이후의 로직에서 문제가 발생했음을 의미해요.
중복 결제가 발생하는 실질적인 부분이지만, 현재는 결제가 진행됐는지를 알 수 있기 때문에 방지할 수 있죠.

현재 어플리케이션에선 결제 성공 여부를 보고 성공이라면 성공 이벤트 발행 동작만 재시도 하면 될 거예요.

마치며

DB에 이벤트 진행 과정을 기록함으로써 중복 처리를 피하는 로직을 구현해봤어요. 다만 실질적인 재시도 로직은 아직 존재하지 않습니다. 이는 추후 배치 프로그램을 추가 개발해 처리할 예정이예요!

profile
아는 척 하기 좋아하는 콩

0개의 댓글