📝 면접 답변 보기
@Async는 스프링 AOP를 통해 동작합니다.
스프링이 @Async가 붙은 메소드를 발견하면, 해당 클래스의 프록시 객체를 생성해서 빈으로 등록합니다.
다른 빈에서 이 서비스를 주입받으면 실제로는 프록시 객체가 주입되고, 메소드를 호출하면 프록시 안에 있는 MethodInterceptor가 AsyncExecutionInterceptor를 실행합니다.
AsyncExecutionInterceptor는 TaskExecutor를 사용해서 새로운 스레드에서 실제 메소드를 실행하고, 호출한 쪽에는 즉시 리턴해서 비동기 처리가 가능해집니다.
중요한 점은, 같은 클래스 내부에서 @Async 메소드를 직접 호출하면 프록시를 거치지 않기 때문에 비동기로 동작하지 않습니다.
면접 질문 정리용 레포지토리
https://github.com/unifolio0/backend-interview-study.git
GitHub - unifolio0/backend-interview-study
Contribute to unifolio0/backend-interview-study development by creating an account on GitHub.
github.com
핵심 개념
1. 스프링 빈과 프록시
빈(Bean)이란?
빈은 스프링 컨테이너가 관리하는 객체를 의미합니다. @Component, @Service, @Repository 등의 어노테이션이나 @Bean 메서드로 등록된 모든 객체가 빈입니다.
@Service
public class EmailService {
public void sendEmail(String to) {
// 이메일 전송 로직
}
}
위 코드에서 EmailService는 스프링 빈으로 등록됩니다.
프록시(Proxy)란?
프록시는 실제 객체를 감싸는 대리 객체입니다. 스프링은 특정 기능을 제공하기 위해 원본 빈 대신 프록시 객체를 생성해서 주입합니다.
2. AOP (Aspect-Oriented Programming)
AOP란 무엇인가?
AOP는 관점 지향 프로그래밍으로, 여러 곳에서 공통으로 사용되는 기능을 모듈화하는 프로그래밍 기법입니다.
예를 들어, 여러 서비스 메소드에 로깅을 추가하고 싶을 때
AOP 없이 구현한다면 모든 메소드에 아래와 같은 로직을 붙여줘야 합니다.
public void methodA() {
log.info("methodA 시작");
// 실제 로직
log.info("methodA 종료");
}
public void methodB() {
log.info("methodB 시작");
// 실제 로직
log.info("methodB 종료");
}
하지만 AOP를 활용하게 된다면 아래와 같이 Aspect를 구현해주면 모든 메소드에 로깅이 적용이 됩니다.
@Aspect
@Component
public class LoggingAspect {
@Around("execution(* com.example.service.*.*(..))")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
log.info(joinPoint.getSignature().getName() + " 시작");
Object result = joinPoint.proceed();
log.info(joinPoint.getSignature().getName() + " 종료");
return result;
}
}
AOP 용어 정리
- Aspect: Advice + Pointcut을 담고 있는 모듈 (위 예시의 LoggingAspect 클래스)
- Advice: 실제로 실행될 부가 기능 코드 (logAround 메서드)
- Pointcut: Advice를 어디에 적용할지 정의하는 표현식 (execution(...))
- JoinPoint: Advice가 적용될 수 있는 지점 (메서드 호출 시점 등)
- Proxy: Aspect를 실제로 적용하기 위해 스프링이 생성하는 대리 객체
스프링 AOP의 동작 방식
- 스프링이 빈을 생성할 때 AOP 적용 대상인지 확인
- 적용 대상이면 프록시 객체 생성
- Advice 로직을 프록시에 포함
- 프록시 객체를 빈으로 등록
- 메서드 호출 시 프록시가 Advice를 실행한 후 실제 메서드 호출
3. CGLIB 프록시
JDK Dynamic Proxy vs CGLIB Proxy
스프링 AOP는 두 가지 방식으로 프록시를 생성할 수 있습니다:
JDK Dynamic Proxy:
- 인터페이스 기반
- 대상 클래스가 인터페이스를 구현해야 함
- Java 리플렉션 API 사용
CGLIB Proxy:
- 클래스 기반 (상속)
- 인터페이스 없이도 프록시 생성 가능
- 바이트코드 조작으로 서브클래스 생성
- 스프링 부트의 기본 방식
CGLIB로 프록시를 생성하게 되면 아래와 같은 형태로 프록시 객체가 생성됩니다.



