본문 바로가기
Spring

요금/재고 동기화 병목 문제를 스프링 스케줄링 개선으로 해결한 후기

by SpearZero 2025. 5. 10.

문제 상황

현재 서비스에서는 외부 연동사의 숙박 업체 정보를 가져와 판매하기 위해, 데이터를 동기화하는 별도의 서버를 운영하고 있다.

 

해당 서버는 여러 개의 큐(자바 내부의 큐)를 통해 데이터를 스케쥴링하여 처리하고 있었는데, 인수인계 받았을 당시 요금/재고 관련 큐에 하루 약 10만 건, 며칠이 지나면 수십만 건의 데이터가 누적되는 현상이 발생하고 있었다. 문제는 이 큐들이 좀처럼 해소되지 않았고, 시간이 지날수록 데이터가 쌓여가고 있었다. 이에 따라, 요금/재고가 동기화 지연이 발생했고, 외부 연동사의 숙박 상품의 예약 실패 사례도 점점 늘어나고 있었다.

 

원인 분석

문제의 원인을 파악하기 위해 코드와 실행 로그를 함께 확인했다. 코드상으로는 약 6개의 주기적인 스케줄링 작업이 존재했고, 실제 로그를 확인해보니 동일한 이름의 스레드가 모든 작업을 처리하고 있는 상황이었다. 이로 인해 모든 작업이 단일 스레드에서 순차적으로 실행되고 있었으며, 특정 작업이 지연되면 다른 작업까지 함께 밀리는 구조였음을 확인할 수 있었다.

 

내부 동작 분석

스프링에서는 별도의 설정 없이 @Scheduled를 사용할 경우, 기본적으로 단일 스레드로 동작하는 것으로 알고 있었고, 이를 다시 확인하기 위해 스프링 문서를 찾아보았다.

// ScheduledAnnotationBeanPostProcessor 문서

setScheduler

public void setScheduler(Object scheduler)

Set the TaskScheduler that will invoke the scheduled methods, or a ScheduledExecutorService to be wrapped as a TaskScheduler.

If not specified, default scheduler resolution will apply: searching for a unique TaskScheduler bean in the context, or for a TaskScheduler bean named "taskScheduler" otherwise; the same lookup will also be performed for a ScheduledExecutorService bean. If neither of the two is resolvable, a local single-threaded default scheduler will be created within the registrar.

스케줄링 설정을 하지 않아, 스프링이 기본으로 제공하는 단일 스레드 스케줄러가 동작하고 있었고, 이로 인해 모든 작업이 하나의 스레드에서 순차적으로 실행되는 구조가 되어버린 것이었다.

Bean post-processor that registers methods annotated with @Scheduled to be invoked by a TaskScheduler according to the "fixedRate", "fixedDelay", or "cron" expression provided via the annotation.
This post-processor is automatically registered by Spring's <task:annotation-driven> XML element and also by the @EnableScheduling annotation.

Autodetects any SchedulingConfigurer instances in the container, allowing for customization of the scheduler to be used or for fine-grained control over task registration (e.g. registration of Trigger tasks). See the @EnableScheduling javadocs for complete usage details.

해당 문서는 또한 @EnableScheduling 선언 시, Spring이 자동으로ScheduledAnnotationBeanPostProcessor를 등록하고, 내부적으로 TaskScheduler 또는 SchedulingConfigurer 구현체를 감지하여 스케줄링 로직을 구성하는 흐름을 설명하고 있다.

 

문제 해결

스레드 설정을 하기 위해 SchedulingConfigurer를 구현한 설정을 만들었고 스레드는 스케줄링 작업이 총 6개였기 때문에, 각 작업이 병렬로 실행될 수 있도록 스레드 풀의 크기를 6개 이상으로 설정하였다.

@Configuration
public class SchedulingConfig implements SchedulingConfigurer {

    @Override
    public void configureTasks(ScheduledTaskRegistrar registrar) {
        ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
        scheduler.setPoolSize(7);
        scheduler.setThreadNamePrefix("...");
        scheduler.initialize();

        registrar.setTaskScheduler(scheduler);
    }
}

스케줄링 작업 설정 이후, 병목의 또 다른 원인이었던 큐 처리 단위 로직을 점검하게 되었다. 요금 및 재고 정보는 한 번에 수십 건의 웹훅이 유입될 정도로 변경 빈도가 높았지만, 기존 로직은 한 번에 10건씩만 큐에서 꺼내 처리하도록 구성되어 있어, 빠른 속도로 누적되는 데이터를 감당하기 어려운 구조였다. 해당 설정은 시스템 부하를 고려해 보수적으로 설정된 것으로 보였지만, 현재의 트래픽 규모에는 맞지 않았기 때문에 처리 단위를 10건에서 100건으로 상향 조정하기로 결정했다.

 

다만, 단순히 값을 수정하고 적용하기보다는 운영 환경에서 수집한 실제 웹훅 데이터를 활용해 검증 서버에서 실험을 진행했다.

 

해당 실험을 통해 스케줄링 처리 시 서버의 부하나 DB 부하가 크게 증가하지 않는다는 것을 확인했고, 충분한 안정성이 확보되었다고 판단하여 상용 환경에 적용하였다.

 

결과

결과적으로, 큐에 데이터는 더 이상 쌓이지 않고 빠르게 해소되기 시작했으며, 예약 실패 역시 눈에 띄게 감소하는 것을 확인할 수 있었다.

 

개선해야 할 점

평소에는 큐가 빠르게 해소되지만, 피크 시간대에는 데이터 적재량이 처리량을 초과하여 병목 현상이 발생하고 있다. 해당 서버는 쿠버네티스 환경에서 3개의 파드로 분산 운영되고 있으며, 각 파드는 독립적인 큐를 통해 데이터를 처리한다.

이러한 구조에서는 동일한 제휴점의 데이터가 서로 다른 파드에 분산되어 처리될 수 있으며,  
그로 인해 먼저 들어온 데이터보다 나중에 들어온 데이터가 먼저 처리되어 이전 상태를 덮어쓰는 정합성 문제가 발생할 가능성이 존재한다.

큐의 누적과 데이터 일관성 측면에서 해결책으로는 @Async를 활용하여 큐 적체를 빠르게 해소할 수 있으며, 이와 함께 Contents 서버의 데이터 모델에 요금/재고 등의 ‘최종 업데이트 시점(timestamp)’ 필드를 추가하고, 처리 대상 데이터의 timestamp가 이미 저장된 값보다 과거일 경우 해당 요청을 무시하도록 처리하면 정합성 이슈는 일정 수준 해결할 수 있다. 이 전략은 메시지 처리 순서가 어긋날 수 있는 환경에서도 최신 상태만 유지하도록 보완하는 현실적인 방안이다. 이 방식은 데이터가 무작위로 처리되는 환경에서도 최신 상태를 유지하는 데 효과적이지만, 구조적으로 순서 자체를 제어하는 방식은 아니다.

순서를 보장하기 위해서는 다른 구조가 필요하다. 예를 들어, 파티셔닝 또는 키 기반 분산 처리를 지원하는 메시지 처리 시스템을 도입하고, 제휴점 단위로 동일 파티션에 데이터가 할당되도록 설계하면, 데이터가 순차적으로 처리되며, 정합성과 순서를 보다 안정적으로 보장할 수 있다.

현재 트래픽 규모와 시스템 구조에서는 큰 문제가 없지만, 장기적으로는 데이터 정합성을 보장할 수 있는 메시지 처리 전략으로의 전환이 필요하다.