Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/opixxx step4 #32

Open
wants to merge 28 commits into
base: base/opixxx
Choose a base branch
from
Open

Feature/opixxx step4 #32

wants to merge 28 commits into from

Conversation

opixxx
Copy link

@opixxx opixxx commented Feb 17, 2025

송금 구조 변경

기존 송금 구조는 Redis를 사용해서 입출금 트랜잭션을 분리를 하여 송금을 구현을 했습니다.
출금을 했는데 Redis가 문제가 생겨 출금된 기록이 제대로 저장되지 않아 출금은 됐지만 입금이 안되는 문제가 생길 수 있습니다. 이 문제를 해결하기 위해 생각한 방법이 트랜잭션 아웃박스 패턴을 생각했습니다.

Redis + 트랜잭션 아웃박스 패턴을 이용해서 데이터의 정합성을 지키는 방법을 분산 환경이 아닌 단일 서버에 도입하는 것이 불필요하다고 생각했습니다.

그래서 기존 Redis를 사용해서 송금 내역을 기록하는 대신 DB에 Transation 테이블에 송금 내역을 저장하여 입출금 트랜잭션을 분리하도록 했습니다.

송금의 종류도 2가지로 나눴습니다.

  1. 즉시 송금(IMMEDIATE_TRANSFER)
    • 송금을 하자마자 receiver 계좌의 입금이 됩니다.
  2. 대기 송금(PENDING_TRANSFER)
    • 송금을 하고 receiver가 직접 확인 후 수령을 합니다.

Transactionl 테이블

입출금을 하기 위해 사용되는 Transaction의 type과 status입니다.

  • type
    • 실시간 송금(IMMEDIATE_TRANSFER)
    • 승인 대기 송금(PENDING_TRANSFER)
  • status
    • WITHDRAW: 즉시 송금에서 출금이 이루어졌을 경우
    • PENDING_DEPOSIT: 대기 송금에서 출금이 이루어졌을 경우(수령 대기)
    • SUCCESS_DEPOSIT : 입금이 이루어졌을 경우
    • FAILED_DEPOSIT : 입금이 실패했을 경우
    • CANCEL: 송금이 취소된 경우

즉시 송금 과정

  • 출금 트랜잭션 성공 시

    • 출금과 Transactional 테이블에 송금 내역을 저장, status = WITHDRAW
  • 출금 트랜잭션 실패시 -> 롤백

  • 입금 트랜잭션

    1. Transactional테이블에서 status = WITHDRAW이 데이터를 커서 페이징으로 데이터를 가져오고 입금을 시도한다.
    2. 입금이 성공하면 status = SUCCESS_DEPOSIT으로 변경
    3. 입금이 실패하면 status = FAILED_DEPOSIT으로 변경
    4. 입금에 실패한 송금 내역을 재시도하기 위해 롤백 스케줄러를 돌면서 다시 재입금 시도한다.
    5. 재시도에서도 실패를 하면 송금을 롤백을 하고 status를 CANCEL로 변경한다.

대기 송금 과정

  • 출금 트랜잭션 성공 시

    • 출금과 Transactional 테이블에 송금 내역을 저장, status = PENDING_DEPOSIT
  • 출금 트랜잭션 실패시 -> 롤백

  • 입금 트랜잭션 (즉시 송금과는 다르게 돈을 받겠다는 API를 하나 만듦)

    • 수령 API를 요청하면 해당 Transaction 데이터를 조회해서 해당 돈을 입금 시킨다. status = SUCCESS_DEPOSIT으로 변경
  • 송금 취소 API

    • 해당 Transactional status가 PENDING_DEPOSIT 이면 송금 취소할 수 있다.
  • 송금 마감 취소 기능

    • PENDING_DEPOSIT인 데이터를 가져 온 다음 애플리케이션 단에서 72시간이 지난 Transaction을 필터링을 하고, 해당 데이터들을 취소시킨다.
  • 송금 마감 알림 기능

    • PENDING_DEPOSIT인 데이터를 가져 온 다음 애플리케이션 단에서 마감 시간까지 24시간이 남은 Transaction을 필터링을 하고, 해당 Transaction receiver에게 이메일 알림을 보낸다.

Transaction status에 따라 입출금이 이루어지다보니 여러 요청에서 동시성 문제가 일어날 수 있다.

  • 동시에 수령, 취소 요청이 들어오는 경우
  • 동시에 수령 요청을 보내는 경우 중복 수령 문제

그렇기 때문에 입출금을 위해 Transaction을 조회할 때는 비관적 락을 사용해서 데이터 정합성을 지키려고 했다.

- Redis -> TransferTransactional 테이블로 입출금 트랜잭션 분리
- Redis -> TransferTransactional 테이블로 입출금 트랜잭션 분리
- 금액을 받는 사용자가 확인 후 송금을 받을 수 있는 API 추가 구현
- 아직 수령하지 않은 송금을 취소하는 기능 구현
@opixxx opixxx self-assigned this Feb 18, 2025
Copy link

@nyh365 nyh365 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

고생하셨습니다~

// transactions 데이터를 스트림을 사용해서 72시간이 지난 transactions 데이터를 필터링
List<Transaction> expiredTransactions = transactions.stream()
.filter(t -> t.getSendTime().isBefore(expirationTime))
.toList();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

72시간이 지난 transactions 데이터를 DB에서 필터링 해서 가져오지 않고, 어플리케이션단에서 필터링한 이유가 있을까요?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DB에서 필터링을 한다면 where t.status = :status and t.sendTime <= :expiredTime and t.id > :lastId로 쿼리를 날립니다.
그렇다면 인덱스를 (status, sendTime, id)로 설정을 해야할 것으로 생각했는데 해당 인덱스에서는 해당 쿼리에서 id 부분에서 제대로 인덱스를 타지 않을 것이라고 생각했습니다. status, sendTime까지 인덱스를 타고 id로 필터링만 될 것이라고 생각해서 애플리케이션단에서 필터링하였습니다. 추후 데이터를 적재해서 실행계획을 통해서 쿼리 성능을 봐야할 것 같긴합니다.

.orElseThrow(NotFoundAccountException::new);

senderAccount.deposit(transaction.getAmount());
transaction.updateStatus(CANCEL);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

스케줄러에 의해 72시간이 지난 송금 내역이 취소되기 전, 동시에 사용자가 송금을 받는다면 어떻게 되나요?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

동시성 문제를 고려해서 현재 입출금 관련해서 Transaction 테이블을 조회해올때 DB단에 락을 걸어 조회합니다

eventPublisher.publishEvent(
new TransactionCreateEvent(
senderAccountId,
request.receiverAccountId(),
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

존재하지 않는 receiverAccountId에 대한 유효성 검증 로직이 따로 없는 것 같아서 고려해보면 좋을 것 같습니다.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네. 한 번 유효성 검증을 통해 넣어야겠네요. step5 에 추가해놓겠습니다!

Copy link

@hellozo0 hellozo0 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수고하셨습니다!
송금과정에 대한 구조를 깊이 고민해보신거 같아요! 덕분에 저도 transactional outbox pattern에 알아볼 수 있어서 이 내용 기반으로 저도 참고해 보겠습니다!

개인적으로 (1) 스케줄러를 사용하는 방식 vs (2)폴링 ...+(3) 로그테일링 방법중에 어떤 방법이 각자 상황에 적합한지 궁금하기도 하네요.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants