Skip to content

Latest commit

 

History

History
133 lines (109 loc) · 7.85 KB

File metadata and controls

133 lines (109 loc) · 7.85 KB

05 · Async 애스펙트 — @Async를 비동기 실행으로 라우팅

패키지: org.springframework.scheduling.aspectj

무엇을 / 왜

@Async가 붙은 메서드는 호출 즉시 별도 스레드 풀에 작업을 제출하고 호출자에게 바로 반환해야 한다. 기본은 프록시 방식이지만, self-invocation이나 컨테이너 밖 객체에서도 비동기화를 보장하려면 위빙이 필요하다. 이 패키지는 @EnableAsync(mode = AdviceMode.ASPECTJ)에서 동작한다.

Async 애스펙트는 다른 애스펙트들과 한 가지 독특한 점을 갖는다. AspectJ의 declare error/declare warning을 써서 컴파일 타임에 잘못된 반환 타입을 잡아낸다. @Async 메서드는 voidFuture만 반환할 수 있는데, 이를 위빙 시점에 강제한다.

핵심 타입

  AsyncExecutionAspectSupport (spring-context)
  ├─ determineAsyncExecutor(method) : 메서드에 맞는 Executor 결정
  ├─ doSubmit(task, executor, returnType) : 작업 제출
  └─ handleError(ex, method, args)
              ▲ extends
  AbstractAsyncExecutionAspect.aj
  ├─ around() : asyncMethod()
  │     → determineAsyncExecutor → doSubmit 어댑터
  └─ abstract pointcut asyncMethod()
              ▲ extends
  AnnotationAsyncExecutionAspect.aj
  ├─ asyncMethod() = @Async 메서드 || @Async 타입의 (void|Future) 메서드
  ├─ getExecutorQualifier(method) : @Async.value() 해석 (메서드 > 클래스)
  ├─ declare error  : @Async가 void/Future 아닌 메서드에 → 컴파일 에러
  └─ declare warning: @Async 클래스의 비 void/Future 메서드 → 경고(동기 실행)

  [활성화]
  AspectJAsyncConfiguration
  └─ @Bean asyncAdvisor = AnnotationAsyncExecutionAspect.aspectOf()
                          .configure(executor, exceptionHandler)
  • AbstractAsyncExecutionAspect: AsyncExecutionAspectSupport를 상속해 around 어드바이스로 비동기 제출 로직에 연결. 기본 생성자에서 super(null)로 기본 Executor를 비워 두고, 나중에 aspectOf() 빈 설정 시 주입받는다.
  • AnnotationAsyncExecutionAspect: @Async(org.springframework.scheduling.annotation.Async)를 매칭하는 구체 애스펙트.

동작 흐름 — 위빙된 @Async 메서드 호출

@Async
public CompletableFuture<Report> generate() { ... }   // void 또는 Future만 허용
service.generate()
   │
   │ AspectJ가 generate()에 위빙해 둔 around 진입
   ▼
AbstractAsyncExecutionAspect.around()
   │   pointcut: asyncMethod()
   │   = execution(@Async (void||Future+) *(..))            // 메서드에 @Async
   │     || execution((void||Future+) (@Async *).*(..))     // 클래스에 @Async
   │
   ▼  thisJoinPointStaticPart.getSignature() → Method
   ▼
determineAsyncExecutor(method)
   │   ├─ executor == null  → return proceed();   // 동기 실행 (Executor 미설정 시)
   │   └─ executor 있음     → 아래로
   ▼
Callable<Object> task = () -> {
    Object result = proceed();                    // 원래 메서드를 워커 스레드에서 실행
    if (result instanceof Future) return ((Future) result).get();  // 값 언래핑
    // 예외 시 handleError(ex, method, args)
    return null;
};
   ▼
doSubmit(task, executor, returnType)
   │     (AsyncExecutionAspectSupport)
   │     → executor에 task 제출, 호출자에겐 즉시 Future(또는 null) 반환
   ▼
호출자는 블로킹 없이 반환받음

핵심 분기는 Executor가 결정되지 않으면 그냥 proceed()로 동기 실행한다는 점이다(javadoc: "Otherwise it will simply delegate all calls synchronously"). 비동기화는 Executor가 주입돼 있을 때만 일어난다.

