1. 배경
- 특가 상품은 지정된 시작/종료 시간에 맞춰 상품 상태를 정확히 변경해야 하는 요구사항이 있었음
- 이를 구현하기 위한 스케줄러 기술을 선택하는 과정에서 아래 네 가지 방식을 검토함
2. 검토한 방식
1) 조회 시점 계산 방식
- 상품 조회 시 현재 시간과 saleStartTime, saleEndTime을 비교해 판매전 / 판매중 / 판매종료 상태를 동적으로 판단하는 방식
- 장점: 구조가 단순하고 스케줄러가 필요 없음
- 단점
- 조회 시 BETWEEN saleStartTime AND saleEndTime 조건이 들어가는데, 이 경우 인덱스를 제대로 활용하지 못해 상품 수가 많아질수록 풀스캔이 발생할 수 있음
- 서버 시간과 DB 시간이 다를 경우, 동일한 상품이 서버에서는 판매중으로 보이고 DB에서는 판매전으로 처리되는 불일치가 생길 수 있음
- 실제 상품 상태값이 DB에 반영되지 않기 때문에 다른 도메인에서 상태를 재사용하거나 관리자 화면에서 현재 상태를 바로 확인하기 어려움
- 특가 시작/종료를 상태 변경 이벤트로 다루기보다 응답 시 계산으로 처리하게 되어 도메인 의미가 약해질 수 있음
2) Redis 기반 예약 키 + 폴링 방식
- 특가 상품 시작/종료 시간을 Redis에 키 또는 ZSET 형태로 저장한 뒤, 주기적으로 현재 시각과 비교하여 상태를 변경하는 방식
- Redis가 메모리 기반이라 빠르고, 재고 차감/분산락/동시성 제어 구조와 자연스럽게 연결할 수 있다는 점이 장점이었음
- 하지만 Redis에 예약 정보를 저장해도 그 시각에 실행할 처리기가 별도로 필요하고, 결국 일정 주기로 예약 키를 확인하는 폴링이 들어가야 함
- 이는 원하던 정확한 시각에 상태 변경과는 차이가 있었고, 1초나 1분 단위 폴링은 불필요한 반복 조회로 이어질 수 있어 비효율적이라 판단함
3) @Scheduled 기반 폴링 스케줄러
- Spring의 @Scheduled를 사용해 1분마다 특가 상품 전체를 조회하고, 시작/종료 대상 상품을 찾아 상태를 바꾸는 방식
- 구현 자체는 단순하지만 전체 상품을 주기적으로 반복 조회해야 하므로 상품 수가 많아질수록 비효율이 커질 수 있음
- 또한 여러 서버 환경에서는 동일한 스케줄러가 중복 실행될 위험도 존재함
- 시간이 되면 딱 상태를 바꾸는 방식을 원했기 때문에 최종 선택에서 제외함
4) Quartz 예약 실행 방식
- 특가 상품을 등록하는 시점에 시작 Job과 종료 Job을 함께 등록해두고, 지정된 시각에 정확히 한 번 실행하여 상태를 변경하는 방식
- 주기적으로 상품 전체를 확인하지 않아도 되고, 시작 시간에는 READY → ON_SALE, 종료 시간에는 ON_SALE → SALE_ENDED처럼 도메인 상태를 명확히 다룰 수 있음
3. 최종 선택 - Quartz
- 특가 상품의 핵심 요구사항은 정해진 시작 시간에 정확히 판매중으로 전환되고, 종료 시간에 정확히 판매종료로 전환되는 것이었음
- 따라서 주기적으로 상태를 확인하는 폴링 방식보다, 상품 등록 시점에 시작/종료 작업 자체를 예약하는 방식이 더 적합하다고 판단함
1) 폴링 없이 예약 실행 가능
- @Scheduled나 Redis 기반 폴링처럼 주기적으로 전체 상품을 조회하지 않고, 상품별 시작/종료 시각에 맞춰 Job을 직접 예약할 수 있음
2) 상태 변경의 의미가 명확함
- Quartz를 사용하면 시작 시각에 READY → ON_SALE, 종료 시각에 ON_SALE → SALE_ENDED로 실제 상태값이 전환되어 도메인 의미가 분명해짐
- 조회 시점 계산 방식처럼 서버/DB 시간 불일치 문제나 인덱스 미활용 문제도 발생하지 않음
3) 역할 분리가 자연스러움
- 특가 시작/종료는 시간 예약 실행 문제이고, 재고 차감/동시성 제어는 동시 요청 처리 문제임
- 이 둘을 같은 기술로 억지로 해결하기보다 역할을 분리하는 것이 더 자연스럽다고 판단함
- 시작/종료 예약 → Quartz
- 재고 차감/동시성/분산락 → Redis
4) 불필요한 반복 조회를 줄일 수 있음
- Quartz는 상품별 시작/종료 시각에 필요한 작업만 실행하기 때문에, 반복 조회 기반 방식보다 불필요한 부하를 줄일 수 있음
4. 쿠폰 스케줄러와 다르게 Quartz를 쓴 이유
- 쿠폰 스케줄러는 Spring @Scheduled + cron 방식으로 구현했음
- 타임세일 스케줄러는 Quartz로 구현했음
- 같은 프로젝트 안에서 스케줄러 기술이 다른 이유는 두 도메인의 성격 자체가 다르기 때문임
- 구분 쿠폰 스케줄러 타임세일 스케줄러
| 실행 시점 | 고정된 주기 (매주 월요일 00시 등) | 상품마다 다른 시작/종료 시각 |
| 등록 방식 | 코드에 cron 표현식으로 고정 | 상품 등록 시점에 동적으로 Job 생성 |
| 실행 횟수 | 반복 실행 | 각 상품당 정확히 1회 실행 |
| 적합한 기술 | @Scheduled + cron | Quartz |
- 쿠폰은 매주 월요일 00시처럼 반복적이고 고정된 주기에 실행하면 되는 작업임
- 반면 타임세일은 상품마다 시작 시간과 종료 시간이 다르고, 각각 딱 한 번만 실행되어야 하는 작업임
- @Scheduled는 미리 정해진 시간에 반복 실행하는 구조라 동적으로 시각을 등록하는 게 어렵고, 상품마다 다른 시각에 맞춰 Job을 만들려면 Quartz처럼 실행 시점을 동적으로 지정할 수 있는 도구가 필요했음
- 그래서 두 도메인에 각각 성격에 맞는 기술을 선택함