πŸ› οΈBackend/🌳Spring

[Spring Framework] μŠ€ν”„λ§ 비동기 처리 방법

junbin2 2025. 7. 25. 04:01

βœ… 1. 비동기(Asynchronous) λž€?

1. ν΄λΌμ΄μ–ΈνŠΈ μš”μ²­
2. μ›Œμ»€ μŠ€λ ˆλ“œ ν΄λΌμ΄μ–ΈνŠΈ μš”μ²­ 처리
3. λ™μž‘ 쀑 비동기 둜직 발견
4. μ›Œμ»€ μŠ€λ ˆλ“œλ₯Ό ν•˜λ‚˜ 더 λ§Œλ“€μ–΄μ„œ 비동기 λ‘œμ§μ„ 처리
5. μš”μ²­ 처리 및 비동기 처리 μŠ€λ ˆλ“œ λ‘κ°œκ°€ λŒμ•„κ°€κ²Œ 됨.
6. μŠ€μΌ€μ€„λŸ¬μ˜ μ˜ν•΄ λ”°λ‘œ λ™μ‹œμ˜ λ™μž‘μ„ν•˜κ²Œ 됨.
  • μ–΄λ–€ μž‘μ—…μ„ μš”μ²­ν•œ ν›„ κ·Έ μž‘μ—…μ˜ μ™„λ£Œ μ—¬λΆ€λ₯Ό 기닀리지 μ•Šκ³  λ‹€μŒ μž‘μ—…μ„ λ°”λ‘œ μˆ˜ν–‰ν•˜λŠ” 방식을 μ˜λ―Έν•œλ‹€.
  • 보톡 κ΅¬ν˜„μ€ μš”μ²­ ν•˜λ‚˜ μŠ€λ ˆλ“œμ—μ„œ λΆ„κΈ°λ˜μ–΄ λ™μ‹œμ— λ‹€λ₯Έ μž‘μ—…λ„ μ²˜λ¦¬ν•˜λŠ” 과정이라고 λ³Ό 수 있음.

βœ… 2. Spring μ—μ„œ 비동기(Asynchronous) 처리

βœ… @EnableAsyncκ°€ ν•˜λŠ” 일 μš”μ•½ ( μ• λ„ˆν…Œμ΄μ…˜ μ‚¬μš©μ‹œ λ‚΄λΆ€ λ™μž‘ )

AsyncAnnotationBeanPostProcessor 등둝
→ @Async μ• λ„ˆν…Œμ΄μ…˜μ΄ 뢙은 λ©”μ„œλ“œλ₯Ό κ°μ§€ν•΄μ„œ
→ ν•΄λ‹Ή λ©”μ„œλ“œλ₯Ό **ν”„λ‘μ‹œ(proxy)**둜 감싸고, 비동기 μ‹€ν–‰λ˜λ„λ‘ μ„€μ •

TaskExecutor (μŠ€λ ˆλ“œ ν’€) μ„€μ •
→ @Async λ©”μ„œλ“œκ°€ 싀행될 λ•Œ μ‚¬μš©ν•  Executorλ₯Ό 결정함
→ TaskExecutorλΌλŠ” 빈이 λ“±λ‘λ˜μ–΄ 있으면 그것을 μ‚¬μš©
→ μ—†λ‹€λ©΄, κΈ°λ³Έ SimpleAsyncTaskExecutorλ₯Ό μ‚¬μš© (λΉ„μΆ”μ²œ: μŠ€λ ˆλ“œ μž¬μ‚¬μš© X)

비동기 호좜 흐름 관리
→ ν”„λ‘μ‹œκ°€ @Async λ©”μ„œλ“œλ₯Ό ν˜ΈμΆœν•  λ•Œ,
→ ν•΄λ‹Ή λ‘œμ§μ„ λ‹€λ₯Έ μŠ€λ ˆλ“œμ—μ„œ μ‹€ν–‰λ˜λ„λ‘ λΆ„κΈ° 처리
  • μŠ€ν”„λ§μ—μ„œλŠ” 기본적으둜 비동기 처리λ₯Ό μœ„ν•œ μ½”λ“œλ₯Ό μ œκ³΅μ„ ν•΄μ€€λ‹€.
  • @EnableAsync μ• λ„ˆν…Œμ΄μ…˜ 섀정을 ν•˜κ²Œ 되면, μŠ€ν”„λ§ λ‚΄λΆ€μ μœΌλ‘œ 비동기 μ²˜λ¦¬μ— ν•„μš”ν•œ 핡심 κ΅¬μ„±μš”μ†Œλ“€μ„ μžλ™μœΌλ‘œ 빈으둜 λ“±λ‘ν•˜κ³  ν™œμ„±ν™”λ₯Ό ν•΄μ€€λ‹€.
  • μ •μ˜ν•΄λ‘” @Async μ• λ„ˆν…Œμ΄μ…˜μ΄ 호좜이 될 λ•Œ λ§ˆλ‹€ ν•΄λ‹Ή λ©”μ„œλ“œλ₯Ό λΉ„λ™κΈ°λ‘œ 처리λ₯Ό ν•΄μ€€λ‹€.
  • 즉, μŠ€λ ˆλ“œλ₯Ό μƒˆλ‘œ λ§Œλ“€μ–΄μ„œ 처리λ₯Ό ν•΄μ€€λ‹€λŠ” μ˜λ―Έμ΄λ‹€.
