본문 바로가기
프로젝트 develop

비동기 처리 구조와 @Async

by BIGENGINEER 2026. 7. 19.

왜 동기 방식에서 비동기로 바꿈?

- 사용자가 AI 분석 요청하면 서버가 AI 처리 끝날 때까지 응답 못 돌려줌

- AI 처리에 10초가 걸리면 사용자의 HTTP 요청도 10초동안 계속 연결된 상태

 

=> 그 요청을 처리하던 서버 Thread도 계속 잡혀 있다는 점

 

 

<보통의 spring boot 요청 처리>

사용자 요청 1 -> tomcat thread 1

사용자 요청 2 -> tomcat thread 2

사용자 요청 3 -> tomcat thread 3

 

but, AI 요청 하나가 오래 걸리면

Thread 1 -> AI 응답 10초 대기

Thread 2 -> AI 응답 12초 대기

Thread 3 -> AI 응답 8초 대기

 

Thread 1
사용자 A 요청
↓
Qwen 호출
↓
응답 기다리는 중............10초
↓
DeepSeek
↓
응답 반환


Thread 2
사용자 B 요청
↓
Qwen 호출
↓
응답 기다리는 중..............12초
↓
DeepSeek
↓
응답 반환


Thread 3
사용자 C 요청
↓
Qwen 호출
↓
응답 기다리는 중........8초
↓
DeepSeek
↓
응답 반환

 

-> Thread 1은 사용자 A의 HTTP 요청에 묶여있는 것

-> 다른 사용자 요청을 처리하는 데 사용할 수 없음

 

시간 →   0초     2초     4초     6초     8초     10초    12초

Thread1  [──────────── AI 처리/대기 ────────────]
Thread2  [──────────────── AI 처리/대기 ─────────────────]
Thread3  [────────── AI 처리/대기 ──────────]

 

 

위의 상황에서 즉, Thread가 3개밖에 없다고 가정하고, D의 요청이 들어온다면 남는 HTTP Thread가 없으므로 기다려야 함

 

 

 

그래서 비동기로 바꿈

 

1. HTTP 요청 처리

2. AI 처리

 

 

사용자
  │
  │ POST /ai/analyze
  ▼
┌──────────────────────┐
│ HTTP Thread           │
│                      │
│ 요청 받기             │
│ ↓                    │
│ Job 생성(PENDING)     │
│ ↓                    │
│ @Async 호출 ───────────────┐
│ ↓                    │      │
│ 202 + jobId 반환      │      │
└──────────────────────┘      │
                              ▼
                     ┌────────────────────┐
                     │ AI Worker Thread   │
                     │                    │
                     │ PROCESSING         │
                     │ ↓                  │
                     │ Qwen               │
                     │ ↓                  │
                     │ DeepSeek           │
                     │ ↓                  │
                     │ 결과 저장           │
                     │ ↓                  │
                     │ DONE               │
                     └────────────────────┘

 

 

 

사용자 A → HTTP Thread 1 → Job 1 생성 → Worker에 전달 → 바로 응답
사용자 B → HTTP Thread 2 → Job 2 생성 → Worker에 전달 → 바로 응답
사용자 C → HTTP Thread 3 → Job 3 생성 → Worker에 전달 → 바로 응답

 

 

 

즉, HTTP 요청 Thread : 사용자 요청을 받고 응답을 돌려주는 역할

AI Worker Thread : 오래 걸리는 qwen, deepseek 처리하는 역할

 

 


 

@Async

 

- 보통 메인 클래스나 설정 클래스에  @EnableAsync -> 붙임 

@SpringBootApplication
@EnableAsync
public class MapiApplication {

    public static void main(String[] args) {
        SpringApplication.run(MapiApplication.class, args);
    }
}

 

< 흐름 > 

@EnableAsync -> spring의 비동기 기능 활성화 -> @Async가 붙은 Bean 감지 -> Proxy 생성 -> 메서드 호출 가로채기 -> Executor에 작업 제출

 

 

 

 

1. @Async가 없을 때

//controller
@RestController
@RequiredArgsConstructor
public class AiController {

    private final AiService aiService;

    @PostMapping("/ai")
    public String requestAi() {
        aiService.processAi();
        return "완료";
    }
}


//service
@Service
public class AiService {

    public void processAi() {
        // Qwen 호출
        // DeepSeek 호출
    }
}

 

사용자 요청
   ↓
Tomcat
   ↓
HTTP Thread-1 배정
   ↓
Controller 실행
   ↓
aiService.processAi() 호출
   ↓
Qwen 호출
   ↓
DeepSeek 호출
   ↓
Controller로 복귀
   ↓
HTTP 응답

 

 

=> 하나의 thread에서 처리됨

 

 

 

 

 

2. @Async가 있을 때

//service
@Service
public class AiAsyncService {

    @Async("aiExecutor")
    public void processAi(Long jobId) {

        // 오래 걸리는 AI 처리
        // Qwen
        // DeepSeek
    }
}

//호출하는 쪽
aiAsyncService.processAi(iobId)

 