@Async의 동작 과정
Step 1 : 스프링 애플리케이션 시작 시
1. @EnableAsync 설정 확인
먼저 스프링 부트 애플리케이션에서 비동기 기능을 활성화해야 합니다. @EnableAsync가 있어야 AsyncConfigurationSelector를 임포트하여 @Async가 붙은 빈을 프록시로 만드는 AsyncAnnotationBeanPostProcessor를 빈으로 등록합니다.
@SpringBootApplication
@EnableAsync // ← 이 어노테이션이 필수!
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
2. @Async가 붙은 메서드를 가진 클래스 발견
스프링이 빈을 스캔하면서 @Async 어노테이션을 찾습니다.
@Service
public class AsyncService {
@Async
public void callAsync(String name) {
System.out.println("callAsync: " + name);
}
public void callNotAsync(String name) {
System.out.println("callAsync: " + name);
}
}
3. CGLIB으로 프록시 클래스 생성
스프링은 AsyncService를 상속하는 프록시 클래스를 동적으로 생성합니다.
AsyncService$$SpringCGLIB$$0 (프록시)
↓ extends
AsyncService (원본)
4. AsyncExecutionInterceptor를 MethodInterceptor로 등록
프록시 객체에 비동기 처리를 담당하는 AsyncExecutionInterceptor를 등록합니다. 이 인터셉터가 실제로 새로운 스레드를 생성하고 메서드를 실행하는 역할을 합니다.
// 스프링 내부 동작 (의사 코드)
AsyncService$$SpringCGLIB$$0 proxy = new AsyncService$$SpringCGLIB$$0();
// 비동기 처리를 위한 인터셉터 등록
AsyncExecutionInterceptor asyncInterceptor = new AsyncExecutionInterceptor();
asyncInterceptor.setExecutor(taskExecutor); // 스레드 풀 설정
proxy.setCallback(0, asyncInterceptor);
5. 프록시 객체를 빈으로 등록
최종적으로 원본 AsyncService 대신 프록시 객체를 스프링 컨테이너에 빈으로 등록합니다.
스프링 컨테이너
├── "asyncService" → AsyncService$$SpringCGLIB$$0 (프록시)
└── (원본 AsyncService는 프록시 내부에만 존재)
Step 2: 메서드 호출 시
1. 외부에서 asyncService.callAsync() 호출
@RestController
public class AsyncController {
private final AsyncService asyncService; // 프록시 객체가 주입됨
public AsyncController(AsyncService asyncService) {
this.asyncService = asyncService;
}
@PostMapping("/async")
public String async() {
System.out.println("비동기 시작: " + Thread.currentThread().getName());
asyncService.callAsync("비동기 처리");
System.out.println("비동기 완료: " + Thread.currentThread().getName());
return "완료";
}
}
2. 실제로는 프록시 객체의 callAsync() 실행
asyncService는 실제로는 AsyncService$$SpringCGLIB$$0 프록시 객체이므로, 프록시의 callAsync 메서드가 호출됩니다. 아래는 실제 프록시 객체의 코드입니다.
public final void callAsync(String var1) {
try {
MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
if (var10000 == null) {
CGLIB$BIND_CALLBACKS(this);
var10000 = this.CGLIB$CALLBACK_0;
}
if (var10000 != null) {
var10000.intercept(this, CGLIB$callAsync$0$Method, new Object[]{var1}, CGLIB$callAsync$0$Proxy);
} else {
super.callAsync(var1);
}
} catch (Error | RuntimeException var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
3. MethodInterceptor.intercept() 호출
프록시 내부에서 MethodInterceptor의 intercept가 호출됩니다. 이때 MethodInterceptor의 구현체 중 하나인 DynamicAdvisedInterceptor의 intercept가 호출됩니다. 아래는 DynamicAdvisedInterceptor의 intercept의 동작을 요약해서 적은 코드입니다. 이 코드의 맨 마지막인 new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain).proceed()에서 AsyncExecutionInterceptor의 invoke() 메소드가 실행됩니다.
@Override
@Nullable
public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
// 1. 타겟 객체 가져오기 (실제 AsyncService 인스턴스)
target = targetSource.getTarget();
Class<?> targetClass = (target != null ? target.getClass() : null);
// 2. 이 메서드에 적용할 Interceptor 체인 가져오기
// AsyncExecutionInterceptor 같은 AOP Alliance MethodInterceptor들이 들어있음
List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
Object retVal;
if (chain.isEmpty()) {
// 3-A. Advice가 없으면 원본 메서드 직접 호출
Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);
retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse);
}
else {
// 3-B. Advice가 있으면 ReflectiveMethodInvocation 생성하고 실행
// 여기서 AsyncExecutionInterceptor.invoke()가 호출
retVal = new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain).proceed();
}
return processReturnType(proxy, target, method, args, retVal);
}
4. AsyncExecutionInterceptor가 TaskExecutor에 작업 제출
TaskExecutor의 스레드 풀에서 사용 가능한 스레드를 할당받아 작업을 실행합니다.
@Override
@Nullable
@SuppressWarnings("NullAway")
public Object invoke(final MethodInvocation invocation) throws Throwable {
Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null);
final Method userMethod = BridgeMethodResolver.getMostSpecificMethod(invocation.getMethod(), targetClass);
AsyncTaskExecutor executor = determineAsyncExecutor(userMethod);
if (executor == null) {
throw new IllegalStateException(
"No executor specified and no default executor set on AsyncExecutionInterceptor either");
}
Callable<Object> task = () -> {
try {
Object result = invocation.proceed();
if (result instanceof Future<?> future) {
return future.get();
}
}
catch (ExecutionException ex) {
handleError(ex.getCause(), userMethod, invocation.getArguments());
}
catch (Throwable ex) {
handleError(ex, userMethod, invocation.getArguments());
}
return null;
};
return doSubmit(task, executor, invocation.getMethod().getReturnType());
}
6. 호출한 쪽에는 즉시 리턴
실행 결과:
비동기 시작: http-nio-8080-exec-1
비동기 완료: http-nio-8080-exec-1 // ← 즉시 리턴!
callAsync: 비동기 처리 // ← 나중에 비동기로 실행됨
'CS' 카테고리의 다른 글
| 동기/비동기, 블로킹/논블로킹은 무엇이고 각각의 차이점은 무엇인가요? (0) | 2025.10.08 |
|---|---|
| 컴퓨터의 병렬 처리 기술 (5) | 2025.07.27 |
| [매일메일] 객체 지향 프로그래밍이란 무엇이고, 어떤 특징이 있나요? (0) | 2025.03.31 |
| [매일메일] PRG 패턴에 대해서 설명해 주세요. (0) | 2025.03.24 |