@Configuration
@EnableAsync
public class AsyncConfig {
    @Bean
    public Executor taskExecutor() { // μŠ€λ ˆλ“œ ν’€ 직접 μ •μ˜ν•˜λŠ” λ©”μ„œλ“œ
        // μŠ€ν”„λ§μ—μ„œ μ œκ³΅ν•˜λŠ” μŠ€λ ˆλ“œ ν’€ κ΅¬ν˜„μ²΄
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5); // 항상 μœ μ§€λ˜λŠ” μ΅œμ†Œ μŠ€λ ˆλ“œ 수
        executor.setMaxPoolSize(10); // μ΅œλŒ€ μŠ€λ ˆλ“œ 수 μ œν•œ
        executor.setQueueCapacity(100); // 비동기 μž‘μ—… μš”μ²­ λŒ€κΈ° 큐 크기
        executor.setThreadNamePrefix("AsyncExecutor-"); // μŠ€λ ˆλ“œ 이름 prefix μ§€μ •
        executor.initialize(); // μŠ€λ ˆλ“œ ν’€ μ΄ˆκΈ°ν™” λ©”μ„œλ“œ
        return executor;
    }
}
  • Async μ„€μ • νŒŒμΌμ€ 보톡 μœ„μ˜ μ½”λ“œμ™€ 같이 섀정을 ν•˜κ²Œ λœλ‹€. ν•΄λ‹Ή μ„€μ • Bean 은 @Async μ—μ„œ μ‚¬μš©ν•  μŠ€λ ˆλ“œ ν’€(TaskExecutor) 을 직접 μ •μ˜ν•˜λŠ” 곳이닀.
  • setXorePoolSize(5) λ©”μ„œλ“œλŠ” 항상 μœ μ§€λ˜λŠ” μ΅œμ†Œ μŠ€λ ˆλ“œ 수λ₯Ό set ν•  수 μžˆλŠ” setter 이닀. μ‰½κ²Œλ§ν•΄, 비동기 μž‘μ—…μ΄ 없어도 5개의 μŠ€λ ˆλ“œλ₯Ό 기본적으둜 μœ μ§€ν•˜λ©°, μ²˜μŒμ—” μ—¬κΈ°κΉŒμ§€μ˜ μŠ€λ ˆλ“œ 수만큼만 생성이 λœλ‹€λŠ” μ˜λ―Έμ΄λ‹€.
  • setMaxPoolSize(10) λ©”μ„œλ“œλŠ” μ΅œλŒ€ μŠ€λ ˆλ“œ 수 μ œν•œμ„ μ˜λ―Έν•˜λ©°, 큐가 가득 μ°¨κ³  더 λ§Žμ€ μš”μ²­μ΄ λ“€μ–΄μ˜€λ©΄, corePoolSize μ΄μƒμœΌλ‘œλ„ μŠ€λ ˆλ“œλ₯Ό ν™•μž₯ν•΄μ„œ 처리λ₯Ό ν•œλ‹€. ν•˜μ§€λ§Œ 이것도 μ΅œλŒ€λŠ” κΈ°μž¬ν•΄λ‘” 것 κΉŒμ§€ κ°€λŠ₯ν•˜λ‹€.
  • setQueueCapaciry(100) λ©”μ„œλ“œλŠ” 비동기 μž‘μ—… μš”μ²­μ΄ λ“€μ–΄μ˜¬ λ•Œ λŒ€κΈ° ν•  수 μžˆλŠ” 큐의 크기λ₯Ό μ˜λ―Έν•œλ‹€. corePoolSize 만큼 μŠ€λ ˆλ“œκ°€ λ‹€ 찼을 λ•ŒλŠ” 이 큐에 μž‘μ—…μ„ λ„£μ–΄μ„œ λŒ€κΈ°λ₯Ό μ‹œν‚¨λ‹€. μ‰½κ²Œλ§ν•΄, κΈ°μ‘΄ μŠ€λ ˆλ“œμ˜ μ‚¬μš©μ΄ λ‹€ 되고 λ°˜λ‚©μ΄ 되면 μ—¬κΈ°μ„œ κΊΌλ‚΄λ‹€κ°€ μ“΄λ‹€λŠ” μ˜λ―Έμ΄λ‹€. λ§Œμ•½ 큐가 κ½‰μ°¨μ„œ ν•œκ³„μ— λ„λ‹¬ν•˜κ²Œ 되면 μ΄ν›„μ˜ μ €μž₯은 RejectedExecutionException μ˜ˆμ™Έκ°€ λ°œμƒν•˜κ²Œ λœλ‹€. μ΄λ ‡κ²Œ 되면, μš”μ²­μ„ λ³΄λƒΏμ§€λ§Œ, 아무 일도 μ•ˆ μΌμ–΄λ‚˜λŠ” ν˜„μƒμ΄ λ°œμƒ ν•  수 있음. 즉, μš”μ²­μ΄ 증발 ν•  수 있음.
  • initialize() ν•΄λ‹Ή λ©”μ„œλ“œλŠ” μŠ€λ ˆλ“œ 풀을 μ΄ˆκΈ°ν™”ν•˜λŠ” λ©”μ„œλ“œλ‘œ λ°˜λ“œμ‹œ ν˜ΈμΆœν•΄μ•Ό μ‹€μ œλ‘œ μ‚¬μš©μ΄ κ°€λŠ₯ν•˜λ‹€.