Spring이 @Async가 붙은 Bean을 그대로 호출하는 게 아니라 중간에 Proxy를 둠

 

<실제 과정>

aiAsyncService.processAi()
↓
Spring Proxy가 먼저 가로챔
↓
어? 이 메서드에 @Async가 붙었네?
↓
현재 HTTP Thread에서 실행하지 않음
↓
Thread Pool에 작업으로 제출
↓
HTTP Thread는 바로 다음 코드로 진행

 

즉, HTTP Thread-1 이 processAi(jobId) 호출하면 Spring Proxy가 

'이걸 내가 직접 실행시키지 말고 aiExecutor에게 작업으로 넣어야겠다'라고 생각함

즉 여기서 aiExecutor는 Thread Pool 인거고 

aiExecutror.execute(작업)을 하고 HTTP Threa-1은 즉시  돌아감

그리고 aiExecutor가 별고의 Worker Thread를 하나 골라서 그 중 하나가 실제 메서드 실행 

 

 

aiExecutor는 쉽게 생각하면 아래와 같은 형식인거고, 얘가 별도의 Worker Thread를 하나 고르는 것

그러면 고른 Thread가 실제 메서드를 실행하는 거지 

AI Thread Pool

Worker-1
Worker-2

 

 

 

 


 

실제 코드를 보면서 확인해보자

@PostMapping("/analyze")
public ResponseEntity<?> analyze(...) {

    Long jobId = aiJobService.createJob();

    aiAsyncService.process(jobId);

    return ResponseEntity
            .accepted()
            .body(jobId);
}

-------------------------------------------------------------

@Service
public class AiJobService {

    public Long createJob() {

        AiJob job = new AiJob();
        job.setStatus(PENDING);

        repository.save(job);

        return job.getId();
    }
}

--------------------------------------------------------------

@Service
public class AiAsyncService {

    @Async("aiExecutor")
    public void process(Long jobId) {

        // PROCESSING 변경

        // Qwen 호출

        // DeepSeek 호출

        // DONE 저장
    }
}

 

[HTTP Thread-1]

1. POST /analyze 요청 받음

2. createJob()
   → DB에 PENDING 저장
   → jobId = 123

3. aiAsyncService.process(123) 호출

4. Spring Proxy가 @Async 발견

5. process(123)을 aiExecutor에 작업으로 등록

6. HTTP Thread-1은 기다리지 않고 바로 다음 줄 실행

7. 202 Accepted + jobId=123 반환

8. HTTP Thread-1 자유로워짐

 

 

 

 

[AI Worker-1]

1. aiExecutor에서 Job 123 작업 가져옴

2. process(123) 실제 실행

3. DB : PENDING → PROCESSING

4. Qwen 호출

5. Qwen 응답 대기

6. DeepSeek 호출

7. DeepSeek 응답 대기

8. 결과 DB 저장

9. PROCESSING → DONE

 

 

 

 

 

 

 

 

 

<주의할 점>

같은 클래스에서 직접 호출하면 안됨

 

@Service
public class AiService {

    public void start() {
        processAsync();
    }

    @Async
    public void processAsync() {
        ...
    }
}

 

start() 안에서 processAsync(); -> 얘 호출하면 사실상 this.processAsync(); 인 것 

 

Proxy가 @Async를 발견할 수 있는 구조는 

외부 객체 -> Proxy -> AiService 여야 함

 

근데, 같은 객체 내부에서 호출하면

AiService

this.processAsync()

 

-> 이렇게 자기 메서드로 들어가서 Proxy를 안 지나감 

그래서 현재 Thread에서 그냥 실행될 수 있음 

 

그래서 이 프로젝트에서는  AiJobService와 AiAsyncService로 분리한 것 

 

 

 

 

 

< 최종 과정 >

1. @EnableAsync로 spring 비동기 기능 활성화

2. @Async 메서드가 있는 Bean을 Spring이 관리

3. spring이 해당 Bean 앞에 Proxy 생성

4. 다른 Bean에서 @Async 메서드 호출

5. Proxy가 호출을 가로챔

6. 메서드를 현재 thread에서 직접 실행하지 않고 executor의 thread pool에 작업으로 제출

7. worker thread가 실제 메서드를 실행

 

 

< 플젝 > 

HTTP Thread
↓
AiJobService
↓
Job PENDING 저장
↓
AiAsyncService.process(jobId) 호출
↓
Spring Proxy
↓
@Async 감지
↓
aiExecutor에 작업 등록
↓
HTTP Thread는 202 반환


동시에


aiExecutor
↓
Worker Thread 선택
↓
process(jobId) 실행
↓
PROCESSING
↓
Qwen
↓
DeepSeek
↓
DONE

 

 

@Async는 메서드의 실행 자체를 다른 worker thread에게 맡기도록 spring proxy가 중간에서 executor에 작업을 제출해주는 기능 

 

'프로젝트 develop' 카테고리의 다른 글

동기 vs 비동기  (0) 2026.07.13
Presigned URL  (0) 2026.07.13
[develop]develop plan  (0) 2026.07.02