// AbstractAsyncExecutionAspect (발췌)
Object around() : asyncMethod() {
    MethodSignature sig = (MethodSignature) thisJoinPointStaticPart.getSignature();
    AsyncTaskExecutor executor = determineAsyncExecutor(sig.getMethod());
    if (executor == null) {
        return proceed();                    // 동기 폴백
    }
    Callable<Object> task = () -> {
        try {
            Object result = proceed();
            if (result instanceof Future) return ((Future<?>) result).get();
        }
        catch (Throwable ex) { handleError(ex, sig.getMethod(), thisJoinPoint.getArgs()); }
        return null;
    };
    return doSubmit(task, executor, sig.getReturnType());
}

컴파일 타임 검증 — declare error / warning

이 애스펙트의 특징적 부분이다. @Async는 반환 타입에 제약이 있는데, 위빙 시점에 이를 검사한다.

// AnnotationAsyncExecutionAspect
declare error:
    execution(@Async !(void || Future+) *(..)):
    "Only methods that return void or Future may have an @Async annotation";

declare warning:
    execution(!(void || Future+) (@Async *).*(..)):
    "Methods in a class marked with @Async that do not return void
     or Future will be routed synchronously";
  • declare error: 메서드에 직접 @Async를 붙였는데 반환 타입이 voidFuture도 아니면 컴파일 자체가 실패한다. 프록시 모드에서는 런타임에야 드러날 실수를, 위빙 모드에서는 빌드 단계에서 잡는다.
  • declare warning: 클래스에 @Async를 붙인 경우, 그 안의 void/Future 아닌 메서드는 (에러가 아니라) 경고만 내고 동기 실행된다. 클래스 레벨 @Async는 모든 메서드에 일괄 적용되므로, 비동기화 불가능한 메서드는 조용히 동기로 흘려보내는 것이 합리적이기 때문이다.

핵심 메서드

  • getExecutorQualifier(Method): @Async.value()로 지정된 Executor 한정자(qualifier)를 해석한다. 메서드 레벨 @Async가 클래스 레벨보다 우선한다(메서드의 value가 빈 문자열이어도 메서드 쪽이 이긴다 — 기본 Executor를 우선 쓰겠다는 의도). 프록시 모드의 AnnotationAsyncExecutionInterceptor와 동일하게 유지하라는 maintainer 주석이 달려 있다.
  • determineAsyncExecutor(Method): 한정자와 BeanFactory를 종합해 실제 AsyncTaskExecutor를 고른다. 없으면 null → 동기 폴백.
  • doSubmit(task, executor, returnType): 반환 타입에 맞춰(예: CompletableFuture, Future, void) task를 제출하고 적절한 Future를 호출자에게 돌려준다.
  • aspectOf() + configure(executor, exceptionHandler): AspectJAsyncConfiguration이 위버 싱글톤에 기본 Executor와 비동기 예외 핸들러를 주입한다.

설계 포인트

  • 동기 폴백: Executor 미설정 시 예외 대신 동기 실행으로 우아하게 퇴화한다. 애스펙트가 위빙됐더라도 설정이 안 됐으면 동작이 깨지지 않는다.
  • 빌드 타임 안전성: declare error로 잘못된 @Async 사용을 컴파일 타임에 차단하는 것은 위빙 모드만의 장점이다. 프록시 모드(AsyncAnnotationBeanPostProcessor)는 이런 정적 보증을 줄 수 없다.
  • 프록시 모드와 일관성: 한정자 해석 로직을 인터셉터와 동일하게 유지하도록 명시적으로 관리해, 두 모드의 Executor 선택이 어긋나지 않게 한다.
  • 반환값 언래핑: 워커 스레드에서 Future가 반환되면 .get()으로 값을 꺼내 다시 doSubmit의 결과 Future로 감싼다. 호출자는 항상 일관된 Future 한 겹을 받는다.

정리

Async 애스펙트는 @Async 메서드를 위빙으로 가로채 AsyncExecutionAspectSupport의 비동기 제출 로직에 연결한다. around가 Executor를 결정하고, 있으면 원래 메서드를 Callable로 감싸 워커 스레드에 제출하며, 없으면 동기 실행으로 폴백한다. 다른 애스펙트와 달리 declare error/declare warning으로 반환 타입 제약을 컴파일 타임에 강제하는 것이 이 패키지의 고유한 특징이다.