(1) μ΄ν›„μ˜ 흐름 μš”μ•½ 

1 ν΄λΌμ΄μ–ΈνŠΈκ°€ HTTP μš”μ²­
2 Controller → Service κ³„μΈ΅μ—μ„œ @Async λ©”μ„œλ“œ 호좜
3 Spring이 taskExecutor()둜 μ •μ˜λœ μŠ€λ ˆλ“œ ν’€μ—μ„œ μ›Œμ»€ μŠ€λ ˆλ“œ 꺼냄
4 비동기 λ©”μ„œλ“œλŠ” 이 μ›Œμ»€ μŠ€λ ˆλ“œμ—μ„œ 싀행됨
5 κΈ°μ‘΄ μš”μ²­ μŠ€λ ˆλ“œλŠ” λ°”λ‘œ ν΄λΌμ΄μ–ΈνŠΈμ—κ²Œ 응닡

 

(2) Async μ‹€ν–‰ μˆœμ„œ

πŸ” @Async의 μ‹€ν–‰ μˆœμ„œ (μš”μ•½)
1. μ‚¬μš©μžκ°€ 비동기 λ©”μ„œλ“œ 호좜
2. corePoolSize 만큼 μŠ€λ ˆλ“œλ‘œ μ‹€ν–‰ (μ¦‰μ‹œ 처리)
3. κ·Έ 이상은 queueCapacity만큼 큐에 μ €μž₯ (λŒ€κΈ° μƒνƒœ)
4. 큐도 꽉 μ°¨λ©΄ → maxPoolSize만큼 μŠ€λ ˆλ“œλ₯Ό μΆ”κ°€ 생성
5. λͺ¨λ“  ν•œκ³„ λ‹€ 찼을 λ•Œ → RejectedExecutionException λ°œμƒ

