Notice
Recent Posts
Recent Comments
Link
«   2026/01   »
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
Tags
more
Archives
Today
Total
관리 메뉴

che01 님의 블로그

Spring @Async 비동기 처리와 프록시 본문

Spring

Spring @Async 비동기 처리와 프록시

che01 2025. 7. 10. 15:36

 @Async 기본 개념

@Async는 Spring이 제공하는 비동기 처리 기능입니다. 메서드에 이 어노테이션을 붙이면 해당 메서드는 별도의 스레드에서 실행되어 메인 흐름과 병렬로 작업을 수행할 수 있습니다.

 프록시 기반 동작 원리

프록시 생성 과정

Spring은 @Async가 붙은 메서드를 감싸기 위해 프록시 객체를 생성합니다.

프록시로 교체되는 시점은 Spring 애플리케이션 시작 시 Bean 생성 시점입니다. Spring이 @Async, @Transactional 등 AOP 대상 어노테이션을 감지하여 자동으로 프록시를 생성하고 Spring 컨테이너에 등록합니다.

실제 실행 과정

keywordService.collect(keyword); // 진짜 객체가 아닌 프록시가 호출됨

실제 실행 흐름:

1. keywordService → 객체 호출

  • 우리가 호출: 컨트롤러나 서비스에서 keywordService.collect("자바") 호출
  • 실제 동작: Spring 컨테이너에는 실제 객체(KeywordService)가 아니라 프록시 객체가 등록되어 있음
  • 정리:
우리가 호출한 것 → keywordService.collect()
실제 호출된 대상 → 프록시 객체의 collect()

2. 프록시 객체 → @Async 감지

  • 설명: 프록시 객체는 내부적으로 메서드에 @Async가 붙어 있는지 확인
  • 근거: Spring 내부 클래스 AsyncExecutionInterceptor가 이를 수행 (실제로는 AsyncAnnotationAdvisor가 만들어주는 프록시 체인에 포함됨)
  • 정리: 프록시 객체는 @Async 어노테이션이 붙었는지 판단하고, 비동기 처리 여부를 결정

3. TaskExecutor.submit() → 새 스레드 풀에 작업 제출

  • 설명: 프록시가 TaskExecutor를 통해 Runnable 작업을 실행 요청
  • 근거: Spring 내부에서 SimpleAsyncTaskExecutor 또는 커스터마이징한 ThreadPoolTaskExecutor 사용
  • 프록시 내부에서 실행:
executor.submit(() -> {
    // 실제 메서드 호출
    target.collect(keyword);
});

4. 새 스레드에서 실행 → 실제 메서드 호출

  • 설명: 위의 Runnable 코드 블록은 새로 생성된 스레드 또는 스레드풀의 스레드에서 실행됨
  • 핵심: 이 블록 안에서 진짜 서비스 객체(KeywordService)의 collect() 메서드가 직접 호출됨
  • 정리:
    • Runnable.run() 안에서 → target.collect(keyword) 호출
    • 이때 target은 진짜 KeywordService Bean (프록시 아님)
    • 실행 스레드는 메인 스레드와 완전히 별개

즉, 프록시가 메서드 호출을 가로채서 별도의 스레드로 실행 요청을 보내는 방식으로 동작합니다.

작동 전제 조건

필수 설정

  • @EnableAsync: 설정 클래스나 main 클래스에 선언 필수
  • Bean 방식 호출: 프록시를 거치도록 다른 Bean에서 메서드를 호출해야 함
  • 예외 처리 주의: 비동기 메서드에서 발생한 예외는 호출자에게 전달되지 않음

설정 예시

@Configuration
@EnableAsync
public class AsyncConfig {
    // @Async 기능 활성화
}

실제 사용 예시

@Service
public class KeywordService {
    @Async
    public void collect(String keyword) {
        // DB 또는 Redis에 기록하는 시간이 걸리는 작업
        // 이 메서드는 별도 스레드에서 실행됨
    }
}

@Service
public class BookService {
    private final KeywordService keywordService;
    
    public List<Book> searchBooks(String keyword) {
        List<Book> books = bookRepository.findByKeyword(keyword);
        
        // 비동기로 검색어 수집 (응답 속도에 영향 없음)
        keywordService.collect(keyword);
        
        return books; // 검색 결과는 즉시 반환
    }
}

동작 흐름:

  1. 사용자가 검색 요청
  2. BookService.searchBooks()는 검색 결과를 동기로 반환
  3. KeywordService.collect()는 @Async로 비동기 처리되어 검색 응답 속도에 영향을 주지 않음

핵심 요약

@Async는 프록시를 통해 호출될 때만 비동기로 실행되며, 이 프록시는 Spring이 애플리케이션 시작 시 Bean 생성 시점에 자동으로 등록됩니다.

프록시 기반이라는 특성을 이해하면 @Async의 동작 원리와 제약사항을 모두 설명할 수 있습니다:

  • 내부 호출이 안 되는 이유: 프록시를 거치지 않기 때문
  • Bean으로 등록되어야 하는 이유: 프록시 객체가 Bean으로 등록되기 때문
  • Spring이 언제 프록시를 만드는지: 애플리케이션 시작 시 Bean 생성 시점

 흐름 검증 및 디버깅

전체 흐름 검증 결과

(1) keywordService → 객체 호출                        ✅ 우리가 호출
(2) 프록시 객체 → @Async 감지                        ✅ 프록시가 비동기 여부 판단
(3) TaskExecutor.submit() → 작업 제출                ✅ 스레드풀에 작업 요청
(4) 새 스레드에서 실행 → 실제 메서드 호출            ✅ 별도 스레드에서 원본 메서드 실행

디버깅 및 확인 방법

@Service
public class KeywordService {
    @Async
    public void collect(String keyword) {
        System.out.println("실행 스레드: " + Thread.currentThread().getName());
        System.out.println("메인 스레드와 다른지 확인: " + 
            !Thread.currentThread().getName().equals("main"));
        // DB 또는 Redis에 기록하는 시간이 걸리는 작업
    }
}