📌 배경
아이의 금융 습관 교육을 위한 대출 기능을 제공하면서, 기한 내에 상환을 하지 않으면 신용 점수가 감점되도록 하는 기능이 필요했습니다. 구체적으로는, 기한이 지난 대출에 대해 매달 신용 점수를 10점씩 감점하고, 이 점수는 0점 이하로 내려가지 않아야 했습니다.
이 기능을 구현하는 과정에서 다음과 같은 고민이 있었습니다:
- 어떻게 정기적으로 자동 실행되는 감점 로직을 구성할 수 있을까?
- 감점 대상이 중복으로 감점되지 않도록 하려면 어떤 기준이 필요할까?
- 실행 도중 문제가 발생해도 트랜잭션과 로그를 통해 복구하거나 추적할 수 있어야 하지 않을까?
🔍 고려한 기술: @Scheduled
vs Quartz
vs Linux cron
항목 | 리눅스 cron |
Spring @Scheduled |
Spring Quartz |
---|---|---|---|
설정 위치 | OS 수준 (서버 crontab) | 코드 내부 | 코드 + DB 관리 가능 |
트랜잭션 처리 | 수동 구현 필요 | Spring 지원 | Spring 지원 |
유지보수 | 서버마다 수동 관리 | 버전 관리 가능 | Job 동적 생성 가능 |
장애 복구 | 불가 | 일부 가능 | HA 구성 가능 |
동적 변경 | 불가 | 불가 | 가능 |
적합도 | ❌ | ⭕ (간단한 작업) | ✅ (복잡한 작업) |
✅ 선택한 기술: Spring @Scheduled
👉 왜 이 기술을 선택했는가?
- 해당 작업은 매달 1회 고정 주기로 실행되는 단순한 로직으로
Quartz
와 같은 무거운 프레임워크보다는@Scheduled
가 더 적합하다고 판단했습니다. - Spring 내부에서 JPA를 통한 DB 접근, 트랜잭션 관리, 로깅, 예외 처리 등을 유기적으로 처리할 수 있어 개발 생산성과 유지보수 측면에서 유리했습니다.
- 테스트 기간에는 2분마다 실행되는 스케줄러를 구성하여 실시간으로 감점 로직을 검증할 수 있었고, 실제 배포 시에는 매일 자정으로 주기를 변경했습니다.
🔧 구현 내용
1️⃣ Loan
엔티티에 lastPenalizedAt
필드 추가
@Column(name = "last_penalized_at")
private LocalDateTime lastPenalizedAt;
- 최근 감점 시점을 저장하여, 중복 감점 방지를 위한 기준으로 활용하였습니다.
2️⃣ 감점 스케줄러 구현
@Scheduled(cron = "0 0 0 * * ?") // 매일 자정 실행
@Transactional
public void applyMonthlyOverduePenalties() {
List<Loan> overdueLoans = loanRepository.findAll().stream()
.filter(loan -> loan.getStatus() == APPROVED)
.filter(loan -> loan.getDueDate().isBefore(LocalDateTime.now()))
.filter(loan -> loan.getLastPenalizedAt() == null || loan.getLastPenalizedAt().plusMonths(1).isBefore(LocalDateTime.now()))
.toList();
for (Loan loan : overdueLoans) {
CreditScore score = creditScoreRepository.findByUser(loan.getParentChild().getChild()).orElse(null);
if (score == null || score.getScore() <= 0) continue;
int updated = Math.max(0, score.getScore() - 10);
score.setScore(updated);
loan.setLastPenalizedAt(LocalDateTime.now());
log.info("대출 ID {}: 감점 적용 → {}", loan.getLoanId(), updated);
}
}
🧪 테스트 및 검증
해당 기능은 개발 초기 단계에서 주기를 2분으로 설정하여 테스트했습니다. 테스트 환경에서는 dueDate
가 지난 대출 데이터를 수동으로 설정하고, 감점이 실제로 적용되는지 로그를 통해 실시간으로 확인했습니다. 또한 lastPenalizedAt
필드를 기준으로 중복 감점이 발생하지 않는지 검증했습니다.
특히 신용 점수가 0점 이하로 내려가지 않도록 Math.max(0, ...)
로 안전하게 처리하였고, 점수 변경 로직에 트랜잭션을 적용하여 실행 중 예외가 발생하더라도 전체 작업이 롤백되도록 구성했습니다. 로그는 Slf4j를 통해 남겨두어, 감점 이력 추적이 가능하게 했습니다.
테스트 결과, 점수는 정확히 10점씩 감소했으며, 한 달 내 중복 감점은 발생하지 않았고, 트랜잭션과 로깅도 정상적으로 작동함을 확인했습니다.
💡 회고 및 느낀 점
이 기능을 구현하면서 단순한 작업일지라도 상태 관리(lastPenalizedAt
)와 트랜잭션 처리가 얼마나 중요한지 깨달았습니다.
또한 Spring의 @Scheduled
는 간단한 주기 작업을 빠르게 구현할 수 있는 반면, 동적 변경이나 클러스터링에는 한계가 있다는 점도 함께 인식하게 되었습니다.
기능의 복잡도가 커지거나 동적으로 스케줄을 변경해야 하는 요구사항이 생긴다면, Quartz나 Redis 기반 분산 스케줄러로 전환할 수 있도록 구조를 유연하게 설계하는 것이 중요하다는 점도 배웠습니다.
📌 결론
단순하고 고정된 주기 작업에는
@Scheduled
가 최적의 선택이었으며,
향후 확장성을 고려할 수 있도록Quartz
도입 가능성을 염두에 두고 설계했습니다.
댓글