βœ… 3. Async λ©”λͺ¨λ¦¬ 기반 Queue의 νœ˜λ°œμ„± 및 μž‘μ—… λˆ„λ½ 문제

  • @Async κ°€ μ‚¬μš©ν•˜λŠ” ThreadPoolTaskExecutor λ‚΄λΆ€μ—λŠ” μž‘μ—…μ„ λ³΄κ΄€ν•˜λŠ” 큐(Queue) κ°€ μ‘΄μž¬ν•¨.
  • 이 νλŠ” μžλ°” μ• ν”Œλ¦¬μΌ€μ΄μ…˜ λ©”λͺ¨λ¦¬ μ•ˆμ—μ„œλ§Œ μœ μ§€λ˜λŠ” μžλ£Œκ΅¬μ‘°μ΄λ‹€ λ³΄λ‹ˆ, JVM ν”„λ‘œμ„ΈμŠ€κ°€ μ’…λ£Œλ˜κ±°λ‚˜ μž¬μ‹œμž‘ 되면 큐 μ•ˆμ— 있던 μž‘μ—… 정보가 λͺ¨λ‘ μ‚¬λΌμ§ˆ 수 μžˆλ‹€. 즉, λ©”λͺ¨λ¦¬ 문제, μ„œλ²„ μž₯μ• , 재배포 μ‹œμ— 비동기 μž‘μ—…μ΄ λ‚ μ•„κ°ˆ μœ„ν—˜μ΄ μžˆλ‹€λŠ” μ˜λ―Έμ΄λ‹€.
  • μ΄λ ‡κ²Œ λœλ‹€λ©΄ ν΄λΌμ΄μ–ΈνŠΈμ˜ μ€‘μš”ν•œ μš”μ²­μ΄ λ‚ λΌκ°€λŠ” λ¬Έμ œκ°€ λ°œμƒ ν•  수 있게 λœλ‹€.

(1) 이게 문제인 이유

  • 비동기 μž‘μ—…μ—λŠ” 주둜 μ€‘μš”ν•œ μž‘μ—…μΈ 이메일 전솑, μ•Œλ¦Ό, 결제 ν›„μ²˜λ¦¬ 등을 처리 ν•˜λŠ”λ° μ‚¬μš©μ„ ν•˜λŠ”λ°, 이 μž‘μ—…μ΄ λˆ„λ½λ˜λ©΄ μ„œλΉ„μŠ€ 신뒰성에 직접적인 μ•…μ˜ν–₯을 μ£ΌκΈ° λ•Œλ¬Έμ΄λ‹€.

βœ… 4. Queue의 νœ˜λ°œμ„± 및 μž‘μ—… λˆ„λ½ 문제 ν•΄κ²° λ°©μ•ˆ

(1) μŠ€λ ˆλ“œν’€ λͺ¨λ‹ˆν„°λ§

  • 큐 μ‚¬μ΄μ¦ˆλ₯Ό λ„ˆλ¬΄ μž‘κ²Œ ν•˜λ©΄ μš”μ²­ ν•œκ³„λ₯Ό λ²—μ–΄λ‚  λ•Œ RejectedExecutionException λ°œμƒ
  • 큐 μ‚¬μ΄μ¦ˆλ₯Ό λ„ˆλ¬΄ 크게 μ„€μ •ν•˜λ©΄ λ©”λͺ¨λ¦¬ μ‚¬μš©λŸ‰ 증가, μž‘μ—… μ§€μ—° κ°€λŠ₯성이 증가함.
  • ThreadPoolWaskExecutor 의 큐 크기, ν™œμ„± μŠ€λ ˆλ“œ 수, μ™„λ£Œ μž‘μ—… 수 λ“± ν˜„μž¬ μƒνƒœλ₯Ό Spring 의 Actuator λ‚˜ JMX, Prometheus + Grafana 같은 μ‹€μ‹œκ°„ λͺ¨λ‹ˆν„°λ§ 도ꡬλ₯Ό ν™œμš©ν•΄μ„œ λͺ¨λ‹ˆν„°λ§μ„ ν•  수 μžˆλ‹€.
  • μ΄λŸ¬ν•œ μ‹€μ‹œκ°„ λͺ¨λ‹ˆν„°λ§μ„ 톡해 큐가 μ‚¬μš©μžμ˜ μ¦κ°€λ‘œ 인해 큐가 κ°€λ“μ°¨λŠ” ν˜„μƒμ„ 발견 ν•  수 있으며, μŠ€λ ˆλ“œ 수λ₯Ό λŠ˜λ¦¬κ±°λ‚˜ μ„œλ²„ μžμ›μ„ μΆ”κ°€ν•¨μœΌλ‘œμ¨ 문제λ₯Ό ν•΄κ²° ν•  수 μžˆλ‹€.
  • 즉, 해결을 μœ„ν•΄μ„œλŠ” λ‘œκ·Έλ‚˜ λͺ¨λ‹ˆν„°λ§μ„ 톡해 큐 크기λ₯Ό μ μ ˆν•œ μˆ˜μ€€μœΌλ‘œ μ„€μ •ν•˜κ±°λ‚˜ νŠœλ‹ν•˜λŠ” 것이 μ’‹μŒ.

