Webflux와 MVC의 스레드 구조에 대해
MVC VS Webflux
스프링에는 웹 서버를 다루기 위해 크게 2개의 프레임워크가 있다.
spring-boot-starter-web과 spring-boot-starter-webflux가 바로 그것이다.
전자는 스프링 MVC라고도 불리는, 서블릿 스펙을 준수하는 동기 호출 기반의 웹 서버를 제작하는 프레임워크이며, 후자는 Reactive Streams를 구현한 리액터 기반의 프레임워크이다.
일반적으로는 전자가 주로 쓰이며, 그 이유는 보통 프로그래밍을 할 때 대체로 블로킹 기반의 라이브러리를 사용하는 것이 쉽고 간편하며 코드 생산성도 높기 때문일 것이다.
이론상으로만 따지자면 물론 서버의 리소스를 최대한 활용할 수 있는 것은 후자인 웹플럭스이다.
서버의 리소스를 최대한 활용한다는 것은 물론 효율적인 프로그램이라는 말과 같다.
그러나 잘못 사용한다면, 웹플럭스는 스레드 블로킹을 일으켜 프로그램 전체가 먹통이 되는 결과를 가져오게 된다.
그러면 우리는 어떤 경우에 웹플럭스를 사용하고 어떤 경우에 MVC를 사용해야 할까?
동기/비동기 프로그래밍에 대해
흔히들 MVC는 동기/블로킹 방식의 스레드 구조를 가졌고 웹플럭스는 비동기/논블로킹 방식의 스레드 구조를 가졌다고 한다.
여기서 동기/비동기 방식의 코드에 대해 간단히 알아보자.
동기 방식은 말하자면 코드를 실행하는 즉시 그 효과가 나타나는 방식이다.
코드 실행 후 결과를 바로 이용하겠다는 것과 같다.
따라서 동기 방식의 코드는 예외 없이 순차적으로 실행된다.
요청을 주고 받을 때, 동기 방식은 무조건 앞의 응답을 받은 뒤에 요청을 준다.
반면 비동기 방식은 코드를 실행한 후 나중에 효과가 나타나게 된다.
코드 실행 후 결과를 기다리지 않는다.
결과를 기다릴 필요가 없으므로 이러한 방식의 코드는 순서에 상관 없이 실행될 수 있다.
비동기 방식은 앞 요청에 대한 응답을 받든 받지 않든 다음 요청을 실행한다.
응답이 오는 순서는 제각각이지만 세 번째 요청까지 걸리는 시간은 동기 방식보다는 빠르다.
이러한 동기/비동기는 기술적인 관점에서 보면 스레드와 관련이 없고 단지 실행 순서에만 관련이 있다.
반면 블로킹/논블로킹은 스레드와 밀접한 관련이 있다.
블로킹/논블로킹에 대해
그러면 이번에는 블로킹과 논블로킹에 대해 알아보자.
사실, 블로킹과 논블로킹의 경우는 관점에 따라 조금 헷갈릴 수 있는 부분이다.
용어 자체가 본래 커널과 어플리케이션의 상호 작용에서 온 것인데, 이 부분은 일반적인 프로그래밍 관점에서는 조금 생소할 수 있다.
여기서는 스레드 관점에서 블로킹과 논블로킹에 대해 설명할 것이다.
보통 블로킹 방식은 실제로 코드를 실행하는 스레드의 관점에서 스레드의 연산이 막혔다(block)라는 것을 말한다.
스레드가 멈췄기 때문에 해당 스레드는 다른 연산을 수행하지 못하고, 따라서 해당 스레드에 무언가 작업을 요청하려고 해도 반응하지 않는다.
말만 들으면 굉장히 안 좋은 것 같아 보이고 실제로도 좋지 않지만, 대부분의 경우 크게 문제가 되지는 않는다.
작업 스레드의 수를 늘리거나 애초에 블로킹 작업 시간을 길게 가져가지 않는 등, 대개의 경우 이에 대응하는 방법이 갖추어져 있기 때문이다.
하지만 특정 상황에서는 문제가 되곤 한다.
부하가 특히나 많이 몰리는 서버 같은 경우가 그렇다.
물론 일반적으로 프로그래머 입장에서 초당 수십 건 이상의 트랜잭션이 발생하는 환경을 접하기는 쉽지 않지만, 그래도 어딘가에는 이런 환경이 있는 법이다.
그래서 이러한 상황을 해결하기 위해 논블로킹에 대한 움직임이 생겨났다.
아무튼 위 동기 방식의 요청 방식에서 블로킹과 논블로킹 구간을 살피면 다음과 같다.
각 요청과 응답 사이의 구간에서 Client가 block된 것을 확인할 수 있다.
정확히는 해당 요청을 하는 코드가 실행된 뒤 return 값을 받기 전까지 해당 코드를 실행하는 스레드가 멈춰 있는 것이다.
반면, 논블로킹은 이와 반대의 개념이라고 보면 된다.
다시 말해 스레드가 막히지 않고 연산을 계속하는 상태이다.
논블로킹 스레드는 응답을 받은 이후에 다음 요청을 하기까지 대기할 필요 없이 별개의 연산을 처리할 수 있다.
그러면 이런 질문이 나올 수 있을 것이다.
위의 비동기 모델에서는 어디가 블로킹 구간이고 어디가 논블로킹 구간일까?
모든 비동기가 논블로킹인 것은 아니지만, 적어도 위의 그림에서는 비동기 모델에 블로킹 구간은 없다.
물론 실제로는 세 응답을 모두 취합하여 새로운 작업을 할 경우(서로 의존적일 경우) 마지막 응답이 올 때까지 기다리게 되지만, 응답 각각을 개별적으로 처리해도 상관이 없다면(서로 독립적일 경우) 굳이 어떤 스레드가 다른 스레드에서 연산이 처리되는 것을 기다려야 할 일은 없을 것이다.
실제 코드의 예
여기까지만 보면 동기 방식이 블로킹 방식이고 비동기 방식이 논블로킹 방식이 아닌가 하는 생각을 할 수 있다.
실제로 많은 경우 동기/블로킹 방식과 비동기/논블로킹 방식이 동의어로 사용되곤 한다.
하지만 엄밀히 따지자면 둘은 엄연히 다른 개념이다.
예를 들어 다음과 같은 코드를 실행한다고 해보자.
1
2
int i = 0;
i++;
i의 값을 1 증가시키는 간단한 코드이고, 이 코드는 또한 동기 방식의 코드이기도 하다.
해당 코드의 값은 즉시 얻어지며, 그 값이 구해지는 동안 스레드가 다른 연산을 하거나 하지는 않기 때문이다.
그런가 하면 해당 코드는 논블로킹 방식의 코드이기도 하다.
해당 연산을 할 때 스레드가 다른 곳에서 진행되는 연산을 기다릴 필요는 없기 때문이다.
극단적으로 다음과 같이 무한 루프를 돌린다면
1
2
3
4
int i = 0;
while (true) {
i++;
}
해당 프로그램의 CPU 점유율은 끝을 모르고 치솟을 것이다.
CPU 점유율이 오르는 것은 일반적으로는 좋지 않은 신호이지만, 그건 보통 처리해야 하는 양이 너무 많아져서 그런 것이고, 처리해야 하는 양이 동일하다고 하면 CPU 점유율이 낮은 것보다는 높은 것이 낫다.
동일한 작업을 더 빠르게 처리한다는 것과 동의어이기 때문이다.
CPU를 바쁘게 만들었다는 점에서 위의 코드는 훌륭하게 논블로킹 방식으로 작동했다고 보아야 할 것이다.
하지만 다음과 같은 코드는 동기 방식이자 블로킹 방식이다.
1
2
3
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<String> response = restTemplate.getForEntity("https://github.com/redberrypi", String.class);
System.out.println(response.getBody());
똑같아 보이는데 왜 이 코드는 논블로킹이 아니라 블로킹 방식이냐면, 해당 코드는 통신을 통해 외부 서버에서 연산 처리된 결과가 들어오기까지 대기를 하기 때문이다.
생각해보면 당연하다.
우리가 만든 프로그램이 외부의 프로그램과 통신을 할 경우 값을 받아오기 전까지 해당 스레드는 할 일이 없다.
위의 코드를 호출한 스레드는 응답이 ResponseEntity
그러면 비동기 방식의 논블로킹 코드는 어떻게 생겼을까?
보통은 다음과 같다.
1
2
3
4
5
6
7
WebClient webClient = WebClient.create(); // 1. webclient 생성
webClient.get()
.uri("https://github.com/redberrypi")
.retrieve()
.toEntity(String.class)
.subscribe(response -> System.out.println(response.getBody())); // 2. Http 응답 이후 실행
System.out.println("Http 요청중!"); // 3. 응답을 받든 받지 못했든 상관 없이 곧바로 실행
중요한 건 subscribe 부분이다.
webClient의 uri와 retrieve, toEntity의 체인을 거쳐 subscribe 함수가 실행되게 되면, webClient는 입력된 정보에 따라 Http 통신을 요청한다.
이후, 해당 요청은 subsribe 내부에 건네준 response -> System.out.println(response.getBody()) 라는 함수를 실행하게 되는데, 이 함수는 Http 통신의 응답이 도착한 이후에 실행된다.
그리고 3번의 sysout 코드는 응답과는 상관 없이 subscribe 이벤트가 발생한 직후 실행되게 된다.
그리하여 우리가 관심있는(보통은 응답 이후 이를 처리하는 부분에 관심이 있을 것이다.) 코드인 2번 코드는 트리거인 subsribe와는 별개로 나중에 실행되게 된다.(비동기)
또한 해당 코드를 수행하는 동안 3번 코드를 비롯한 그 밑의 코드는 연산이 중지되는 일 없이 연속적으로 수행된다.(논블록)
물론 그 대신 webClient를 선언하고 sysout 코드가 실행되는 해당 함수(subscribe를 호출한)는 응답 이후 호출되는 response -> System.out.println(response.getBody()) 코드에 접근할 수는 없다.
만약 어떻게든 ResponseEntity에 접근하고 싶다면 subscribe 대신 block 함수를 사용하면 되지만 이건 대놓고 스레드를 block하는 함수라는 것을 알 수 있다.
이처럼 논블로킹 방식으로 코드를 짤 경우 코드의 실행 순서가 직관적이지 않기 때문에 일반적으로는 많은 고생을 하게 된다.
그러면 비동기 방식의 블로킹 방식 코드는?
이론상으로는 존재하지만 스레드 관점에서 이를 구현하기는 쉽지 않다.
원래의 커널 관점에서는 어플리케이션이 커널에 제어권과 콜백을 넘겨주면, 커널의 작업이 끝나고 어플리케이션에 제어권을 건네준 뒤(비동기) 콜백을 호출하는 방식이라고 한다.
그러면 그 동안 어플리케이션은 제어권이 없기 때문에 콜백이 호출되는 것을 마냥 기다리므로(블로킹) 비동기 블로킹 방식이라고 한다.
이것을 일반적인 스레드 관점에서 구현하려면 우선 1, 2번 스레드가 존재하고 함수 A, B, C가 존재한다고 했을 때 1번 스레드가 A함수를 실행하고, A함수는 2번 스레드에게 B함수를 실행시키게 한 뒤 결과값을 받지 않고 종료(비동기), 이후 2번 스레드 작업이 끝나면 B함수에서 1번 스레드에게 C함수(콜백 함수)를 실행시키게 한 뒤 자기자신은 종료되게 하면 될 것이다.
그리고 그 동안 1번 스레드는 아무것도 하지 않은 채 대기하면 된다.(블로킹)
설명만 들어도 상당히 비효율적이라는 것을 알 수 있다.
사실, 이건 1번 스레드가 중간에 대기하지만 않아도 비동기 논블로킹 방식으로서 꽤 효율적인 방식이라고 할 수 있다.
그런데 굳이 멀쩡한 스레드를 놀려가면서까지 그렇게 하는 건 결코 칭찬 받을 짓은 아닐 것이다.
그래서 보통 이러한 패턴은 거의 볼 수도 없고, 좋은 패턴으로 여겨지지도 않는다.
보통은 몰라도 된다고 할 수 있다.
CPU 스레드 VS OS 스레드
계속 스레드라고 하는데, 여기서 말하는 스레드가 정확히 무엇인지 간단히 짚고 넘어가는 것이 좋겠다.
우리가 일반적으로 스레드라고 부르는 건 사실은 두 가지 종류가 있다.
하나는 우리가 CPU를 구매할 때 스펙으로 나오는 4코어 8스레드, 8코어 24스레드 등, CPU의 물리적 스레드이다.
다른 하나는 OS에서 내부적으로 사용하는 OS 스레드이다.
보통 아무리 많아도 100을 넘지 않는 CPU 스레드와는 달리, OS 스레드는 수만에서 수십만, 그 이상까지도 갈 수 있다.
우리가 만드는 프로그램의 스레드는 OS 스레드에 속하며, 시스템에 따라 다르지만 프로세스 하나당 수백~수만 정도의 스레드 수를 가질 수 있다고 한다.
그러면 이 많은 스레드를 상대적으로 적은 수의 CPU가 어떻게 처리하냐고 할 수 있을 것이다.
하지만 대부분의 OS 스레드는 보통 유휴 상태에 있기 때문에 CPU가 연산을 할 필요는 없다.
기본적으로는 연산이 필요한 OS 스레드가 있으면, CPU 스레드가 이를 잡아 채서 CPU 코어에 데려가는 식이다.
보통 이를 비유할 때 CPU 코어를 입, CPU 스레드는 손으로 비유한다.
입이 하나일 때 손이 하나면 음식을 집으러 갈 때마다 입이 비게 된다.
그래서 손이 두 개가 있으면 한 손이 음식을 집으러 갈 때 다른 손으로 음식을 먹고, 그 손이 음식을 집으러 갈 때 다른 손이 음식을 입에 물어다 주기 때문에 효율적이라고.
보통 1코어당 2스레드, 혹은 3스레드가 일반적인 이유이기도 하다.
그런데 이 때 말고도 CPU 스레드가 OS 스레드를 선택할 때에도 컨텍스트 스위칭이라는 것이 일어나게 된다.
컨텍스트 스위칭이 자주 일어나게 되면 성능이 떨어지게 된다.
그런데 이러한 컨텍스트 스위칭이 언제 일어나게 되냐면, 위에서 말한 스레드 블로킹 작업이 일어날 때 일어나게 된다.
위의 동기/블로킹 코드에서 getForEntity는 통신의 응답을 받을 때까지 기다리지만, 물리적 CPU 입장에서는 이를 끝까지 기다려야 할 이유가 없다.
안 그래도 할 일은 많고 자신을 기다리는 OS 스레드도 많겠다.
연산이 비어 있는 동안 컨텍스트 스위치가 일어나 CPU는 다른 밀려 있는 작업을 하게 되고, 이것이 자주 일어나게 되면 CPU의 성능을 온전히 뽑아내지 못하는 것이다.
(물론 이러한 작업 자체가 별로 없다면 그렇게까지 신경쓰지 않아도 될 정도의 손해라는 것을 다시 한 번 강조한다.)
반면에 논블로킹 방식은?
잘만 된다면 스레드가 멈출 일이 없고, 그래서 컨텍스트 스위치를 하지 않아도 되고, 그래서 성능이 더 좋게 나온다.
그리고 그렇기 때문에 논블로킹 방식의 OS 스레드는 딱 코어 수만큼만 만들어진다.
더 많은 스레드를 만드는 이유는 컨텍스트 스위칭을 통해 블로킹된 OS 스레드의 빈 자리를 파고들기 위함인데, 논블로킹 방식으로는 파고들 틈 자체가 없어야만 하기 때문이다.
그러면 논블로킹은 무적인가?
굳이 말하자면 논블로킹은 ‘이론상 무적’이라고 해야 옳을 것이다.
잘만 만든다면 무조건 블로킹보다 이득이지만, 이 잘이라는 부분이 쉽지가 않다.
코드 작성과 테스트, 디버깅 등이 모두 어렵다.
게다가 쉽지 않기 때문에 보통은 생산성이 떨어지게 된다.
보통 프로그래머 입장에서 생산성은 알게 뭐냐는 식이지만 실제로 사업을 하는 사업가 입장에서 생산성은 돈과 직결되기 때문에 결코 간단한 문제가 아니다.
더군다나 제일 문제가 되는 부분은 뭐냐면, Webflux의 경우 생성되는 OS 스레드의 수가 정해져 있기 때문에 모든 OS 스레드가 블록될 경우 까딱 잘못하면 프로그램 전체가 먹통이 될 수 있다라는 부분이다.
물론 이건 블로킹 방식인 MVC 역시 내포하고 있는 문제이지만 MVC의 경우는 해결이 쉽다.
일단 스레드 수를 늘리면 해결된다.
하지만 Webflux는 이 방법이 먹히지 않는다.
Webflux 자체가 내부적으로 모든 코드가 적절하게 배치된 것을 전제로 하기 때문에, 애초에 스레드 수를 인위적으로 늘릴 방법이 없다.
그래서 무조건 블로킹이 일어난 부분을 찾아야만 하는데, 위에서 적었듯 논블로킹은 디버깅이 쉽지가 않다.
보통은 StackTrace를 찾아 에러 난 부분을 콕 찝어 해당 코드로 넘어가면 되는데, Webflux에서 에러가 난 부분을 찾으려 StackTrace를 살펴보면 두 눈이 휘둥그레진다.
Mono…? Flux…?
분명 내가 만든 로직 어딘가에서 에러가 난 것은 맞는데 내가 쓴 코드 부분은 한 줄 없고 subcribe니 flatMap이니 생소한 함수들만 한가득이다.
물론 이러한 Webflux에도 나름의 디버깅 툴은 있다.
하지만 이것도 알아서 찾아서 미리 잘 세팅을 해 놓아야지 안 한 상태에서 에러가 난다면 고역일 것이다.
게다가 디버깅 툴을 설치했다고 해서 모든 게 잘 풀리는 것도 아니다.
거기에서 원인인 부분을 찾아내기 위해 flux chain을 한참 동안 뜯어보고 고뇌해야만 한다.
하다보면 현타가 온다.
이게 정녕 사람이 할 짓이 맞나?
라이브러리에 대해 잘 알아야 한다
본론으로 넘어가 어떨 때 MVC를 사용하고 어떨 때 Webflux를 사용하는 것이 좋은지를 살펴보자.
중요한 건 내부적으로 어떤 코드, 어떤 라이브러리를 사용할 것이냐는 것일 터다.
특히 해당 함수가 블로킹인지 논블로킹인지를 아는 것이 중요하다.
당연하다면 당연하지만 블로킹 함수가 하나라도 있다면 Webflux보다는 MVC를 사용해야만 한다.
하지만 라이브러리 하나를 도입하는데 내부적으로 코드 하나를 샅샅이 훑어보고 블로킹인지 논블로킹인지를 살펴볼 수는 없는 노릇이다.
그러나 사실, 잘 생각해보면 그렇게까지 어려운 것은 아니다.
예를 들어 내가 컴퓨터에게 연산거리를 준다고 생각해보자.
1 + 1은?
연산을 멈출 이유가 없을 것이다.
1 + 1 + 1 + 1 + … + 1은?
이 역시 컴퓨터 입장에서는 연산을 멈출 이유가 없다.
사람과는 달리 컴퓨터 입장에서 지친다는 개념은 존재하지 않기 때문이다.
하지만 다음과 같은 명령을 내린다고 해보자.
저기 미국에 있는 chatgpt한테 C++ 고수가 될 수 있는 방법 좀 알아와 봐봐.
컴퓨터는 인터넷에 연결된 회선을 통해 요청할 것이다.
그러면 chatgpt 서버가 이 요청을 받고 열심히 CPU를 굴릴 것이다.
아무리 chatgpt라 하더라도 C++ 고수가 되는 방법은 단기간에 도출되지는 않을 것이다.
1초. 2초. 5초가 흐르고 10초가 흘러도 답이 나오지 않는다.
그 동안에 내 컴퓨터는?
아무것도 하지 않는다.
그리고 그 동안 응답을 처리해야 하는 그 밑의 코드들도 스레드와 함께 붙잡혀 있어야만 할 것이다.
외부 통신을 하게 되면 블로킹이 일어난다.
일반적으로 외부 통신을 하게 될 경우 스레드의 블로킹이 일어나게 된다.
다만 주의해야 하는 것은, 통신이라고 해서 무조건 스레드의 블로킹이 일어나는 것은 아니라는 것이다.
만약 그렇다고 한다면 Webflux를 사용했을 때 DB 이용(딱 봐도 외부 통신이다.)도 HTTP 통신도(이 역시 명백한 외부 통신이다.) 불가능할 테니까 말이다.
그리고 이것이 불가능하다면, 우리가 이미 알듯이 Webflux가 이 정도로 유명하지는 않았을 것이다.
구체적으로 말하자면 블로킹이 일어나는 것은 통신이 일어나는 함수가 동기적 또는 명령적(restTemplate의 예와 같이)인 경우라고 할 수 있다.
만약에 함수가 비동기적으로, 선언적으로 콜백 함수를 통해 응답을 처리하게 되어 있다면(webClient의 예와 같이) 이는 논블로킹으로 설계되었을 확률이 크다.
이러한 논블로킹으로 설계된 라이브러리들은 보통 subscribe 함수를 통해 미리 설계된 동작을 실행하게 되는데, 이 경우 subscribe 함수를 call한 함수에서는 이러한 실행 결과에 직접적으로 접근하지 못하게 된다.
하지만 Webflux와 같이 논블로킹 시스템이라면 subscribe를 호출하는 것이 아니라, 이미 subscribe에 의해 호출된 상태로 Mono 혹은 Flux 체인에 해당 함수를 연결하여 응답 결과를 처리하게 되는 것이 일반적이다.
1
2
3
4
5
6
7
8
9
public Mono<ServerResponse> upload(ServerRequest request) {
Mono<String> then = request.multipartData().map(it -> it.get("files"))
.flatMapMany(Flux::fromIterable)
.cast(FilePart.class)
.flatMap(it -> it.transferTo(Paths.get("/tmp/" + it.filename())))
.then(Mono.just("OK"));
return ServerResponse.ok().body(then, String.class);
}
보이지는 않지만 위의 코드가 실행되는 시점에서 어딘가에서는 subscribe를 실행한 상태다.
원래라면 then이라는 Mono는 subscribe를 통해 발동시켜야 하겠지만, Webflux의 Controller에서는 그럴 필요가 없다.
이미 Publisher의 subsribe chain 안에 들어와 있는 상태이기 때문이다.
제가 쓰는 jdbc에는 subscribe 함수가 없는데요? - Webflux의 한계
예를 들어 일반적인 jdbc는 보통 동기 방식으로 호출하기 때문에 Webflux에서는 스레드가 block되는 문제가 있다.
하지만 찾아보면 DB를 비동기로 호출하기 위한 async jdbc 라이브러리들이 존재한다.
R2DBC가 그 대표적인 예일 것이다.
참고로 R2DBC는 단지 명세일뿐이고, 실제 구현체는 DB 벤더사에서 제공하는 것을 집어넣어야만 한다.
다만 모든 벤더사에서 R2DBC의 구현체를 제공하는 것은 아니라는 문제가 또 있다고 한다.
jdbc와는 다르게 말이다.
아무튼 Webflux에서는 이러한 async 라이브러리를 이용하여 블로킹이 일어나지 않도록 DB를 호출하고 그 값을 제대로 처리할 수 있다.
하지만 모든 라이브러리나 벤더사에서 논블록/비동기 함수를 제공하는 것은 아니다.
이러한 경우 어쩔 수 없이 블로킹 스레드로 작업을 해야만 한다는 한계가 있다. 다시 말해, 이와 같은 경우에는 Webflux를 사용할 수가 없다고 할 수 있다.
결론
이 글을 보는 여러분이 Webflux에 대해 얼마나 들어봤는지는 모르겠다.
다만 내가 알기로는 일반적으로 Webflux를 쓰지 않아도 되거나, 쓰지 않는 게 좋다고 아는 것이 일반적일 것이다.
그리고 그 이유는 위에서 기술한 바와 같다.
많은 개발자들은 콜백 함수보다는 return값을 받는 프로그래밍에 익숙하며, 벤더사들조차 항상 논블록/비동기 라이브러리를 제공하는 것은 아니다.
그럼에도 잘만 쓰면 분명 좋은 기술인 것은 맞지만, 이 세상에 잘만 쓰면 좋은 것이 얼마나 많은가?
아무리 좋은 기술이라도 사용자가 잘 파악하지 못한다면 내부적으로는 엉망진창이 될 수 있다.
정말로 좋은 개발자가 되기 위해서는 끊임없이 자신이 모르는 기술에 대해 공부하고 연구해야 할 것이다.
덧붙임 - sysout은 블로킹? 논블로킹?
System.out.println()이라는 함수는 모두가 알듯 콘솔에 문자를 출력하는 함수이다.
그런데 이런 의문이 떠오른다.
시스템이 외부 시스템과 통신을 할 때 스레드의 블록이 일어난다고 하였다.
그런데 콘솔 역시 프로그램 입장에서 보면 외부 프로그램으로 취급되어 블로킹이 일어나는 거 아닐까?
결론적으로 말하자면 그렇기도 하고 그렇지 않기도 하단다.
Windows의 경우는 속절없이 블로킹이지만 Linux에서는 buffer 처리가 되어 그렇게 느린 편이 아니라고.
그러나 블로킹이든 아니든 sysout이 동기 방식이라는 것은 때로는 신경 쓰이는 부분일 것이다.
만약 블로킹이든 논블로킹이든 로그의 시점이 크게 중요하지 않다면, log4j2의 경우 async 로거를 사용하여 로그를 비동기 처리할 수 있다.
로그를 바로 출력하는 것이 아니라 일단 queue에 로그를 쌓아 두었다가 짬짬이 로그를 출력하는 방식이라서 로그를 찍는 시간이 거의 없어진 것처럼 느껴진다고 한다.
물론 해야 하는 연산 자체를 줄여주는 것은 아니지만, 어쨌든 중요한 로직의 처리가 빨라지니 이득이라고 할 수 있을 것이다.
참고 사이트
https://docs.spring.io/spring-boot/docs/current/reference/html/web.html
https://www.quora.com/What-is-the-difference-between-CPU-thread-and-OS-thread
https://stackoverflow.com/questions/17304013/is-console-output-a-blocking-operation
Comments powered by Disqus.