Skip to Content

닫는 글 — 다시, 기다림 앞에서

이 책이 열여덟 장 내내 재온 건 스레드가 아니라 기다림이었습니다. 기다림은 사라지지 않습니다. 누가 그 값을 무느냐만 바뀔 뿐이에요.


새벽 두 시로 돌아가서

여는 글의 장면으로 돌아가봅시다. CPU 3%, 응답 시간 30초, 헬스체크 타임아웃.

이제 이 장면을 읽을 수 있습니다.

  • 스레드 200개가 자고 있었습니다. 커널의 대기 큐에 들어가 하류의 응답을 기다리면서요. read() 한 번이 어떻게 스레드를 재우는지는 5장에서 봤습니다.
  • 자는 스레드는 CPU를 안 씁니다. 실행 큐에서 아예 빠지니까요(5장). 그래서 자원 그래프가 조용했어요. 대신 태스크 구조체와 스택이라는 자리는 계속 붙들고 있습니다(1장).
  • 자리 수에는 천장이 있습니다. 1장에서 커널이 4,068개에 거부했고(상한은 kern.num_taskthreads 4,096), 8장에서 그 자리값이 왜 남에게까지 청구되는지 갈랐습니다.
  • 그래서 201번째로 도착한 요청은 앞사람이 깰 때까지 실행조차 못 됐습니다. 여는 글의 2,676ms가 그 값이에요.

진단명을 붙이면 이렇습니다. 자원이 부족한 게 아니라 자리가 부족했습니다.

이 책이 실제로 잰 것

관통 문장은 이거였죠 — 비동기는 더 빨리 하는 기술이 아니라, 기다리는 동안 스레드를 놓아주는 기술이다.

열여덟 장에서 나온 숫자를 한자리에 놓으면 이렇게 됩니다.

무엇을 물었나어디서
스레드는 몇 개까지 만들 수 있나4,068개에서 커널이 거부1장
자는 스레드가 남에게 값을 물리나4,000개가 무관한 인계를 20배대로 (1장 22.5배 / 8장 재측정 21.5배)1·8장
이벤트 루프는 OS에 있나없습니다. 유저스페이스의 while 문입니다7장
논블로킹은 무엇인가플래그 하나(O_NONBLOCK)와 EAGAIN6장
비동기가 지연을 줄이나아니요. 줄인 건 병렬화입니다 (순차 322ms → 병렬 108ms → CF 107ms)여는 글
비동기가 바꾸는 건묶인 스레드 4개 → 0개여는 글·10장
그 대가는스택 8프레임에 내 코드 0줄4장
가상 스레드는 얼마나 싼가1GB에 140만 개 이상, 개당 700~1,000바이트대17장
스레드 모델을 바꾸면 처리량이 느나병목이 스레드일 때만18장

마지막 줄이 이 책에서 제일 자주 되풀이된 이야기입니다. 병목을 먼저 묻지 않으면 나머지 답은 다 틀립니다.

사라지지 않는 것

4부에서 본 네 자리 중 셋 — 사라진 로그, 오염된 전역 풀, 막힌 루프 — 은 전제를 되사오면 같이 사라집니다. 스레드를 안 갈아타면 컨텍스트도 안 사라지고, 공유 풀에 블로킹을 던질 이유도, 막힐 루프도 없어지니까요.

하나는 남습니다. 18장에서 재봤듯이, 스레드가 아무리 싸져도 백프레셔는 그대로예요. 오히려 더 나빠지는 면이 있습니다 — 풀 크기를 적던 자리 자체가 사라졌으니까요(18장).

이게 우연이 아닌 게, 백프레셔만 성격이 다릅니다. 나머지 셋은 코드를 뒤집은 대가였어요 — 둘은 실행 흐름이 스택을 떠나서, 하나는 소수의 루프를 모두가 공유해서입니다(4장이 기억해두라고 한 그 두 뿌리요). 뒤집지 않기로 하면 셋 다 근거가 사라집니다.

