[MSA 프로젝트] 해외주식 정산 D+2 반영을 위한 자정 배치 처리

greenlemonT·2025년 3월 6일

프로젝트

목록 보기
13/15

은행이나 금융 시스템에서는 사용자의 모든 거래 내역을 즉시 데이터베이스(DB)에 반영할 수 없다.
거래가 발생할 때마다 DB를 업데이트하면 부하가 커지고 성능이 저하될 위험이 있기 때문이다.
그래서 실제 금융 시스템에서는 Redis 같은 캐싱 시스템을 활용하여 임시 저장한 후, 자정에 배치(batch) 처리를 통해 DB에 반영하는 방식이 일반적이다.


1. 왜 배치 처리가 필요한가?

해외주식 거래는 국내 주식과 달리 D+2 결제 원칙을 따른다.
즉, 매수 또는 매도 주문이 체결되더라도 2일 후(D+2)에 예수금이 최종 반영된다.

  • 해외주식 예수금 처리 방식
    매수 시: 주문 즉시 예수금 차감 → D+2일에 최종 출금 반영
    매도 시: 주문 즉시 증거금 증가 → D+2일에 매도 대금 입금 반영

즉시 DB를 업데이트하면 안 되는 이유

  • 실시간으로 업데이트하면 트랜잭션이 많아져 DB 부하 증가
  • 해외 거래소에서 실제 정산이 완료되지 않았는데, 사용자 잔고를 업데이트하면 오류 발생 가능
  • 대형 금융 서비스에서는 배치 처리로 성능 최적화
    -> 따라서 Redis에 저장해두고, 자정에 배치 프로세스를 실행하여 최종적으로 반영하는 방식을 구현했다.

2. 배치 처리 시스템 설계

배치 처리 시스템은 매일 밤 12시(00:00) 에 실행되어 D+2일이 된 데이터만 반영한다.

<배치 처리 흐름>

  • 해외주식 거래 체결 후 배치 예수금 데이터를 Redis에 저장 (user:{userId}:batch_balance:{날짜})
  • 매일 자정에 배치 스케줄러 실행 (@Scheduled(cron = "0 0 0 * * ?"))
  • 오늘 날짜(D+2)의 예수금 데이터를 조회하여 DB 업데이트
  • 업데이트된 데이터를 다시 Redis에 저장하여 최신 상태 유지

3. 배치 처리 코드 분석

00:00 자정 배치 실행 (applyPendingUpdates())

@Scheduled(cron = "0 0 0 * * ?") // 매일 00:00 실행
public void applyPendingUpdates() {
    String today = LocalDate.now().format(DATE_FORMATTER);
    Set<String> keys = redisTemplate.keys("user:*:batch_balance:" + today);

    if (keys == null || keys.isEmpty()) {
        log.info("[배치 처리] 오늘({}) 적용할 예수금 데이터 없음.", today);
        return;
    }

    for (String key : keys) {
        String userId = key.split(":")[1];  // Redis 키에서 userId 추출
        String balanceStr = redisTemplate.opsForValue().get(key);
        String newBalance = balanceStr;

        accountRepository.updateAccountWithholding(Long.parseLong(userId), Long.parseLong(newBalance));
        log.info("사용자 {} 예수금 DB 업데이트 (D+2 반영): {}", userId, newBalance);

        // Redis 사용자 잔고 업데이트
        String userBalanceKey = "user:" + userId + ":balance";
        redisTemplate.opsForValue().set(userBalanceKey, newBalance, EXPIRATION_DAYS, TimeUnit.DAYS);
        log.info("Redis 사용자 {} 실제 예수금 업데이트: {}", userId, newBalance);

        log.info("[배치 적용 완료] 사용자ID: {}, 적용 예수금: {}", userId, newBalance);
    }
}
  • 매일 00:00 (@Scheduled(cron = "0 0 0 * * ?")) 실행
  • 오늘 날짜(D+2)에 해당하는 배치 예수금 데이터를 Redis에서 가져옴
  • DB(accountRepository.updateAccountWithholding())에 업데이트
  • 업데이트된 값을 다시 Redis에 저장하여 캐싱 데이터 유지

4. Redis에 배치 예수금 저장 방식

체결 후 배치 예수금을 저장할 때, Redis에 D+2 날짜를 포함한 키로 저장!

  • 배치 예수금 저장 방식 (user:{userId}:batch_balance:{날짜})
user:123:batch_balance:2025-03-19 -> "5000000"
user:456:batch_balance:2025-03-19 -> "2500000"

user:123:batch_balance:2025-03-19 → D+2일(2025-03-19)에 500만 원 반영 예정
✔ user:456:batch_balance:2025-03-19 → D+2일(2025-03-19)에 250만 원 반영 예정

  • 장점:

D+2 날짜가 포함된 키로 저장하여 배치 실행 시 필요한 데이터만 조회 가능
캐싱을 활용하여 빠른 조회 가능
실제 DB 업데이트가 필요한 시점(D+2)에만 반영하여 부하 감소

트러블슈팅: 배치 예수금 반영 시 발생한 문제 및 해결

  • 문제 1: 배치 실행 후에도 Redis 값이 즉시 반영되지 않음
    이슈:
    -> 배치가 실행된 후 DB에는 정상 반영되었지만, Redis의 사용자 잔고(user:{userId}:balance)가 갱신되지 않아 사용자가 조회할 때 오래된 값이 보임

  • 해결 방법:
    배치 실행 후 DB 업데이트와 동시에 Redis에도 최신 데이터를 반영

String userBalanceKey = "user:" + userId + ":balance";
redisTemplate.opsForValue().set(userBalanceKey, newBalance, EXPIRATION_DAYS, TimeUnit.DAYS);

-> 배치 실행 후 DB & Redis 동시 업데이트하여 사용자 조회 시 최신 예수금 표시

결론: 자정 배치 처리의 중요성

금융 시스템에서 예수금을 실시간으로 DB에 반영하는 것은 비효율적이며, 부하를 줄이기 위해 배치 프로세스가 필수적이다 !

0개의 댓글