웜업 2탄 - 커스텀 어노테이션

Choi Wang Gyu·2025년 5월 27일

🔍 문제상황

@Component
public class WarmUpRunner implements ApplicationRunner {
    private final BoardService boardService;
    private final CategoryService categoryService;
    // 서비스 추가 시 매번 필드 + 호출 추가 필요

    @Override
    public void run(ApplicationArguments args) {
        boardService.warmupBoardData();
        categoryService.warmupCategoryData();
    }
}

웜업 작업의 수동 관리가 필요한것입니다. 웜업 수동 방식은 다음과 같은 문제를 가집니다.

  • 중복 코드 증가: 서비스가 늘어날수록 의존성주입 필드와 호출이 반복
  • 유지보수 어려움: 특정 웜업 로직이 바뀌면 WarmUpRunner도 수정해야 함
  • 의존 순서 보장 어려움: 예: UserService가 먼저 초기화되어야 ProfileService가 작동함
  • 확장성 부족: 새 서비스가 생기면 매번 Runner 코드 추가 필요

🛠 해결 아이디어: 어노테이션으로 웜업 대상을 선언하고 자동 실행하자

왜 어노테이션인가?

코드변경없이 @Warmup어노테이션 하나만 붙여서 자동으로 실행되도록
실행 순서도 지정 가능하게
어노테이션에 선언적으로 메타정보만 부여하고, 나머지 로직은 리플렉션이 처리

  1. 유지보수의 비효율 특정로직을 수정하면 웜업서비스에서도 코드수정이 필요하다. 이는 유지보수가 어렵고 관리포인트를 늘린다.

  2. “B가 A를 사용하는 경우, A를 먼저 웜업해야 하지 않을까?”
    예를 들어, UserService가 내부적으로 api호출을 한다면,
    UserService가 먼저 웜업하고 그다음 UserService를 실행하는 것이 더 효율적이다.
    의존성 순서에 따라 웜업이 실행되도록 순서를 조절할 수 있어야 한다.

고민했던 다양한 방식

방법장점단점
AOP실행 감지에는 좋음실행 트리거에는 부적합
컴파일 타임 APT성능 좋음웜업은 런타임에 실행돼야 하므로 부적절
리플렉션 + 어노테이션실행 자동화, 선언적 구성리플렉션 비용은 있지만 초기 1회이므로 감내 가능

결국, 런타임에서 자동으로 웜업 메서드를 찾아 실행할 수 있는 리플렉션 기반 설계로 확정했습니다.

설계 구조

어노테이션 정의

@Repeatable(WarmupContainer.class)
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Warmup {
    boolean enabled() default true;
    WarmupType[] type() default { WarmupType.DEFAULT };
    int order() default 1; // 의존성 순서 조절용
}

웜업실행 로직 (리플렉션 기반)

@EventListener(ApplicationReadyEvent.class)
public void onApplicationReady() {
    if (!warmupEnabled) return;

    List<WarmupMethod> warmupMethods = new ArrayList<>();

    for (String beanName : context.getBeanDefinitionNames()) {
        Object bean = context.getBean(beanName);
        Method[] methods = bean.getClass().getDeclaredMethods();

        for (Method method : methods) {
            Warmup warmup = method.getAnnotation(Warmup.class);
            if (warmup == null || !warmup.enabled() || method.getParameterCount() > 0) continue;

            warmupMethods.add(new WarmupMethod(bean, method, warmup.order()));
        }
    }

    warmupMethods.stream()
        .sorted(Comparator.comparingInt(WarmupMethod::order)) // 순서대로 실행
        .forEach(warmupMethod -> {
            try {
                warmupMethod.method().setAccessible(true);
                warmupMethod.method().invoke(warmupMethod.bean());
                log.info("✅ [Warmup 완료] {}.{}", warmupMethod.bean().getClass().getSimpleName(), warmupMethod.method().getName());
            } catch (Exception e) {
                log.warn("❌ [Warmup 실패] {}.{}", warmupMethod.bean().getClass().getSimpleName(), warmupMethod.method().getName(), e);
            }
        });

    warmupState.markComplete();
}

🚀 결과

  • 어노테이션 기반 자동화 덕분에 웜업 관리가 훨씬 쉬워졌다
  • 새로운 서비스가 추가되어도 코드 수정 없이 어노테이션만 붙이면 된다

POC 예제 코드

https://github.com/cwangg897/warm-up

참조

https://oliveyoung.tech/2024-10-30/application_warmup_algorithm/

0개의 댓글