Notice
Recent Posts
Recent Comments
Link
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
Tags
more
Archives
Today
Total
관리 메뉴

잡동사니를 모아두는 서랍장

스프링의 간단한 Async 처리 본문

Spring

스프링의 간단한 Async 처리

kingkk31 2020. 2. 27. 21:07

일하면서 요청이 들어오면 처리 후 응답을 해주는게 아니라 비동기적으로 선 응답 후 이벤트로 결과를 알려주는 방식으로 구현해야 하는 일이 생겼다. 비동기 응답에 대해서 생 자바부터 스프링 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 되지 않고 곧바로 다음 로직을 타고 리턴된다. 

 

 

참고

Comments