serversim
학습시뮬레이터템플릿가격
…
…
메뉴 열기
학습↗시뮬레이터↗템플릿↗가격↗
내 Documents문서
목차/15단원15 / 16

어떻게 여기까지 왔나 — 60년의 발전사

지금 쓰는 방법들은 왜 하필 이 순서로 태어났을까요? 새 방법은 좋은 아이디어에서 나온 게 아니라, 앞 세대가 받아 든 청구서에서 나왔습니다.

지금 우리가 당연하게 쓰는 스레드풀·이벤트루프·코루틴은 누군가 어느 날 좋은 아이디어를 떠올려서 생긴 것이 아닙니다. 전부 그 앞 세대가 지불한 대가 때문에 나왔습니다. 프로세스가 무거워서 스레드가 나왔고, 스레드가 메모리를 못 버텨서 이벤트루프가 나왔고, 이벤트루프의 코드가 읽히지 않아서 코루틴이 나왔습니다. 아래 연표를 한 시대씩 넘기면서, 각 시대가 무엇을 얻고 무엇을 지불했는지를 따라가 보세요.

① 발명은 진보가 아니라 거래였다

역사를 "점점 좋아진 이야기"로 읽으면 아무것도 배우지 못합니다. 모든 단계는 무언가를 얻는 대신 무언가를 지불했습니다. 시분할은 CPU 활용률을 얻고 컨텍스트 스위치 비용을 냈습니다. 스레드는 가벼움을 얻고 경쟁 상태와 락을 냈습니다. 이벤트루프는 메모리를 얻고 콜백 지옥과 "블로킹 금지"라는 족쇄를 냈습니다. 그래서 새 모델이 나왔다고 옛 모델이 틀린 것이 되지 않습니다 — 지불할 대가가 달라졌을 뿐입니다.

② 큰 도약은 세 번뿐 — 격리, 공유, 등록

60년을 세 개의 결정으로 압축할 수 있습니다. (1) 격리 — 실행 중인 프로그램을 저장·복원 가능한 단위로 자르고 서로의 메모리를 못 보게 막았습니다(프로세스). (2) 공유 — 격리를 벗기고 주소공간 하나 안에서 흐름만 여러 개 두었습니다(스레드). (3) 등록 — 기다림에 아예 실행 흐름을 붙이지 않고, 커널의 통지 목록에 등록만 해두었습니다(이벤트루프). 이후의 모든 것(코루틴, 고루틴, 가상 스레드)은 세 번째 결정을 어떻게 포장하느냐의 문제입니다.

③ 코루틴은 새 발명이 아니었다

흥미로운 사실이 하나 있습니다. 코루틴이라는 이름은 1963년에 이미 붙었고, Simula 67에는 언어 기능으로 들어가 있었습니다. 그런데 40년 넘게 주변부에 머물렀습니다. 왜냐하면 그때는 그것이 필요할 만큼 동시 연결이 많지 않았기 때문입니다. C10K가 이벤트루프를 강요했고, 이벤트루프가 콜백 지옥을 만들었을 때 비로소 이 옛 아이디어가 정답이 되었습니다. 기술은 좋아서 채택되는 것이 아니라, 문제가 그것을 요구할 때 채택됩니다.

④ 그리고 코드는 원점으로 돌아왔다

2023년의 가상 스레드로 짜는 코드는 1969년 Unix 시절의 코드와 모양이 같습니다. 그냥 위에서 아래로 읽히고, 블로킹 호출을 그대로 씁니다. 바뀐 것은 코드가 아니라 그 아래입니다 — 기다림 하나의 값이 수 MB에서 수 KB로 내려갔고, 중단점을 잡아주는 주체가 커널 → 라이브러리 → 컴파일러 → 런타임으로 옮겨왔습니다. 역사는 원을 그렸지만 같은 자리는 아닙니다.

⑤ 연표에서 확인할 것

연표를 한 시대씩 넘기면서 그림을 보세요. 상자는 주소공간(프로세스), 초록 막대는 커널이 스케줄링하는 스레드, 파란 점은 그 위에 얹힌 사용자 수준 작업입니다. 1950년대에는 상자 하나에 막대 하나뿐이고, 1969년에는 상자가 늘어나고, 1985년에는 상자가 하나로 합쳐지면서 막대가 늘어납니다. 1999년에 막대가 더는 늘어나지 못하고, 2004년부터는 막대 하나 위에 파란 점이 수만 개 얹힙니다. 그리고 매 시대의 "대신 지불한 것"을 함께 읽으세요 — 그것이 다음 시대를 부른 이유입니다.

단계 1/8속도
1950년대

배치 처리 — 한 번에 프로그램 하나

동시 단위의 모양

프로그램

▌실행 흐름

갈아타기를 정하는 주체
사람 (운영자가 덱을 바꿔 끼움)
단위 하나의 비용· 관행값
메모리 전체 — 프로그램 하나가 기계를 통째로 점유
한 대가 감당한 동시 개수· 관행값
1