(2) 거절 μ •μ±…(RejectedExecutionHandler) μ„€μ •

  • μž‘μ—… μš”μ²­μ΄ λ„ˆλ¬΄ λ§Žμ•„ 큐와 μŠ€λ ˆλ“œκ°€ λ‹€ 찼을 λ•Œ 둜그λ₯Ό 남기고 λ²„λ¦¬λŠ” μ •μ±… λ“± μ μ ˆν•œ λ°©μ–΄ λ‘œμ§μ„ λ‘λŠ” 방식이닀.
  • ν•˜μ§€λ§Œ, 이 방식은 근본적인 문제 해결이라기 보단 증상 μ™„ν™”λ‚˜ μž„μ‹œ λŒ€μ‘μ— κ°€κΉŒμš΄ 방식이닀.

(3) MQ(Message Queue) μ‚¬μš©

  • MQ λŠ” 비동기 μž‘μ—…μ΄ 생성이 되면 ν•΄λ‹Ή μž‘μ—…μ„ λ©”μ‹œμ§€λ‘œ 보고, Queue 에 μ €μž₯을 ν•˜λŠ” 방식이닀.
  • ν•΄λ‹Ή MQ 의 μ’…λ₯˜λŠ” λŒ€ν‘œμ μœΌλ‘œ RabbitMQ, Kafka 두 κ°€μ§€κ°€ μžˆλ‹€.
  • 두 MQ λŠ” λ©”μ‹œμ§€λ₯Ό λ””μŠ€ν¬ 기반 큐에 μ €μž₯을 ν•˜κΈ° λ•Œλ¬Έμ—, κΈ°μ‘΄ 인메λͺ¨λ¦¬ 비동기 μž‘μ—…μ΄ μ‚¬λΌμ§€λŠ” 문제λ₯Ό ν•΄κ²°ν•΄μ€€λ‹€.
  • λ©”μ‹œμ§€κ°€ λ””μŠ€ν¬μ— μ €μž₯λ˜μ–΄ μ„œλ²„ μž₯애에도 μž‘μ—… 손싀이 μ—†λŠ” 내ꡬ성을 보μž₯ν•œλ‹€.
  • μ†ŒλΉ„μž 수 쑰절둜 λΆ€ν•˜ λΆ„μ‚° 및 μŠ€μΌ€μΌ 아웃이 κ°€λŠ₯ 즉, μ²˜λ¦¬λŸ‰ 쑰절이 κ°€λŠ₯ν•˜λ‹€λŠ” μ˜λ―Έμ΄λ‹€.
  • 외뢀에 λΆ„λ¦¬λœ 큐 μ‹œμŠ€ν…œμœΌλ‘œ μ•ˆμ •μ μ΄κ³  μœ μ—°ν•œ 비동기 처리λ₯Ό κ°€λŠ₯ν•˜κ²Œ λ„μ™€μ€Œ.
  • μ‹€νŒ¨ν•œ λ©”μ‹œμ§€λ₯Ό 별도 큐둜 보내 재처리 κ°€λŠ₯ν•˜κ²Œ λ„μ™€μ€Œ.
  • μ—¬λŸ¬ μ„œλ²„κ°€ λ©”μ‹œμ§€λ₯Ό λ‚˜λˆ  μ²˜λ¦¬κ°€ κ°€λŠ₯함. 즉, λΆ„μ‚° ν™˜κ²½μ„ μ§€μ›ν•΄μ€€λ‹€λŠ” μ˜λ―Έμž„. 맀우 쒋은 μž₯점인듯..

βœ… 5. 정리

  • @AsyncλŠ” λ‚΄λΆ€μ μœΌλ‘œ μŠ€λ ˆλ“œν’€(ThreadPoolTaskExecutor) 을 μ΄μš©ν•΄ 비동기 μž‘μ—…μ„ μ‹€ν–‰ν•œλ‹€.
  • κ°„λ‹¨ν•˜κ³  μž‘μ€ 규λͺ¨ λ‚΄λΆ€ 비동기 μž‘μ—…μ΄λΌλ©΄ @Async κ°€ λΉ λ₯΄κ³  κ°„νŽΈν•˜μ§€λ§Œ μ•ˆμ •μ„±, ν™•μž₯μ„±, λΆ„μ‚°μ²˜λ¦¬κ°€ ν•„μš”ν•˜κ±°λ‚˜ μž₯μ•  μ‹œμ—λ„ λ©”μ‹œμ§€ μœ μ‹€μ„ 막아야 ν•œλ‹€λ©΄ MQλ₯Ό μ‚¬μš©ν•˜λŠ”κ²Œ 훨씬 λ‚«λ‹€κ³  νŒλ‹¨μ΄ 됨.