서비스에서 이벤트를 진행하면서 각 사업장의 방문자를 집계하고, 조건을 만족한 사용자에게 쿠폰을 지급해야 하는 요구사항이 생겼다.
이 기능은 사용자의 요청으로 실행되는 API가 아니라, 매일 정해진 시간에 자동으로 처리되어야 하는 작업이었다.
따라서 Spring Scheduler를 이용해 반복 작업을 자동화했고, 이번 글에서는 회사의 비즈니스 로직은 제외한 채 구현 과정과 설계하면서 고민했던 부분을 정리해보려고 한다 😊
Spring Scheduler는 애플리케이션 내부에서 일정 주기로 작업을 실행할 수 있도록 지원하는 기능이다.
이번 기능처럼 사용자의 요청과 관계없이 정해진 시간에 반복 실행되어야 하는 작업을 구현하기에 적합했다.
이번 기능 요구사항
방문 발생 → 방문자 집계 → 발급 대상 조회 → 쿠폰 발급
서비스 설계 흐름
Scheduler → Service → Mapper → Coupon Service
Scheduler는 실행 시점만 담당하도록 하고 실제 비즈니스 로직은 Service 계층으로 분리했다.
이를 통해 스케줄러는 실행 책임만 가지도록 하고, 비즈니스 로직은 다른 환경에서도 재사용할 수 있도록 구성했다.
고민했던 점
방문자 집계는 하루에 한 번 수행되지만, 배치가 재실행될 가능성도 고려해야 했다.
동일 날짜의 집계 데이터가 이미 존재하는 경우에는 갱신하고, 존재하지 않는 경우에는 새로 생성해야 했기 때문에 MERGE INTO를 적용했다.
이를 통해 배치가 동일한 날짜에 다시 수행되더라도 데이터의 정합성을 유지할 수 있었다.
예시
// 매일 23시
@Scheduled(cron = "0 0 23 * * *")
public void aggregateVisitors() {
batchService.aggregateDailyVisitors();
}
// Service
public void aggregateDailyVisitors() {
String visitDt = DateTimeUtil.nowOfKst().format(DateTimeUtil.yyyyMMdd);
visitorMapper.upsertVisitorCountByVisitDt(visitDt);
}
MERGE INTO visitor_summary target
USING (
SELECT
business_id,
visit_dt,
COUNT(*) AS visitor_count
FROM visitor_log
WHERE visit_dt = #{visitDt}
GROUP BY business_id, visit_dt
) source
ON (
target.business_id = source.business_id
AND target.visit_dt = source.visit_dt
)
WHEN MATCHED THEN
UPDATE SET
target.visitor_count = source.visitor_count,
target.updated_at = CURRENT_TIMESTAMP
WHEN NOT MATCHED THEN
INSERT (
business_id,
visit_dt,
visitor_count,
created_at,
updated_at
)
VALUES (
source.business_id,
source.visit_dt,
source.visitor_count,
CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP
);
스케줄러는 서비스 계층을 호출하고, 서비스에서는 집계 기준 날짜를 생성한 뒤 Mapper를 통해 집계 작업을 수행한다.
집계 과정에서는 동일한 날짜와 사업장 기준으로 데이터를 갱신하거나 생성해야 했기 때문에 MERGE INTO를 사용했다.
⚠️ 집계가 완료된 데이터는 이후 쿠폰 발급 서비스에서 발급 대상자를 조회하는 기준 데이터로 활용된다. 해당 로직은 회사 비즈니스 로직에 해당하므로 이번 글에서는 제외했다.
마치며
이번 기능을 구현하면서 Spring Scheduler는 단순히 특정 시간에 메서드를 실행하는 기능이 아니라, 반복 작업을 안정적으로 수행하기 위한 하나의 실행 도구라는 점을 다시 느꼈다.
실제 비즈니스 로직은 서비스 계층에 위임하고, 스케줄러는 실행 시점만 관리하도록 분리함으로써 역할을 명확하게 나눌 수 있었다.
앞으로는 실행 이력 관리나 실패 재처리뿐만 아니라, 분산 환경에서의 중복 실행 방지까지 고려하여 더욱 안정적인 배치 구조를 구성해보고 싶다.
'Case Study > Modernization' 카테고리의 다른 글
| OTP 2차 인증(2FA) 구현기: Security 생명주기 안으로 인증 흐름 끌어들이기 (0) | 2026.06.04 |
|---|---|
| FTP 배포에서 CI/CD 자동화로 넘어가며 얻은 안정성 (1) | 2026.03.19 |
| PHP 레거시 시스템을 Spring Boot로 전환하며 고민했던 기록들 (0) | 2026.03.12 |