그런데 백프레셔는 산술의 문제입니다. 들어오는 속도가 나가는 속도보다 빠르면 어딘가에 쌓이고, 그건 스레드가 싸든 비싸든 참이에요.

그래서 16장의 결론은 도구가 바뀌어도 안 바뀝니다. 그 문장을 그대로 옮깁니다 — “공짜인 선택지는 없습니다. 다만 아무것도 안 고르면 무한 큐가 기본값입니다.”

20년 뒤에 읽는 사람에게

이 책은 2026년의 JDK 25에서 쟀습니다. 몇 년 지나면 숫자들은 틀리겠죠. synchronized가 pin한다는 조언이 JDK 24에서 하루아침에 옛말이 된 것처럼요.

그래도 안 바뀔 것 세 가지를 골라 적어두겠습니다.

첫째, 기다림은 사라지지 않습니다. 네트워크가 아무리 빨라져도 원격 호출은 로컬 계산보다 자릿수가 다르게 느려요. 이 책 안에서만 봐도 다섯 자릿수가 벌어집니다 — 데이터가 있는 read() 873ns(5장), 잠들었다 깨어나는 값 31μs(5장), 외부 API 100ms(여는 글). 비동기든 가상 스레드든 그다음에 뭐가 오든, 하는 일은 늘 같습니다 — 기다림의 값을 누구에게 물릴지 고르는 것. 커널의 대기 큐에 물릴지, 콜백 객체에 물릴지, 힙 위의 스택 조각에 물릴지요.

둘째, 추상화는 실패할 때 새어 나옵니다. 가상 스레드는 “블로킹처럼 쓰되 값은 안 치른다”는 훌륭한 추상화입니다. 그런데 네이티브 프레임 하나가 그 추상화를 뚫고 나오죠(17장). 리액티브의 선언적 파이프라인도 그렇고요 — 블로킹 한 줄이면 무너집니다(15장). 잘 도는 동안은 아무 문제 없다가, 무너질 때 아래층 지식을 요구합니다. 그게 이 책이 커널까지 내려간 이유예요.

셋째, 측정하지 않은 것은 모르는 것입니다. 이 책을 쓰면서 제가 확신했다가 측정에 반박당한 게 여러 번입니다. “이벤트 루프는 과부하에서 효율이 좋아진다”고 썼다가 대조군 실험에 반박당했고, commonPool 문제를 “개발 환경에서는 재현 안 된다”고 썼다가 제 8코어 노트북에서 바로 재현했습니다. 둘 다 지우고 다시 썼습니다. 반대로 지우지 않고 그대로 남긴 것도 하나 있어요. 3장의 그 상자입니다 — “풀 8이 오히려 느리다”고 한 번씩만 재고 썼다가, 3회로 늘리니 노이즈였던 것요.

전부 그럴듯했습니다. 그럴듯한 게 제일 위험해요. 반박당한 주장들의 공통점은 하나였습니다 — 비교 대상도 반복도 없이, 한 번 재고 그럴듯한 이야기를 붙였다는 것.

다시, 기다림 앞에서

다음에 CPU 3%짜리 장애를 만나면 볼 곳이 생겼을 겁니다. 자원 그래프 말고, 자리를 세어보세요. 스레드 덤프를 뜨고, 몇 개가 어디서 자고 있는지 보고, 그 자리가 몇 개까지 있는지 확인하는 거죠.

그리고 답이 “자리가 없다”로 나오면 선택지는 셋뿐입니다.

  1. 자리를 늘린다 — 가상 스레드 (17장)
  2. 자리를 안 붙들게 한다 — 이벤트 루프·CompletableFuture (7·11·12장)
  3. 덜 받는다 — 입장 제한과 백프레셔 (16·18장)

셋 다 값이 있고, 이 책은 그 값을 하나씩 쟀습니다. 어느 걸 고를지는 여러분의 병목이 정합니다.

읽어주셔서 고맙습니다. 이제 여러분 서버에서 직접 재보세요. 숫자가 다르게 나오면 그게 맞는 겁니다 — 여러분 기계니까요.


Last updated on