본론에 들어가기 전에 동기와 비동기에 대해서 알아보자.
동기(Syncronized)
순차적인 작업 순서를 보장하는 것이 동기 방식이다.
동기 방식은 호출한 쪽(Caller)에서 응답이 오는 것에 대해서 신경쓰는 것을 의미한다.
비동기(Asyncronized)
작업 순서가 순차적이지 않을 수 있는 것이 비동기 방식이다.
비동기 방식은 호출한 쪽(Caller)에서 응답이 오는 것을 신경쓰지 않고, 호출된 쪽(Callee)에서 작업이 완료된 후, 콜백(Callback)을 통해 응답을 호출한쪽에 전달한다.
웹 어플리케이션 서버를 개발하는 과정에서 우리는 어느 관점에서 ‘비동기’ 개념을 사용할 수 있을까?
비동기 로직을 사용하면 ‘응답을 기다리는 시간’을 효율적으로 사용할 수 있기 때문에, 시간적인 측면에서 이점이 있다. 따라서 요청이 많거나, 특정 로직에 시간이 오래 소요될 경우, 비동기 로직을 사용하면 효율적으로 시간을 절약할 수 있다.
또한, 요청에 대한 주 로직, 보조 로직에 따라서 보조 로직이 주 로직의 행위에 영향을 끼칠 때, 보조 로직을 비동기 처리하여 주 로직의 트랜잭션과 보조 로직의 트랜잭션을 분리할 수 있다.
마지막으로 DB, 3rd party API 호출등의 지연으로 인해 Tomcat 스레드 풀의 요청 스레드가 오랫동안 살아 있는 경우이다. 동기 방식의 경우, 외부 API 호출의 응답을 기다리느라 block 상태의 스레드를 유지하지만, 비동기 방식의 경우 외부 API 호출을 기다리는 제약을 제거할 수 있다.
위 내용을 정리하면 비동기가 사용되는 관점은 다음과 같다.
@Async
를 사용해보자.동기 방식으로 작동하는 메서드와 비동기 방식으로 작동하는 메서드를 통해 비동기의 효과를 체험해보자.
먼저 동기 방식으로 작동하는 HTTP 호출 ~ 응답 시나리오는 다음과 같다.
a. 동기 방식으로 비즈니스 로직을 수행했을 경우.
RestSampleController - syncCall()
SyncService의 sync() 호출
SyncService - sync()
5초 걸리는 Task()를 3번 호출
StopWatch를 통해 sync() api 응답 시간 출력
5초가 걸리는 메서드를 반복문을 통해 3번 호출하였으므로, 5 * 3 초가 수행될 것이다.
실행 결과는 다음과 같다.
동기 방식은 보는것처럼 순차적으로 실행되기 때문에 5초가 걸리는 Task를 3번 수행하는데 15초의 시간이 걸리는 것을 확인할 수 있다.
그럼 비동기 방식으로 작동하도록 변경하면 어떤 결과가 발생할까?
b. 비동기 방식으로 비즈니스 로직을 수행했을 경우.
Spring 에서 비동기 처리를 하기 위해선 비동기처리를 위한 스레드들을 관리해줄 수 있는 Thread Pool을 Bean으로 등록하는 과정이 필요하다.
주의❗
위 과정이 없으면 @Async
를 사용하더라도 비동기방식으로 동작하지 않는다.
AsyncConfig - threadPoolTaskExecutor()
corePoolSize : 기본 생성 스레드 갯수
maxPoolSize : 최대 스레드 갯수
queueCapacity : 대기 스레드 최대 갯수
기본으로 생성되는 스레드 갯수는 5개
최대로 생성될 수 있는 스레드 갯수는 10개
대기 스레드의 최대 갯수를 20개로 설정한 ThreadPool를 Bean으로 등록하였다.
실제로 시간이 단축되었는지 테스트해보자.
/async API
를 호출했을 때 호출되는 메서드인 fiveSecondsTask()는 비동기 방식으로 호출자는 해당 메서드의 return 값을 기대하지 않으므로(void), 한번의 호출은 응답까지 5초 미만(1초==1000ms)의 시간이 걸릴 것을 테스트 해보았다.
테스트 결과는 다음과 같다.
시간 값으로 테스트를 해본다는 것이 부정확한 검증 지표이지만, 동기 방식보다 비동기 방식으로 진행했을 때, 시간을 단축하는 것을 확인해볼 수 있었다.
다음 포스팅에선 서비스의 요구사항 중 동기 처리로 인해 발생할 수 있는 문제를 비동기로 처리하는 과정을 소개하도록 하겠다.