1_[spring]spring 비동기처리 맛보기
비동기 처리하기
- Application.java 부분에 @EnableAsync 를 붙여주자
package com.example.async;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;
@SpringBootApplication
**@EnableAsync**
public class AsyncApplication {
public static void main(String[] args) {
SpringApplication.run(AsyncApplication.class, args);
}
}
2.간단하게, 실질적으로 비즈니스 로직을 담당하는 서비스 부분(@Service)에서 비동기를 유발시키도록 특정 메서드의 윗부분에 @Async 어노테이션을 붙여주자
package com.example.async.service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
@Service
@Slf4j
public class AsyncService {
**@Async**
public void hello(){
for(int i = 0 ; i < 10; i++){
try {
Thread.sleep(2000);
log.info("thread sleep---#{}",i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.info("service end");
}
}
3.그리고 Rest Api Controller에서 이 비동기를 유발하는 메서드를 부르도록 하자
package com.example.async.controller;
import com.example.async.service.AsyncService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api")
**@RequiredArgsConstructor**
public class RestApiController {
private final AsyncService asyncService;
@GetMapping("/hello")
public String hello(){
System.out.println("hello start in rest");
asyncService.hello();
System.out.println("hello end in rest");
return "hello";
}
}
지금 정리해보면, http://localhost:8089/api/hello 로 요청을 보내면
hello()메서드로 인해서 작동되어야 할 기능들은 아래와 같다
- System.out.println(“hello start in rest”);
- Service단의 hello 메서드 -for 루프를 돌면서 2초 간격으로 로깅 -for루프 후 “service end” 로그 찍기
- System.out.println(“hello end in rest”);
- plain text response로 “hello “ 내려주기
원래 비동기가 아니었다면, 지금 적은 순서대로 수행되어야 하지만 @Async, @EnableAsync로 인해서 동시에 작업이 이루어 지면서 작업이 종료되는 대로 결과가 반환되는 것을 확인해볼 수 있다
위의 경우 response가 먼저 확인되었고, 그 후 콘솔에 “hello start in rest”와 “hello end in rest”가 순서대로 찍힌 후에, service단의 hello 메서드가 찍히는 것을 확인해볼 수 있었다
이를 조금 더 명확하게 살펴보기 위해서 컨트롤러에서도 로깅을 해보자
package com.example.async.controller;
import com.example.async.service.AsyncService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RestController
@RequestMapping("/api")
@RequiredArgsConstructor
public class RestApiController {
private final AsyncService asyncService;
@GetMapping("/hello")
public String hello(){
log.info("hello start in rest");
asyncService.hello();
log.info("hello end in rest");
return "hello";
}
}
그러면 이를 통해서 서로 다른 스레드에서 작업이 진행되고 있음을 확인해볼 수 있다
Async 작업에 따른 response 처리
1.먼저 Application.java 에서 EnableAsync를 그대로 유지해주자
package com.example.async;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;
@SpringBootApplication
@EnableAsync
public class AsyncApplication {
public static void main(String[] args) {
SpringApplication.run(AsyncApplication.class, args);
}
}
2.서비스에서는 hello에서 Async를 지우고,CompletableFuture 형을 반환하는 run()메서드를 만들자! 이때 run 메서드에서는 AsyncResult(hello()),completable()을 반환한다. 그리고 run에 Async를 붙이자
- CompleatableFuture: 비동기 작업시 2개 이상의 스레드에 대한 정보를 받을 수 있는 클래스
- AsyncResult : 비동기 작업 결과를 Future로 반환
- public CompletableFuture
completable() : AsyncResult를 CompletableFuture로 만들기
CompletableFuture (Java Platform SE 8 )
AsyncResult (Spring Framework 5.3.9 API)
package com.example.async.service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.AsyncResult;
import org.springframework.stereotype.Service;
import java.util.concurrent.CompletableFuture;
@Service
@Slf4j
public class AsyncService {
**@Async**
**public CompletableFuture run(){
return new AsyncResult(hello()).completable();
}**
//@Async
public String hello(){
for(int i = 0 ; i < 10; i++){
try {
Thread.sleep(2000);
log.info("thread sleep---#{}",i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.info("service end");
return "hello";
}
}
3,RestApiController에서는 내부적으로 서비스의 run()메서드를 hello()메서드 내부에서 부르도록 하자
package com.example.async.controller;
import com.example.async.service.AsyncService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.CompletableFuture;
@Slf4j
@RestController
@RequestMapping("/api")
@RequiredArgsConstructor
public class RestApiController {
private final AsyncService asyncService;
@GetMapping("/hello")
public CompletableFuture hello(){
// log.info("hello start in rest");
// asyncService.hello();
// log.info("hello end in rest");
// return "hello";
log.info("completable future");
**return asyncService.run();**
}
}
CompletableFuture는 다른 스레드에서 해당 작업을 수행할 수 있도록 지원해준다!
위의 경우, log.info(“completable future”);의 스레드와 “ Async와 CompletableFuture가 붙은 메서드”의 스레드가 서로 다르게 돌아가고 있음을 확인해볼 수 있다
🌟 비동기를 적용하기 알맞은 예시 🌟
- 여러개의 api를 전송 후 그 결과를 조인할 경우
✴️ 위의 경우는 기본적으로 스프링에서 정한 스레드 최대 범주(약 최대 8개)에서 비동기 작업을 수행 가능 ▶️ 하지만, 이 부분이 한계가 될 수 있다! 직접 스레드를 만들어 지정해보자
🌟 Async : AOP 기반이기 때문에 프록시 패턴을 따른다!
🌟 하나의 메서드에서 Async를 붙인 상황에서 내부적으로 메서드를 호출하면 그 내부 메서드에는 Async가 붙지 않음
(심화)쓰레드를 직접 만들어서 해당 쓰레드를 이용하도록 하기
- 이 방법은 경험이 쌓이면서 접근해보고, 웬만하면 스프링 웹 MVC를 이용하는 것을 권장[DB와 연동시, MySQL의 경우 Spring 웹 flux와 같이 사용하는 것을 권장하지만, 실상 DB는 동기방식으로 진행하기 때문에 async가 소용없음]
📌 ThreadPoolTaskExecutor
- 쓰레드풀을 이용해서 멀티 쓰레드 구현을 할 수 있도록 도와줌
ThreadPoolTaskExecutor (Spring Framework 5.3.9 API)
- setCorePoolSize 메서드 - 동시에 실행시킬 수 있는 쓰레드 갯수 설정
- setMaxPoolSize 메서드 - 쓰레드 풀의 최대 사이즈
- setQueueCapacity 메서드 : 쓰레드 corePoolSize가 다 차면 큐의 용량만큼 사용하고, 큐도 다 사용하면 corePoolSize로 지정한 갯수만큼 corePoolSize가 늘어나서 사용하고, 또 다 사용하면 큐 용량만큼 이용 반복
- setThreadNamePrefix 메서드 : 쓰레드 이름 접미사 설정
쓰레드를 만들 클래스를 AppConfig로 하고, 빈 객체로 등록하자
- 쓰레드의 이름 접미사는 “Async-“로 설정
package com.example.async.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
@Configuration
public class AppConfig {
@Bean("async-thread")
public Executor asyncThread(){
**ThreadPoolTaskExecutor threadPoolTaskExecutor=
new ThreadPoolTaskExecutor();
threadPoolTaskExecutor.setMaxPoolSize(100);
threadPoolTaskExecutor.setCorePoolSize(10);//core pool 10개를 다 쓰게 되면 큐로 넘어감
//그러다가 큐도 다 차면, core는 인자값으로 넣어준 크기만큼 또 생김
threadPoolTaskExecutor.setQueueCapacity(10);
threadPoolTaskExecutor.setThreadNamePrefix("Asnyc-");
return threadPoolTaskExecutor;**
}
}
그리고 기존의 서비스의 비동기 메서드에 대해서 Async(“빈 객체명”)으로 수정해주자
package com.example.async.service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.AsyncResult;
import org.springframework.stereotype.Service;
import java.util.concurrent.CompletableFuture;
@Service
@Slf4j
public class AsyncService {
**@Async("async-thread")**
public CompletableFuture run(){
return new AsyncResult(hello()).completable();
}
//@Async
public String hello(){
for(int i = 0 ; i < 10; i++){
try {
Thread.sleep(2000);
log.info("thread sleep---#{}",i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.info("service end");
return "hello";
}
}
2021-08-04 02:07:45.407 INFO 40524 --- [nio-8089-exec-1] c.e.async.controller.RestApiController : completable future
2021-08-04 02:07:47.426 INFO 40524 --- [ Asnyc-1] com.example.async.service.AsyncService : thread sleep---#0
2021-08-04 02:07:49.437 INFO 40524 --- [ Asnyc-1] com.example.async.service.AsyncService : thread sleep---#1
2021-08-04 02:07:51.440 INFO 40524 --- [ Asnyc-1] com.example.async.service.AsyncService : thread sleep---#2
2021-08-04 02:07:53.447 INFO 40524 --- [ Asnyc-1] com.example.async.service.AsyncService : thread sleep---#3
2021-08-04 02:07:55.457 INFO 40524 --- [ Asnyc-1] com.example.async.service.AsyncService : thread sleep---#4
2021-08-04 02:07:57.464 INFO 40524 --- [ Asnyc-1] com.example.async.service.AsyncService : thread sleep---#5
2021-08-04 02:07:59.472 INFO 40524 --- [ Asnyc-1] com.example.async.service.AsyncService : thread sleep---#6
2021-08-04 02:08:01.478 INFO 40524 --- [ Asnyc-1] com.example.async.service.AsyncService : thread sleep---#7
2021-08-04 02:08:03.481 INFO 40524 --- [ Asnyc-1] com.example.async.service.AsyncService : thread sleep---#8
2021-08-04 02:08:05.488 INFO 40524 --- [ Asnyc-1] com.example.async.service.AsyncService : thread sleep---#9
2021-08-04 02:08:05.488 INFO 40524 --- [ Asnyc-1] com.example.async.service.AsyncService : service end
그러면 더이상 Task-1이 아닌 지정했던 접미사를 이용한 Async-1이라는 스레드가 확인된다!