작성자: 권지원
<aside>
@Scheduled 는 JVM 단위로 동작 → 동일 스케줄러 동시 실행 Issue 발생@SchedulerLock 으로 스케줄러 메서드 진입 자체를 단일 인스턴스로 제한
</aside>ForPets 백엔드는 수평 확장(scale-out)을 전제로 설계되어 있다. 트래픽이 늘어나면 동일한 애플리케이션을 N대 복제해서 로드밸런서 뒤에 배치한다.
문제는 @Scheduled 어노테이션이 JVM 단위로 동작한다는 점이다. 인스턴스가 3대라면 매 주기마다 스케줄러가 3번 실행된다.
현재 ForPets 에는 4개의 스케줄러가 존재한다:
| 스케줄러 | 주기 | 주요 작업 | 외부 I/O |
|---|---|---|---|
ReservationExpireScheduler |
1분 | PENDING 예약 만료 + 환불 + 상태 복원 | PortOne 환불 API |
PostExpireScheduler |
10분 | OPEN 공고 만료 + Proposal cascade 만료 | DB only |
CareRequestExpireScheduler |
10분 | PENDING/ACCEPTED 돌봄 요청 만료 | DB only |
UnavoidableCancelAutoApproveScheduler |
매일 00:00 | UNAVOIDABLE 취소 자동 승인 + 알림 발송 | Kafka |
실제 장애를 재현하기 전에 설계 단계에서 리스크를 먼저 분석했다. 아래는 인스턴스 3대(A, B, C)가 동시에 스케줄러를 실행할 때 발생 가능한 문제들이다.
ReservationExpireScheduler 는 만료 대상 예약에 대해 PortOne 환불 API 를 호출한다.
T=0 A, B, C 가 동시에 만료 대상 예약 조회
→ 동일한 reservation_id 목록을 각자 조회
T=0.1 A, B, C 가 동시에 PortOne 환불 API 호출
→ 동일한 건에 대해 환불 요청 3회 발생
PortOne 은 멱등성 키(paymentId)가 있어 실제 이중 환불은 막히지만, 의도치 않은 API 호출이 발생했다는 사실 자체가 시스템 이상이다. 환불 API 스펙이 바뀌거나, 멱등성 키 없는 다른 외부 API 로 교체될 경우 실제 이중 환불로 이어질 수 있다.
UnavoidableCancelAutoApproveScheduler 는 cron 으로 정확히 00:00 에 트리거된다. N대가 거의 동시에 같은 대상을 조회하고 Kafka 에 알림 이벤트를 발행하면, 사용자는 동일한 알림을 N번 받는다.
엔티티 락(lock:reservation:{id})이 있어 실제 상태 변경은 1회만 일어나지만, 락 획득 이전에 이미 알림 발행 코드가 실행되는 race window 가 존재한다. 이 경우 상태는 정합성을 유지하더라도 알림은 중복 발송된다.