잡동사니를 모아두는 서랍장
스프링의 간단한 Async 처리 본문
일하면서 요청이 들어오면 처리 후 응답을 해주는게 아니라 비동기적으로 선 응답 후 이벤트로 결과를 알려주는 방식으로 구현해야 하는 일이 생겼다. 비동기 응답에 대해서 생 자바부터 스프링 async 까지 한번씩 건들여봤다.
1. 직접 쓰레드를 새로 생성하는 방법
당장 생각나는 방법은 가장 무식한 방법. 컨트롤러에서 요청을 받았을 때 비즈니스 로직이 구현된 서비스를 호출하지 않고 중간에 대신 실행해주는 서비스를 호출하는 방식. 대행하는 서비스는 쓰레드를 생성해 비즈니스 로직이 구현된 서비스를 호출한다.
@Slf4j
@Service
public class ServiceTest {
public void getTest() {
try {
Thread.sleep(5000); // 명확한 확인을 위해 5초 sleep을 걸었다.
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("This is service");
}
}
@Slf4j
@Service
public class ServiceExecutor {
@Autowired
ServiceTest serviceTest;
private ExecutorService executorService = Executors.newSingleThreadExecutor();
public Object execute() {
this.executorService.execute(() -> {
try {
log.info("execute");
this.serviceTest.getTest();
} catch (Exception e) {
e.printStackTrace();
}
});
return null;
}
}
@Slf4j
@RestController
public class ControllerTest {
@Autowired
private ServiceExecutor serviceExecutor;
@Autowired
private ServiceTest serviceTest;
@RequestMapping(value = "/test/sync", method = RequestMethod.GET)
public Object getTestSync() {
log.info("[sync] start");
this.serviceTest.getTestSync();
log.info("[sync] end");
return new ResponseEntity(HttpStatus.OK);
}
}
결과는 당연히 먼저 응답하고 나중에 서비스의 로그가 찍힌다. 포스트맨으로 요청해보면 바로 응답이 온다.
2020-03-02 15:19:47 [http-nio-8888-exec-1] INFO c.k.t.a.controller.ControllerTest - [test] start
2020-03-02 15:19:47 [http-nio-8888-exec-1] INFO c.k.t.a.controller.ControllerTest - [test] end
2020-03-02 15:19:47 [pool-1-thread-1] INFO c.k.t.async.service.ServiceExecutor - execute
2020-03-02 15:19:52 [pool-1-thread-1] INFO c.k.test.async.service.ServiceTest - This is service
근데 스프링 부트 프로젝트인데...내가 이걸 일일이 다 만들어야 할까?
거의 생 자바 방식. 좀 더 좋게 바꾸고 싶다. 팀분들께 조언을 구해봤다.
2. @Async 어노테이션 사용을 사용하는 방법
스프링에서는 비동기로 처리하고 싶은 메소드를 @Async 어노테이션을 사용하여 비동기로 만들 수 있다.
사전에, Configuration에 @EnableAsync 를 추가하여 @Async 를 사용할 수 있게 한다.
스프링은 기본값으로 SimpleAsyncTaskExecutor 를 사용하여 실제 메소드들을 비동기로 실행한다고 한다.
하지만 SimpleAsyncTaskExecutor는 쓰레드를 계속 생성해낸다(쓰레드 풀이 아님). 쓰레드가 계속 생겨날 수 있으므로 비효율적이다.
때문에 쓰레드 풀을 빈으로 등록해서 사용하게 한다. 여기선 ThreadPoolTaskExecutor를 설정하여 쓰레드 풀을 사용하게했다. 풀 사이즈는 상황에 따라 다르겠지만...일단 코어를 기준으로 했다.
@Configuration
@EnableAsync
public class ConfigurationTest {
@Bean
public Executor asyncThreadTaskExecutor() {
ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
threadPoolTaskExecutor.setCorePoolSize(Runtime.getRuntime().availableProcessors());
threadPoolTaskExecutor.setMaxPoolSize(Runtime.getRuntime().availableProcessors() * 2);
threadPoolTaskExecutor.setThreadNamePrefix("async-thread-pool");
return threadPoolTaskExecutor;
}
}
설정은 끄읕.
자, 그럼 처음의 소스 코드를 수정해보자. 비교를 위해서 sync 동작부분도 같이 기재했다.
둘의 차이는 하나밖에 없다. 비동기로 처리하고 싶은 메소드에 @Async 를 추가한다. 그럼 getTestSync는 동기, getTestAsync는 비동기로 동작한다. 대행하는 서비스가 필요없어졌으므로 컨트롤러에서 ServiceExecutor를 제거했다.
@Slf4j
@Service
public class ServiceTest {
//async로 동작
@Async
public void getTestAsync() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("This is async service");
}
//비교를 위한 sync
public void getTestSync() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("This is sync service");
}
}
@Slf4j
@RestController
public class ControllerTest {
@Autowired
private ServiceTest serviceTest;
@RequestMapping(value = "/test/async", method = RequestMethod.GET)
public Object getTestAsync() {
log.info("[async] start");
this.serviceTest.getTestAsync();
log.info("[async] end");
return new ResponseEntity(HttpStatus.OK);
}
@RequestMapping(value = "/test/sync", method = RequestMethod.GET)
public Object getTestSync() {
log.info("[sync] start");
this.serviceTest.getTestSync();
log.info("[sync] end");
return new ResponseEntity(HttpStatus.OK);
}
}
실행결과
//sync api
2020-03-02 15:19:54 [http-nio-8888-exec-2] INFO c.k.t.a.controller.ControllerTest - [sync] start
2020-03-02 15:19:59 [http-nio-8888-exec-2] INFO c.k.test.async.service.ServiceTest - This is sync service
2020-03-02 15:19:59 [http-nio-8888-exec-2] INFO c.k.t.a.controller.ControllerTest - [sync] end
//async api
2020-03-02 15:20:01 [http-nio-8888-exec-3] INFO c.k.t.a.controller.ControllerTest - [async] start
2020-03-02 15:20:01 [http-nio-8888-exec-3] INFO c.k.t.a.controller.ControllerTest - [async] end
2020-03-02 15:20:06 [async-thread-pool1] INFO c.k.test.async.service.ServiceTest - This is async service
sleep을 걸어놨기 때문에 sync api는 5초후에 로그 출력하고 리턴된다. 포스트맨으로도 응답이 5초 걸렸다가 온다.
async api의 경우는 응답이 바로온다! 이전에 등록해놓은 쓰레드풀에서 비동기로 등록한 메소드를 실행시키기 때문에 요청을 받았던 쓰레드는 sleep 되지 않고 곧바로 다음 로직을 타고 리턴된다.
참고
'Spring' 카테고리의 다른 글
Request Body를 따로 읽고 싶을 때 (0) | 2020.04.21 |
---|---|
Request Body를 인터셉터에서 읽고 싶을 때 (0) | 2020.04.20 |
인터셉터와 필터 차이 (0) | 2020.02.23 |
PropertySourcesPlaceholderConfigurer로 프로퍼티로 지정하기 (0) | 2020.02.22 |
스프링 프로퍼티 우선 순위 (0) | 2020.02.07 |