고딩왕 코범석

@Async, 비동기 기능 본문

Language & Framework/Spring

@Async, 비동기 기능

고딩왕 코범석 2021. 2. 16. 22:12
반응형

안녕하세요! 오늘은 제가 "스프링과 JPA 기반 웹 애플리케이션 개발"을 클론코딩하면서 보았던 비동기 개념에 대해 배우고 정리해보겠습니다.


우선, 저는 동기와 비동기, 블로킹과 논블로킹이라는 것에 대해 좀 생소했었는데, 여기를 통해 개념이 좀 많이 잡혔습니다. 제가 이 네가지 개념에 대해 작성하는 것 보다는 참조하는 것이 훨씬 여러분들께 도움이 될 것 같아 저 링크를 참조하시면 될 것 같습니다.


이제 위 네가지 개념에 대해 알았다면, 본격적으로 실습해보겠습니다. 저는 이 게시글을 참조해서 작성했습니다. 제 설명이 부족하다면 해당 게시글을 참조하시면 될 것 같습니다.


Configuration

스프링에서 비동기 작업을 하기 위해서는 Configuration을 등록해주어야 합니다.


@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {

    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(2);                // 실행을 대기하고 있는 쓰레드 갯수
        executor.setMaxPoolSize(2);                // 동시에 동작하는 최대 쓰레드 갯수
        /**
         * QueueCapacity : MaxPoolSize를 초과하는 요청이 Thread 생성 요청시
         * 해당 내용을 큐에 저장한다.
         * 사용할 수 있는 Thread 여유 자리가 발생하면 큐에서 하나씩 꺼내서 동작한다.
         */
        executor.setQueueCapacity(2);
        executor.setThreadNamePrefix("Async-");     // 스프링이 생성하는 쓰레드의 접두사
        executor.initialize();                      // 초기화
        return executor;
    }
}

AsyncConfigurer 인터페이스를 상속 받아 getAsyncExecutor를 재정의합니다. ThreadPoolTaskExecutor는 비동기 실행을 위해 사용됩니다. @EnableAsync가 있는 Configuration 클래스에서 스프링이 ThreadPoolTaskExecutor bean을 찾아 셋팅한 값들을 읽어들입니다.


ThreadPoolTaskExecutor setter 역할들은 주석에 적혀있습니다. setCorePoolSize, setMaxPoolSize, setQueueCapacity를 왜 작게 해주었는지는 예제를 실행시킬 때 살펴보겠습니다.


@Slf4j
@Service
@RequiredArgsConstructor
public class AsyncService {

    /**
     * public 접근, void 혹은 Future 반환형
     * 이 메서드를 호출한(ex) Controller) 쓰레드는
     * Non-blocking으로 동작한다.
     */
    @Async
    public void onAsync() {
        try {
            Thread.sleep(5000);
            log.info("onAsync");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public void onSync() {
        try {
            Thread.sleep(5000);
            log.info("onSync");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

@Slf4j
@RestController
@RequiredArgsConstructor
public class AsyncController {

    private final AsyncService asyncService;

    @GetMapping("/async")
    public String goAsync() {
        asyncService.onAsync();
        String ret = "비동기 호출, 위치 = Controller";
        log.info(ret);
        log.info("========================================================================");
        return ret;
    }

    @GetMapping("/sync")
    public String goSync() {
        asyncService.onSync();
        String ret = "동기 호출, 위치 = Controller";
        log.info(ret);
        log.info("========================================================================");
        return ret;
    }
}

서비스와 컨트롤러 클래스를 만들었습니다. 이제 run을 시키고 나서 postman을 이용해 요청을 보내보겠습니다.


image


동기 방식으로 호출하면 서비스 로직에서 5초간 멈추고 난 다음, onSync를 로그로 출력하고 Controller에서 동기로 호출, 위치를 로그로 남겨주었습니다. 이 동작은 동기 + 블로킹 방식으로 호출되었네요!


이제 비동기 방식으로 호출해보겠습니다!


image


이 동작은 비동기 + 논블로킹 방식으로 호출되었네요! 서비스의 비동기 메서드에 @Async가 붙어있어 /async를 컨트롤러에서 호출했을 때, 컨트롤러 로직은 비동기로 호출, 위치를 로그로 남겨주었고, 서비스 메서드에서는 5초 뒤에 onAsync가 로그로 남겨졌습니다.


이제 네 번 연속으로 따닥따닥 눌러보면!


image


이렇게 출력이 됩니다! 제가 Configuration 했을 때, 접두사를 Async-로 했기 때문에 로그에서는 Async-N 이라고 출력이 되었습니다. 이 때, 저는 동시에 쓰레드를 실행시킬 수 있는 Max값을 2로 설정했고, QueueCapacity도 2로 설정했기 떄문에


image


제가 드래그한 Async-1, -2는 따닥 바로 로그가 남겨집니다. 드래그하지 않은 Async-1, -2는 드래그한 로그를 호출하고 나서 5초 뒤에 출력이 되는 사실을 알 수 있습니다. 요청의 최대 크기를 2로 설정했기 때문에 동시에 네번 요청했을 경우, 2번째 요청까지는 바로 실행되지만, 나머지 두번의 경우는 큐에서 대기하고 있는 것이죠. 만약, 여기서 다섯번을 실행하면 어떻게 될까요?


image


오류 로그를 자세히 보면 [Running, pool size = 2, active threads = 2, queued tasks = 2, completed tasks = 14]라 적혀있습니다. 풀 사이즈가 2, 진행중인 쓰레드가 2, queue에 대기중인 task가 2기 때문에 다섯번째 요청에는 에러가 났네요. 이러한 에러를 처리하는 것도 따로 해줘야 될 것 같습니다. 아직은 제가 많이 초보여서 이 포스팅에서는 에러를 처리하는 것은 다루지 못할 것 같아요.. 죄송합니다.

이번 포스팅은 간단하게 스프링에서 동기, 비동기 방식으로 동작하는 과정들을 예제로 살펴보았습니다. 언제나 피드백은 고맙게 받습니다!

반응형