가상 쓰레드를 인터랙티브 시각화 두 개로 가르쳐본 이야기 — 메이킹 & 사용 가이드
텍스트로는 안 잡히던 ForkJoinPool·Work-Stealing·Poller·커널 인터럽트 흐름을 인터랙티브 HTML 두 개로 풀어본 과정. 무엇을 어떻게 쪼개서 그렸는지, 어떻게 쓰면 가장 빨리 이해되는지.
가상 쓰레드를 인터랙티브 시각화 두 개로 가르쳐본 이야기
같은 디렉터리의 쓰레드 풀과 가상 쓰레드 완벽 정리 글의 자매편입니다. 본 글은 그 글에 박아둔 두 인터랙티브 시각화를 어떻게 만들었고 어떻게 쓰면 좋은지 풀어쓴 메이킹 기록입니다.
1. 왜 시각화를 만들었나
가상 쓰레드 글을 쓰다가 두 번 막혔습니다.
막힘 1. 8.12 "VT 라이프사이클" 절을 글로만 설명하니, mount → park → unmount → re-mount 라는 동사 네 개가 코드 위에서 어떤 시점에 일어나는지 그림이 안 잡힙니다. 텍스트로 "Continuation이 yield 되면 스택이 힙으로 복사된다"고 적어도, 독자는 "그래서 그 순간 캐리어는 뭘 하고 있는데?"가 안 풀립니다.
막힘 2. 8.16 "OS ↔ JVM 경계" Q&A에서 더 심각해졌어요. "아무도 폴링하지 않는다", "OS는 fd만 알고 VT는 모른다" 같은 핵심 메시지는 순서와 행위자를 동시에 보여줘야 와닿습니다. NIC → 인터럽트 → 커널 → epoll → wake_up → Poller → unpark → 캐리어 mount. 단계가 9개고 행위자가 3종(하드웨어/커널/User)인데, 글의 한 단락에 한 단계씩 풀면 14문단입니다. 읽다가 길 잃습니다.
그래서 단계 진행 + 색깔로 행위자 구분 + 상태 변화 애니메이션이 가능한 인터랙티브 HTML 두 개를 만들었습니다. React나 D3 같은 거 안 썼고, 그냥 vanilla JS + CSS 한 파일짜리입니다. 단일 HTML이라 npm 빌드 없이 그대로 public/visualizations/에 떨궈 정적 자산으로 서빙합니다.
- Scheduler 시각화 — ForkJoinPool, Work-Stealing, Poller가 시간에 따라 어떻게 협력하는지 (sandbox-style, 직접 조작)
- Kernel-Flow 시각화 — 패킷 한 번이 어떻게 user mode VT까지 도달하는지 (story-style, 14단계 순차 진행)
두 도구는 의도적으로 다른 모드입니다. 하나는 sandbox(독자가 시나리오를 만들어봄), 하나는 story(정해진 한 길을 따라감). 같은 양식이면 한쪽이 잉여가 됩니다.
2. 시각화 1 — Scheduler: "직접 만져보는 sandbox"
2-1. 무엇을 보여주고 싶었나
가상 쓰레드 동작 원리에서 가장 직관에 안 맞는 게 work-stealing입니다. "각 캐리어가 자기 deque를 가진다", "한가하면 다른 deque 끝에서 훔친다" — 글로 읽으면 그럴듯한데, 실제로 한 캐리어에 일이 몰렸을 때 다른 캐리어가 어느 타이밍에 어느 deque에서 어느 방향으로 훔치는지는 손으로 그려봐야 잡힙니다.
목표는 한 줄이었어요. "VT 10개를 한 캐리어에 쏟아부었을 때, 나머지 3개 캐리어가 알아서 부하 분산하는 장면을 1분 안에 보여줘야 한다."
2-2. 단계별 설계 과정
Step 1. 데이터 모델 먼저 그리기
UI 그리기 전에 상태 모델을 종이에 적었습니다.
carriers: [
{ id: 1, running: VT?, deque: VT[] },
{ id: 2, ... },
...
]
parkedVTs: VT[] // I/O 대기 중
pendingIO: { vtId: number }[] // Poller가 처리할 큐
VT의 라이프사이클은 ready → running → parked → ready 4상태로 단순화. 본문(threadpool.mdx 8.12) 상태 전이도와 일치시켜서, 글과 시각화 사이 용어 차이 없게.
Step 2. tick 함수에 모든 규칙 압축
setTimeout 루프 하나가 1틱마다 3가지 일을 합니다.
function tick() {
// ① Poller가 pendingIO 하나 꺼내 → 가장 한가한 캐리어 deque로 enqueue
// ② 각 캐리어의 running VT는 workRemaining-- 하다가
// 0이 되면: I/O면 → parked, CPU면 → 종료
// ③ idle 캐리어는 자기 deque top에서 pop(LIFO)
// 자기 deque 비었으면 → 가장 일 많은 캐리어 deque base에서 shift(FIFO)
// (= work-stealing)
}핵심 제약 두 가지:
- 주인은 top(
pop)에서, 도둑은 base(shift)에서. Chase-Lev deque의 본질을 그대로 코드로. - mount는 자기 deque 비어있을 때만 steal. 다른 deque에 일 있어도 자기 게 우선.
Step 3. "한 번에 한 가지"만 보이도록 UI 분리
세 영역으로 잘랐습니다.
[ Poller Thread + I/O 이벤트 큐 ] ← 위쪽
[ ForkJoinPool: Carrier × 4 + 각자 deque ] ← 중앙
[ Parked VTs ] ← 아래
각 카드가 한 가지 책임만 갖습니다. 본문에서 "Poller가 unpark하면 deque로 enqueue된다"라고 적은 화살표가, 시각화에선 위 카드 → 중앙 카드로의 시각적 이동으로 보입니다.
Step 4. 색·애니메이션으로 행위자 표시
- VT는 노랑 (정보 단위)
- Carrier는 파랑 (실행 단위, OS 쓰레드)
- Work-stealing은 빨강 강조 + 로그에 🦹 이모지
- I/O 이벤트 도착 시 Poller 카드가 빨강으로 깜빡
색을 의미와 1:1 매핑해두면 글의 강조 톤(파랑 = Java/user, 빨강 = 커널/이벤트)과도 일관됩니다.
2-3. 사용 가이드 — 4가지 시나리오
이 시각화는 시나리오를 만들어보는 게 핵심입니다. 그냥 ▶ 재생 누르고 보면 너무 빨라요. 다음 순서로 직접 조작해보세요.
시나리오 A. 평화로운 상태 관찰 (30초)
▶ 재생누르고 1.5초/틱 정도로 늦춰서 봅니다.- 캐리어 4개가 자기 deque의 top에서 VT를 차곡차곡 꺼내가는 패턴 확인.
- 통계 박스의 "활성 캐리어" 숫자가 4 근처에서 머무는지 확인.
시나리오 B. 일부러 한쪽에 부하 몰기 (work-stealing 발동)
- 한 번
↺ 리셋. + VT 10개 폭주클릭 → 한 캐리어의 deque에 VT 10개가 한꺼번에 쌓입니다.⏯ 1스텝을 천천히 눌러가며 다른 캐리어들이 그 deque의 base(아래쪽) 에서 VT를 훔쳐가는 순간을 포착. 로그에 🦹가 뜹니다.- 포인트: 주인은 top(맨 위)에서, 도둑은 base(맨 아래)에서. 위치가 다른 게 핵심.
시나리오 C. Park → Unpark 사이클
▶ 재생을 1초 정도 돌리면 자연스럽게 parked VT가 생깁니다 (I/O 작업 VT가 끝까지 가면 park).⚡ I/O 완료 트리거클릭 — Poller가 깨어나 parked VT 하나를 deque로 다시 enqueue.- 포인트: parked VT가 어느 카드로 갔다가 어디로 돌아오는지 시선으로 따라가기. 글 8.16의 "OS는 fd만 알고 VT는 Java가 안다"는 그 다리가 이 한 클릭에 압축돼 있습니다.
시나리오 D. 압박 테스트
+ VT 10개 폭주두세 번.▶ 재생을 10초/틱으로 느리게.- 통계의 "총 훔치기" 카운터가 점점 올라가는 걸 보면서 work-stealing이 실제로 부하 분산 역할을 하는지 확인.
3. 시각화 2 — Kernel-Flow: "정해진 길을 따라가는 story"
3-1. 무엇을 보여주고 싶었나
이쪽은 sandbox가 아니라 한 사건의 14단계 분해입니다. "패킷 한 번이 도착해서 VT가 깨어나기까지" 라는 단일 시나리오를 끝까지 따라가는 형식.
핵심 메시지는 본문(8.16)의 마지막 문단입니다.
하드웨어(NIC, CPU) — 신호 발사, 명령어 실행. 판단 없음. 커널 코드 — CPU 위에서 돌며 큐를 조작. wait↔ready 이동의 주체. Java 코드 — Poller도 VT도 캐리어도 다 user mode. fd→VT 변환과 ForkJoinPool 운영.
이 세 행위자가 어느 단계에서 등판하는지가 그림으로 안 들어오면 다음 단락이 안 읽힙니다.
3-2. 단계별 설계 과정
Step 1. 14단계를 상수 배열로 박제
자유도가 낮은 만큼 데이터 모델이 훨씬 단순합니다. STEPS 배열 하나.
const STEPS = [
{ id: 'idle', label: '대기', desc: '모두 잠.' },
{ id: 'packet', label: '패킷 전송', desc: '클라이언트가 8080으로 송신.' },
{ id: 'nic', label: 'NIC 수신', desc: 'DMA로 RAM에 적재.' },
{ id: 'interrupt', label: '인터럽트 발사', desc: 'NIC → CPU. CPU 강제 Ring 0.' },
{ id: 'handler1', label: '핸들러: 드라이버', ... },
... // 14개
{ id: 'done', label: '완료', desc: 'VT가 socket.read() 다음 줄부터.' },
];이 배열 하나가 글의 "9단계 흐름" 도식과 1:1 대응됩니다. 글에서 단계 번호를 바꾸면 시각화 배열의 인덱스도 바꿔야 한다는 강제성이 생겨서, 도리어 글과 시각화가 어긋날 수 없게 됐어요.
Step 2. applyStep(i) — 단계 = 화면 상태
각 step의 id마다 switch 분기로 화면 변경을 적용. "이전 step의 변경을 누적"하지 않고, 매 단계마다 깨끗하게 활성 상태 클래스를 비우고(clearActive()) 그 단계만 활성화합니다.
function applyStep(i) {
clearActive(); // 이전 단계의 강조 다 끄기
const s = STEPS[i];
switch (s.id) {
case 'interrupt':
el('arrowIrq').classList.add('active');
el('cpu').classList.add('active');
el('cpuSub').innerHTML = '<strong>Ring 0 진입!</strong>';
log('[HW] NIC → CPU 인터럽트 발사', 'hw');
break;
case 'handler1':
el('handler').classList.add('active');
el('hs1').classList.add('active'); // ① NIC 드라이버: 패킷 읽기
log('[KERNEL] 인터럽트 핸들러 시작', 'kernel');
break;
// ...
}
}이 패턴 덕분에 "처음으로" 버튼이 그냥 currentStep = 0; applyStep(0) 두 줄이면 됩니다. 어떤 단계에서 어떻게 와도 멱등하게 reset.
Step 3. 레이어별 색 + 로그 prefix
화면을 세 줄로 잘랐습니다.
[ HARDWARE 레이어 ] 왼쪽 테두리 검정, 배경 그라데이션
[ KERNEL 레이어 ] 왼쪽 테두리 빨강, 배경 옅은 빨강
[ USER 레이어 ] 왼쪽 테두리 파랑, 배경 옅은 파랑
로그도 prefix를 색으로:
[HW]— 검정[KERNEL]— 빨강[USER]— 파랑
같은 단어("핸들러", "스케줄러")가 어느 레이어에서 일어나는지 한눈에 들어옵니다. 글의 "행위자 세 종류만 기억" 결론과 같은 색 체계를 쓰니, 시각화와 글이 색을 통해 묶입니다.
Step 4. step-tracker 핀
상단에 14개의 동그란 pill로 진행 표시. 완료된 단계는 초록, 현재 단계는 빨강. "지금 14개 중 몇 번째인지"가 항상 보이게.
3-3. 사용 가이드 — 3가지 보는 법
방법 A. 처음 한 번은 자동 재생
▶ 자동 재생누르고 1.8초/스텝 그대로.- 끝까지 한 번 흘려보내면서 전체 윤곽 잡기.
- 14단계 트래커가 다 초록으로 채워지면 한 사이클 완료.
방법 B. 두 번째는 한 단계씩 (책 읽듯이)
↺ 처음으로→⏯ 1스텝을 천천히 누름.- 단계마다 멈춰서:
- 어느 레이어(HW/Kernel/User)가 활성인지
- 어느 큐(epoll ready list / wait queue / scheduler ready queue)가 변했는지
- 로그가 어떤 색으로 찍혔는지
- 포인트 단계: 4(
인터럽트 발사), 8(wake_up), 10(Poller 깨어남), 12(unpark). 이 네 단계가 의미 도약 지점입니다.
방법 C. 본문과 교차 비교
- threadpool 글의 8.16 "패킷 한 번의 일생" 절을 옆에 띄우고
- 시각화에서
⏯ 1스텝을 누를 때마다 글에서 같은 번호 단락을 읽음. - 시각화 단계 4 = 글 단계 [3] = "CPU 강제 Ring 0 진입" — 식의 매칭.
세 매체(시각화 / 글 / 로그)가 같은 사건을 다른 각도로 보여주는 효과.
4. 두 시각화를 모두 만들면서 정한 디자인 원칙
1. 의존성 0
React/Vue/D3 다 안 씁니다. 단일 HTML 파일, vanilla JS, vanilla CSS. 빌드 파이프라인 없음. 이유:
- 학습 자료는 링크 공유 시 즉시 동작해야 함. npm install 같은 단계가 끼면 안 쓰임.
- 5년 뒤에도 그대로 열려야 함. 프레임워크 버전 호환성 신경 안 쓰고 싶음.
- 본문 글 안에서
<a href="/visualizations/...">한 줄로 끼워넣을 수 있음.
2. 텍스트 1차, 시각화는 보조
시각화 없이 글만 봐도 의미가 닫혀야 합니다. 시각화는 "막힐 때 새 탭으로 여는 보조 도구". 글에 시각화를 임베드하지 않은 이유도 같습니다 — 글의 흐름이 시각화에 종속되면 안 됩니다.
3. 같은 색 = 같은 의미
빨강은 두 시각화 모두에서 커널/이벤트/도둑(긴급/주의). 파랑은 Java/Carrier/user mode(평상시). 노랑은 VT/데이터. 색을 두 시각화 사이에서 일치시켜서, 한 시각화에서 익힌 색 코드가 다른 시각화에서 그대로 통하게.
4. 로그는 "이게 왜 일어났는지" 한 줄로
상태 변화만 보여주지 말고 항상 한 줄 설명. 🦹 Carrier-2: Carrier-1 deque base에서 VT-7 훔침! (work-stealing, FIFO) 처럼 동작 + 이유를 같이. 학습자는 화면 변화보다 텍스트를 더 신뢰하는 경우가 많습니다.
5. 단계 자유도 ↔ 시각 자유도 트레이드오프
Sandbox(자유)는 데이터 모델을 충실히 — 상태 머신·tick 함수·자동 균형. Story(제한)는 14단계 배열 + switch — 자유도 낮춘 대신 글과 1:1 대응.
같은 주제를 두 모드로 따로 만든 이유가 이겁니다. 자유는 직관을 키우고, 제한은 정확성을 키운다. 둘 중 하나만 있으면 한쪽이 부족해집니다.
5. 만들면서 배운 점
1. "정답이 정해진" 학습자료는 sandbox로 만들면 안 된다.
처음엔 한 시각화 안에 14단계 모드 + sandbox 모드를 다 넣으려고 했어요. 결과는 끔찍했습니다. UI가 양쪽 다 만족시키려고 하면 어느 쪽도 못 만족시킵니다. 분리하는 게 정답이었습니다.
2. 색을 의미에 묶으면 글까지 같이 묶인다.
본문 글의 강조 색(빨강/파랑)을 시각화에도 그대로 가져갔더니, 글 → 시각화 → 글 사이 인지 비용이 거의 0이 됐습니다. 색은 무의식 다리.
3. "행위자"를 명사로 박제하라.
NIC, CPU, 커널 코드, Poller, Carrier, VT, fd, epoll #5, 인터럽트 핸들러, ready queue, wait queue — 11개의 명사가 시각화 카드 하나씩에 매핑됩니다. 글에서 이 명사들을 흩뿌리면 흐려지지만, 시각화에선 한 명사 = 한 카드 = 한 색 = 한 자리. 명사를 박제하면 동사 변화(active, idle, parked, running)가 의미를 가집니다.
4. 단계 데이터를 글과 동기화하기.
Kernel-flow의 STEPS 배열이 글의 단계 번호와 1:1이라, 글이나 시각화 한쪽을 고치면 다른 쪽도 같이 고쳐야 한다는 자연스러운 제약이 생깁니다. 이게 두 자료가 어긋나지 않는 가장 단순한 방법.
5. 의존성 없는 한 파일 HTML의 위력.
배포가 한 줄. 깃 히스토리에 그대로 박힘. 친구한테 보낼 때도 한 파일. 학습자료는 이 형태가 거의 최적이라는 결론. 복잡한 시각화 라이브러리를 학습용에 끌어들이는 건 종종 과잉.
6. 사용하실 때 / 만드실 때
- 본 글을 처음 보신 분: Scheduler 시각화부터 1분만 만져보고 본문(쓰레드 풀과 가상 쓰레드 완벽 정리)으로 가시면 8.12 ~ 8.16절이 훨씬 빨리 잡힙니다.
- 본문을 읽다 막히신 분: 글 안에 박힌 링크 박스에서 그 시점에 맞는 시각화로 점프하시면 됩니다. 자유롭게 왔다 갔다.
- 본인 분야에서 비슷한 시각화 만들고 싶으신 분: 두 HTML 파일은 둘 다 단일 파일이라 그대로 가져다 데이터 모델만 갈아끼우셔도 됩니다. (
carriers/STEPS배열만 본인 도메인으로 교체).
마치며
원래는 글 한 편이면 되리라 생각했는데, 결국 글 한 편 + 시각화 두 개 + 메이킹 글 한 편 합쳐 네 개의 자료가 됐습니다. 한 가지 개념을 가르치는 데 같은 정보를 다른 매체로 세 번 보여주는 것 — 글(정확성), 인터랙티브(직관), 사용 가이드(맥락) — 가 결과적으로 가장 효율적인 학습 패키지였습니다.
다음에 또 어려운 개념을 정리하게 되면, 글 본문보다 데이터 모델을 먼저 그리고 그걸로 시각화부터 만드는 순서로 가보고 싶습니다. 시각화가 강제하는 명사 단순화가 글의 톤도 정리해주거든요.
링크 모음