Thread 에 대해 이해하는 것은 굉장히 중요합니다. 서비스의 성능을 좌지우지하는 요소 중에 하나이기 때문인데요. '아 그런가보다' 하고 넘어갈 수도 있지만, 계속해서 생각할 수록 어려운 내용입니다. Thread에서 파생되는 주제가 [ Multi-Threading, Concurrency, Parallelism, Blocking, Non-blocking .. ] 와 같이 많기 때문이죠.
그 중에서 오늘은 멀티 쓰레딩에 대해서 조금만 고민해보려고 합니다.
Multi Threading
다수 사용자로부터 요청이 오면 빠르게 처리를 해야하기 때문에 여러 개의 쓰레드를 두는 것이 필요해졌습니다. 여러 개의 요청을 순차적으로 처리하는 것이 아니라 동시에 처리하면서 전반적인 시스템의 효율이 증가합니다. 멀티 쓰레딩의 장점을 들어보면 다음과 같습니다.
1. 높은 처리량을 통한 반응성 증가.
2. 리소스를 활용.
하지만 멀티 쓰레딩으로 위와 같은 장점을 얻는 것에는 한계가 존재합니다. 쓰레드의 개수를 늘려가다 보면, 어느 순간 처리량 증가폭이 적어지게 됩니다. 아래와 같이 로그 함수의 그래프를 보입니다.
처음에 쓰레드를 증가시켰을 때는 가시적인 처리량의 증가를 보이지만, 계속 해서 늘려가면 어느 순간 늘려도 효과가 없어 보이는 것이죠. 이를 암달의 법칙이라고도 부릅니다.
멀티 쓰레딩 환경에서 각 쓰레드는 'Context Switching' 이라는 동작을 통해서 여러개의 작업을 동시에 처리하게 됩니다. 컨텍스트 스위칭 비용은 쓰레드의 개수에 비례합니다. 이런 비용들이 위와 같은 결과를 만들어내는 것입니다.
한 줄로 정리해보면, Multi-Threading 에는 한계가 존재한다는 것이죠.
Multi Threading Language
그렇다면 멀티 쓰레딩 언어들은 이런 한계점을 가지고 있는데 어떻게 하고 있을까요? Java를 예시로 생각해봅시다.
Java 에서 아래와 같이 두 개의 서비스에 요청을 보내고, 결과가 오면 처리를 해야하는 로직이 있다면 어떻게 수행할까요?
user 정보를 얻고, account 정보를 얻고, Finance 객체를 만들어서 반환을 해야합니다. 로직 상으로는 문제가 없지만, 외부 서비스에 요청을 보내 정보를 얻는 과정에서 대기 시간이 존재하죠.
쓰레드들은 요청을 받으면 응답값을 내려주기 위해서 작업을 계속 잡고 있습니다. 따라서 작업이 끝날 때까지는 그 쓰레드는 다른 일들을 처리 못합니다. 위에서 말한 것처럼 쓰레드의 개수는 한정적인데 대기가 길어지면 서비스의 성능에 굉장히 큰 위협이 됩니다.
Finance 객체를 만들어달라는 하나의 요청을 처리하기 위해서, 위와 같이 n+m 초가 소모됩니다. 외부 서비스로 보내는 요청을 비동기로 처리하게 되면 좀 더 빠르게 수행이 가능합니다.
외부 서비스로 응답이 오는 것을 기다려야하기는 하지만 두 개의 요청을 동시에 처리해 n+m 초가 걸리는 것을 max(n,m) 초로 개선할 수 있습니다.
이렇게 한정된 자원을 최대한 활용하기 위해서, 각 언어들은 다양한 방법으로 최적화를 진행합니다. 당연히 이렇게 하더라도 더 많은 요청이 들어오면 쓰레드 모델은 계속해서 느려질 수 밖에 없습니다. 하지만 상대적으로 적은 수의 쓰레드로도 더 많은 요청을 처리하기 위해서 다양한 노력하는 것이죠.
그리고 최근에 이러한 것들을 기존의 문제점들을 개선한 모델인 Virtual Thread가 Java 21에 포함되었습니다.
결국 자원을 최대한 활용하기 위한 것
Multi Threading 은 한계가 존재합니다. 그래서 쓰레드의 개수는 정해져 있으니 우리가 가진 자원 아래에서 최대한의 처리량을 내는 방법에 대해서 고민을 하게 된 것이죠.
Virtual Thread, Coroutine, Reactive Programming 과 같은 것들 모두 가진 자원을 최대한 활용하기 위해서 나온 결과물입니다. 여기에 적힌 것들을 무조건 좋다는 당연히 아닙니다.
제가 말하고 싶은 것은 각자의 서비스에서 필요한 기능과 성능이 있기 때문에, 어떠한 방법으로 성능을 높일지 고민해서 선택하는 것이 필요할 것 같습니다.
예를 들어, 이런 경우가 있을 수 있을 것 같네요.
의견을 남겨주세요