왜 동기 방식에서 비동기로 바꿈?
- 사용자가 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 |