Skip to content

Conversation

@opixxx
Copy link

@opixxx opixxx commented Feb 4, 2025

메인 계좌(A) -> 메인 계좌(B) 송금

  • 메인 계좌 A에서의 출금 시 걸리는 락을 메인 계좌 B에 입금 작업이 끝날 때까지 가지고 가기 때문에 락의 범위를 줄이기 위해 분리했습니다.

    • 트랜잭션 분리를 위해 메시지 큐를 찾아봤으나 현재 프로젝트의 서버가 한 대를 바탕으로 진행하고 있기 때문에 메시지 큐를 사용하는 것보다 Redis의 List 자료구조를 사용했습니다.
  • 송금 요청 시 출금을 하고 커밋을 완료 후 Redis List에 출금 내용을 저장하는 이벤트를 발행합니다.

    • @TransactionalEventListener를 사용해서 출금 트랜잭션이 커밋이 된 후에 이벤트가 실행되도록 했습니다.
  • 입금 트랜잭션의 경우 스케줄러를 돌면서 Redis List에 저장되어 있는 출금 기록을 통해 입금을 수행합니다.

  • 입금 로직의 경우 멀티 스레드를 사용해 병렬 처리를 합니다.

  • 입금 성공 시 Redis List에 출금 내용을 삭제하는 이벤트를 발행한다.

  • 입금 실패 시 AOP를 통해 재시도를 위한 Redis List에 저장 한다.

    • 재시도도 실패 시 출금 롤백을 시도한다.

송금 시 잔액 부족 시 10,000원 단위로 충전 후 송금

송금 시 과정

  • 현재 내 잔액에 돈이 충분한지 확인하고 일일 한도를 초과하는 금액이 아닌지 확인
  • 충분하다면 송금
  • 부족하다면 부족한 금액을 10,000원 단위로 계산 후 충전할 금액이 일일 한도를 초과하는지 확인 후 송금

한도 유효기간 관리

  • Step 1 기존 로직을 유지했습니다.
  • 인당 한도가 달라지는 기능이 추가되거나 한도 초기화 주기가 바뀌는 상황이 있을 때를 대비하기 위해서 변경하기 좋은 구조를 고려해야할 것 같습니다.

입금 하는 과정에서 실패하는 경우 재시도를 시도하고 재시도도 실패하면 출금을 복구하는 과정으로 구현했는데, 출금을 복구하는 과정에 보상 트랜잭션에서 또 문제가 있을 경우 보상 트랜잭션에 대한 보상 트랜잭션을 구현해야 되는 것인지 고민이 됩니다. 이럴 경우 어떻게 하나요?

opixxx added 12 commits January 31, 2025 01:05
- 출금, 입금의 트랜잭션 분리
- Redis Hash를 이용해서 출금 기록을 저장
- 스케쥴러를 돌며 Redis Hash에서 출금 기록을 확인하며 알맞은 계좌에 입금
- 입금 시 멀티 스레드를 이용한 병렬 처리
- 출금, 입금의 트랜잭션 분리
- Redis Hash를 이용해서 출금 기록을 저장
- 스케쥴러를 돌며 Redis Hash에서 출금 기록을 확인하며 알맞은 계좌에 입금
- 입금 시 멀티 스레드를 이용한 병렬 처리
- 기존 Redis Hash -> Redis List로 변경
  - 기존 방법은 개별 출금 기록을 추적할 수 없어 입금 취소를 할 수 없음
- 입금 실패 시 일정 횟수 재시도 후 횟수를 초과할 경우 출금 계좌에 다시 돈을 복구
- 다른 메인 계좌에서 메인 계좌로의 송금 기능으로 인해 충돌 가능성이 높아질 것으로 생각하여 비관적 락으로 변경
@opixxx opixxx closed this Feb 4, 2025
@opixxx opixxx reopened this Feb 4, 2025
@opixxx opixxx closed this Feb 4, 2025
@opixxx opixxx reopened this Feb 4, 2025
@opixxx opixxx self-assigned this Feb 6, 2025
Copy link

@ZZAMBAs ZZAMBAs left a comment

Choose a reason for hiding this comment

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

저는 하나의 DB로만 처리하려 했었는데 레디스와 이벤트 처리를 통해 트랜잭션 처리를 할 수 있다는 인사이트를 얻어갑니다. 고생 많으셨어요!

//기본값 -> Repeatable Read
@Transactional(isolation = Isolation.READ_COMMITTED)
public void chargeMoney(Long accountId, long money) {
Account account = accountRepository.findByIdWithLock(accountId)
Copy link

Choose a reason for hiding this comment

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

비관적 락으로 바꾸신 것 같아요. 혹시 이유가 있으실까요?

Copy link
Author

Choose a reason for hiding this comment

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

  1. 메인 계좌(A) -> 메인 계좌(B) 송금
  2. 메인 계좌(B) 충전
  3. 메인 계좌(B) -> 적금 계좌로 송금

이 처럼 동시에 B 계좌에 접근을 할 요청이 많아 충돌 가능성이 높아질 것이라고 생각해서 비관적 락으로 변경했습니다.

Copy link

Choose a reason for hiding this comment

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

👍

long money = Long.parseLong(parts[3]);

try {
redisTemplate.opsForList().remove(PENDING_DEPOSIT, 1, deposit);
Copy link

Choose a reason for hiding this comment

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

이 코드가 멀티 스레드 처리가 가능한 이유가 레디스는 연산을 싱글 스레드로 처리하기 때문이 맞을까요?

Copy link
Author

Choose a reason for hiding this comment

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

레디스 연산이 원자적이라서 멀티 스레드 처리를 해도 동시성 문제가 없다고 생각했습니다.

log.debug("입금 성공: transactionId={}, senderAccountId={}, receiverAccountId={}, money={}",
transactionId, senderAccountId, receiverAccountId, money);
} catch (Exception e) {
redisTemplate.opsForList().rightPush(FAILED_DEPOSIT, deposit);
Copy link

Choose a reason for hiding this comment

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

redisTemplate.opsForList().remove(PENDING_DEPOSIT, 1, deposit)에서 에러가 나서 이 코드가 실행될 수도 있나요?

try {
redisTemplate.opsForList().remove(PENDING_DEPOSIT, 1, deposit);

Account receiverAccount = accountRepository.findByIdWithLock(receiverAccountId)
Copy link

Choose a reason for hiding this comment

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

트랜잭션이 없는 상태에서의 락은 바로 풀어지지 않나요? processDeposit()이 여러 동일 receiverAccountId에 대해 정상 작동하나요?

Copy link
Author

Choose a reason for hiding this comment

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

그렇겠네요. 락이 바로 풀어지겠네요. 테스트 시 정상 동작하긴 했는데 한 번 더 확인해봐야겠습니다.

- 출금(재시도) 성공 시 커밋 완료 후 이벤트 리스너를 사용하여 Redis에 저장된 송금 내역 삭제
- 출금(재시도) 실패 시 예외를 감지하는 AOP를 통해 송금 기록이 PENDING_DEPOSIT에서 제거되고, FAILED_DEPOSIT에 추가된다.
@sonarqubecloud
Copy link

sonarqubecloud bot commented Feb 7, 2025

@opixxx opixxx merged commit b560afc into base/opixxx Feb 10, 2025
3 checks passed
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