실제로 있었던 일

  • 1954IBM 704 — 카드 덱을 제출하고 결과를 기다리는 방식이 표준
  • 1956GM-NAA I/O — 초기 배치 모니터. 다음 작업을 자동으로 이어 실행
  • 1959채널/DMA 확산 — 장치가 CPU 없이 스스로 데이터를 옮기기 시작

앞 시대가 남긴 문제

출발점입니다. 컴퓨터 한 대 값이 집 몇 채였고, 시간당 사용료를 받았습니다. 그런데 그 비싼 CPU가 실제로 계산하는 시간은 얼마 되지 않았습니다.

그래서 나온 것

프로그램을 천공카드 덱으로 제출하면 운영자가 한 번에 하나씩 기계에 걸었습니다. 카드를 읽고, 계산하고, 결과를 인쇄하고, 다음 덱으로 넘어갑니다. 메모리에 올라온 프로그램은 언제나 하나뿐이었습니다.

대신 지불한 것

카드리더와 테이프가 도는 동안 CPU는 정말로 아무 일도 하지 않았습니다. 대신 올릴 프로그램이 메모리에 없었으니까요. 활용률이 한 자릿수 퍼센트인 것이 정상이었습니다.

이 시대가 남긴 문제 →

"기다리는 동안 그 비싼 CPU를 다른 프로그램에 쓸 수는 없나?" — 이 한 문장이 이후 60년의 모든 발명을 낳습니다.

관련 단원

7단원

시뮬레이터에서 해보기

  1. 1.'Evolution Step 1 — MVP' 프리셋을 엽니다. LB → Spring 3종(각 2대) → 단일 MySQL. burst 부하(평균 7,000 RPS, 순간 21,000 RPS)에서 KPI 패널 Success 65.6%(드롭 29,120건 전부 queue-full).
  2. 2.라이브러리에서 'Evolution Step 2'로 바꿔보세요 — 같은 부하에 Redis 캐시 + Read Replica 2대만 추가됐는데 Success가 65.6% → 100%로 뜁니다(다만 이벤트가 아직 동기라 primary MySQL 카드의 CPU가 burst 꼭대기에 90%까지 튐).
  3. 3.'Evolution Step 3'까지 바꿔보면 이벤트만 Kafka로 비동기 분리해서 Success 100%를 유지하면서 primary MySQL 카드의 CPU 피크가 90%→61%로, p99 피크가 62.9ms→59.9ms로 내려갑니다 — 이 단원에서 본 '한 걸음씩 새 방법이 등장한 이유'가 아키텍처 레벨에서도 똑같이 반복됩니다.
시뮬레이터에서 실험해보기 →

이 단원의 용어

배치 처리
프로그램을 한 번에 하나씩 순서대로 돌리는 방식. 메모리에는 언제나 하나만 올라간다.
시분할(time-sharing)
여러 프로그램을 메모리에 올려두고 CPU 시간을 잘게 나눠 주는 방식. 프로세스의 출발점.
C10K
1999년 제기된, 서버 한 대가 동시 연결 1만 개를 감당하는 문제. 스레드 모델의 한계선.
M:N 스케줄링
사용자 수준 작업 M개를 커널 스레드 N개 위에 얹어 런타임이 배치하는 방식. 고루틴·가상 스레드가 이 방식.
캐리어 스레드
가상 스레드를 실제로 실행해 주는 커널 스레드. 블로킹을 만나면 위에 얹힌 것만 바꿔 끼운다.
언마운트(unmount)
런타임이 캐리어 스레드에서 가상 스레드를 걷어내는 것. 대기 중인 것은 스택째 힙에 보관된다.

왜 이렇게 발전했나

순서를 정한 것은 취향이 아니라 그 시대에 가장 비쌌던 자원입니다. 1950~60년대에는 CPU가 압도적으로 비쌌기 때문에 CPU를 놀리지 않는 방향(시분할)으로 갔습니다. 1990년대에는 CPU는 싸졌지만 메모리와 스레드 스택이 벽이 되었기 때문에 연결당 비용을 줄이는 방향(이벤트루프)으로 갔습니다. 2010년대에는 하드웨어보다 개발자의 시간과 실수가 더 비싸졌기 때문에, 성능은 유지하면서 코드를 다시 읽히게 만드는 방향(async/await, 가상 스레드)으로 갔습니다. 다음 모델이 궁금하다면 지금 무엇이 가장 비싼지를 보면 됩니다.

모든 실행 모델은 "기다림의 값을 누가 치를 것인가"에 대한 그 시대의 답이었다.

남은 문제

마지막으로, 이 방법들을 같은 조건에서 실제로 돌린 숫자를 한 표에서 봅시다 →

← 이전 단원

14단원. 함정 ② — 루프 위의 블로킹 호출

다음 단원 →

16단원. 종합 — 같은 일, 네 가지 방법