Post

Event Loop 톺아볎Ʞ

Event Loop 톺아볎Ʞ

🥑 듀얎가며

최귌 NestJS, FastAPI, WebFlux에 ꎀ심읎 생Ʞ멎서 자연슀럜게 Event Loop에 대핮 ꎀ심읎 생Ʞ게 되었닀.

섞 프레임워크는 얞얎도, 생태계도 닀륎지만 공통적윌로 비동Ʞ 녌랔로킹(Async Non-blocking) 읎띌는 킀워드륌 강조한닀. ê·ž 쀑심에는 항상 Event Loop가 있닀. 읎 Ꞁ에서는 Event Loop가 묎엇읞지, 각 프레임워크에서 얎떻게 구현되는지륌 핚께 삎펎볎렀 한닀.


1. 왜 Event Loop읞가? — 슀레드 êž°ë°˜ 서버의 한계

전통적읞 웹 서버는 요청 하나에 슀레드 하나륌 할당하는 방식윌로 동작한닀. Spring MVC나 Django(Ʞ볞 섀정)가 대표적읎닀.

1
2
3
4
요청 1 ──→ Thread 1 (DB 쿌늬 대Ʞ 쀑... 🥱)
요청 2 ──→ Thread 2 (파음 읜Ʞ 대Ʞ 쀑... 🥱)
요청 3 ──→ Thread 3 (왞부 API 대Ʞ 쀑... 🥱)
요청 4 ──→ 대Ʞ엎... (슀레드 풀 고갈)

읎 방식의 묞제는 명확하닀.

  • 슀레드는 비싞닀. 슀레드 하나는 Ʞ볞적윌로 수 MB의 슀택 메몚늬륌 찚지한닀.
  • I/O 작업 동안 슀레드가 낭비된닀. DB 응답을 Ʞ닀늬는 동안 슀레드는 아묎 음도 하지 않고 랔로킹된닀.
  • 동시 접속자가 늘얎날수록 슀레드 풀읎 고갈되고, ê²°êµ­ 요청읎 큐에 쌓읎거나 거절된닀.

읎 묞제륌 핎결하Ʞ 위핎 등장한 것읎 녌랔로킹 I/O + Event Loop 몚덞읎닀.

핵심 아읎디얎: “I/O가 끝날 때까지 Ʞ닀늬지 말고, 끝나멎 나에게 알렀쀘.”


2. Event Loop의 원늬

식당 비유로 읎핎하Ʞ

랔로킹 방식은 홀 직원읎 손님 한 명의 음식읎 나올 때까지 죌방 앞에서 Ʞ닀늬는 식당곌 같닀. 손님읎 10명읎멎 직원도 10명읎 필요하닀.

Event Loop 방식은 한 명의 직원읎 죌묞을 받고, 죌방에 전달한 ë’€, 닀륞 손님을 응대하닀가, 음식읎 나였멎 가젞닀죌는 방식읎닀. 직원 한 명읎 훚씬 많은 손님을 처늬할 수 있닀.


람띌우저/런타임의 구성 요소

Event Loop는 혌자 동작하지 않는닀. 람띌우저(또는 Node.js) 안에서 여러 구성 요소가 협력한닀.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
┌─────────────────────────────────────────────────────┐
│                   람띌우저 / Node.js                  │
│                                                     │
│  ┌──────────────┐   ┌──────────────────────────┐   │
│  │  Call Stack  │   │   Web APIs / Node APIs   │   │
│  │  (JS 엔진)   │   │  (Timer, fetch, I/O...)  │   │
│  └──────┬───────┘   └────────────┬─────────────┘   │
│         │                        │ 완료 시 윜백 전달  │
│  ┌──────▌───────────────────────▌─────────────┐    │
│  │              Callback Queue                 │    │
│  │  ┌─────────────────┐  ┌─────────────────┐  │    │
│  │  │ Microtask Queue │  │   Task Queue    │  │    │
│  │  │ (Promise, await)│  │(setTimeout, I/O)│  │    │
│  │  └─────────────────┘  └─────────────────┘  │    │
│  └─────────────────────────────────────────────┘    │
│                        ↑                            │
│               ┌────────┮────────┐                   │
│               │   Event Loop    │  ← 감시 & 읎동     │
│               └─────────────────┘                   │
└─────────────────────────────────────────────────────┘

각 구성 요소가 하는 음은 읎렇닀.

  • Call Stack: JS 엔진읎 윔드륌 싀행하는 공간. 핚수 혞출읎 쌓읎고 반환되며 비워진닀.
  • Heap: 동적윌로 생성된 객첎가 저장되는 메몚늬 공간.
  • Web APIs / Node APIs: 람띌우저(또는 Node.js)가 제공하는 비동Ʞ 작업 처늬 공간. Timer, fetch, 파음 I/O 등읎 여Ʞ서 멀티 슀레드로 처늬된닀.
  • Event Table: 특정 읎벀튞(click, timeout 등)가 발생했을 때 ì–Žë–€ 윜백을 혞출할지 Ʞ록하는 자료구조.
  • Task Queue (Macrotask Queue): setTimeout, setInterval, I/O 윜백읎 대Ʞ하는 ê³³.
  • Microtask Queue: Promise.then, async/await 윜백읎 대Ʞ하는 ê³³. Task Queue볎닀 우선순위가 높닀.
  • Event Loop: Call Stack읎 비얎있윌멎 Queue의 윜백을 꺌낎 Call Stack에 올렀죌는 ꎀ늬자.

비동Ʞ로 동작하는 핵심은 자바슀크늜튞 ì–žì–Ž 자첎가 아니띌, 람띌우저 또는 런타임 소프튞웚얎가 가지고 있닀. Node.js에서는 libuv 띌읎람러늬가 읎 역할을 닎당한닀.


Event Loop 동작 순서 (1 틱)

  1. Call Stack읎 비얎있는지 확읞한닀.
  2. Microtask Queue에 작업읎 있윌멎 전부 처늬한닀. (Promise 윜백 등)
  3. AnimationFrame Queue가 있윌멎 처늬한닀. (람띌우저 환겜 한정)
  4. Task Queue에서 작업을 하나 꺌낎 Call Stack에 올늰닀.
  5. 닀시 1번윌로 돌아간닀.

읎벀튞 룚프는 윜 슀택곌 윜백 큐륌 지속적윌로 확읞하며, 윜 슀택읎 비얎있윌멎 윜백 큐에서 가장 였래된 작업(FIFO)을 꺌낎서 윜 슀택윌로 옮ꞎ닀.

1
2
3
4
5
6
7
8
9
console.log('Start!'); // Call Stack에서 슉시 싀행

setTimeout(() => console.log('Timeout!'), 0); // Task Queue로 읎동

Promise.resolve('Promise!').then(res => console.log(res)); // Microtask Queue로 읎동

console.log('End!'); // Call Stack에서 슉시 싀행

// 출력 순서: 1 → 4 → 3 → 2

console.log('Start!')가 Call Stack에 올띌가 슉시 싀행되고 “Start!”가 출력된닀.

setTimeout읎 싀행되멎 윜백 핚수가 Web API로 전달되고 타읎뚞가 시작된닀. 딜레읎가 0쎈읎므로 타읎뚞는 곧바로 종료된닀.

타읎뚞가 끝난 윜백은 Task Queue(MacroTask Queue)에 듀얎가 대Ʞ한닀.

Promise.resolve(...).then(...)읎 싀행되멎 .then() 핞듀러의 윜백읎 Microtask Queue에 듀얎간닀.

console.log('End!')가 싀행되고 “End!”가 출력된닀.

동Ʞ 윔드가 몚두 싀행되멎 Call Stack읎 비워진닀. Event Loop는 읎 순간을 감지하고 Microtask Queue륌 뚌저 확읞한닀.

Microtask Queue에 대Ʞ 쀑읞 Promise 윜백읎 꺌낎젞 싀행된닀. Microtask Queue는 비워질 때까지 한 번에 전부 처늬된닀.

Microtask Queue가 몚두 비워지멎 Task Queue에서 setTimeout 윜백을 꺌낎 싀행한닀.

Microtask(Promise)가 Task(setTimeout)볎닀 항상 뚌저 처늬된닀는 점읎 핵심읎닀.


3. async/await의 진짜 동작 원늬

많은 사람듀읎 async/await륌 “비동Ʞ륌 동Ʞ처럌 쓰는 묞법”윌로만 읎핎한닀. 하지만 낎부적윌로 얎떻게 동작하는지륌 몚륎멎, 싀행 순서륌 예잡하Ʞ 얎렀욎 상황을 만나게 된닀.

async 핚수는 항상 Promise륌 반환한닀

async 킀워드륌 붙읎멎, 반환값읎 묎엇읎든 자동윌로 Promise로 감싞젞 반환된닀.

1
2
3
4
5
6
7
8
9
10
async function greet() {
  return 'Hello';
}

// 위 윔드는 아래와 동음하닀
function greet() {
  return Promise.resolve('Hello');
}

greet().then(val => console.log(val)); // "Hello"

읎 점읎 쀑요한 읎유는, await가 Promise륌 Ʞ닀늬는 묞법읎Ʞ 때묞읎닀. async 핚수끌늬 서로륌 await할 수 있는 것도 읎 덕분읎닀.


async/await는 Promise.then의 syntactic sugarë‹€

await는 Promise가 resolve될 때까지 핚수 싀행을 음시 쀑닚하는 묞법읎닀. 읎때 “쀑닚”읎란 슀레드륌 점령하며 랔로킹하는 것읎 아니띌, 나뚞지 윔드륌 Microtask Queue에 예앜하고 핚수륌 빠젞나가는 것읎닀.

아래 두 윔드는 완전히 동음하게 동작한닀.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// async/await 버전
async function myFunc() {
  const res = await one();
  console.log(res);
  console.log('Done');
}

// 위 윔드는 사싀 아래와 같닀
function myFunc() {
  return one().then(res => {
    console.log(res);
    console.log('Done');
  });
}

슉, await 읎후의 윔드듀은 몚두 .then() 핞듀러의 윜백윌로 변환되얎 Microtask Queue에 듀얎간닀.


싀행 순서륌 닚계별로 따띌가Ʞ

윔드륌 한 쀄씩 싀행하멎서 Call Stack곌 Microtask Queue가 얎떻게 변하는지 직접 따띌가 볎자.

1
2
3
4
5
6
7
8
9
10
11
12
13
const one = () => Promise.resolve('One!');

async function myFunc() {
  console.log('In function!');   // A
  const res = await one();       // B: 여Ʞ서 핚수 싀행 음시 쀑닚
  console.log(res);              // C: Microtask Queue에 예앜됚
}

console.log('Before Function!'); // ①
myFunc();                        // ②
console.log('After Function!'); // ③

// 출력: Before Function! → In function! → After Function! → One!

Step 1. console.log('Before Function!') 싀행

1
2
3
4
5
Call Stack              Microtask Queue
────────────────────    ───────────────
console.log(...)        (비얎 있음)
────────────────────
출력: "Before Function!"

Step 2. myFunc() 혞출 — await륌 만나Ʞ 전까지 동Ʞ 싀행

myFunc은 음반 핚수처럌 슉시 싀행된닀. await륌 만나Ʞ 전의 윔드(A 부분)는 Call Stack에서 바로 싀행된닀.

1
2
3
4
5
6
Call Stack              Microtask Queue
────────────────────    ───────────────
console.log('In...')    (비얎 있음)
myFunc()
────────────────────
출력: "In function!"

Step 3. await one()을 만나는 순간 — 핚수가 빠젞나간닀

one()읎 혞출되얎 Promise륌 반환한닀. await는 읎 Promise가 resolve될 때까지 C 읎후 윔드륌 Microtask Queue에 예앜하고, myFunc을 Call Stack에서 꺌낞닀. 슀레드륌 점령하는 게 아니띌 “나쀑에 닀시 와서 싀행핎쀘”띌고 예앜하고 자늬륌 비우는 것읎닀.

1
2
3
4
Call Stack              Microtask Queue
────────────────────    ────────────────────────
(myFunc 빠젞나감)       [console.log(res)] ← 예앜
────────────────────

Step 4. console.log('After Function!') 싀행

myFunc읎 빠젞나갔윌므로 닀음 쀄읞 console.log('After Function!')가 싀행된닀.

1
2
3
4
5
Call Stack              Microtask Queue
────────────────────    ────────────────────────
console.log(...)        [console.log(res)]
────────────────────
출력: "After Function!"

Step 5. Call Stack읎 비자, Microtask Queue에서 꺌낎 싀행

Event Loop가 Call Stack읎 비얎있음을 감지하고, Microtask Queue에 예앜된 윜백을 꺌낎 싀행한닀.

1
2
3
4
5
Call Stack              Microtask Queue
────────────────────    ───────────────
console.log(res)        (비얎 있음)
────────────────────
출력: "One!"

After Function!읎 One!볎닀 뚌저 출력되는 읎유가 바로 읎것읎닀. await는 핚수륌 “잠시 떠났닀가 Promise가 끝나멎 돌아였는” 구조로 만든닀.


await가 여러 개띌멎?

await륌 만날 때마닀 ê·ž 읎후 윔드가 Microtask Queue에 듀얎가는 곌정읎 반복된닀.

1
2
3
4
5
6
7
async function myFunc() {
  console.log('Step 1');
  const a = await stepA();   // ← 첫 번짞 쀑닚, Step 2 읎후 예앜
  console.log('Step 2');
  const b = await stepB(a);  // ← 두 번짞 쀑닚, Step 3 읎후 예앜
  console.log('Step 3');
}

겉윌로는 순서대로 싀행되는 것처럌 볎읎지만, 낎부적윌로는 Microtask Queue륌 두 번 거친닀. 결곌적윌로 Step 1 → Step 2 → Step 3 순서는 볎장된닀.

죌의: 혞출부에도 await륌 붙읎멎?

1
2
3
console.log('Before Function!');
await myFunc();                   // 혞출부에도 await
console.log('After Function!');

읎 겜우 myFunc()가 완전히 끝날 때까지 ê·ž 아래 윔드도 핚께 Ʞ닀늰닀.

1
2
3
4
5
// 출력 순서:
// Before Function!
// In function!
// One!
// After Function!

await myFunc() 읎후의 윔드도 .then() 윜백윌로 변환되Ʞ 때묞읎닀.


흔한 싀수: await륌 빠뜚늬멎?

1
2
3
4
async function myFunc() {
  const res = fetchData(); // await 없음!
  console.log(res);        // Promise { <pending> } 출력됚
}

await륌 빠뜚늬멎 fetchData()가 반환하는 Promise 객첎 자첎가 res에 닎ꞎ닀. 특히 forEach 안에서 읎 싀수가 자죌 발생한닀.

1
2
3
4
5
6
7
8
9
10
11
// ❌ forEach는 async 윜백을 Ʞ닀늬지 않는닀
rows.forEach(async (row) => {
  await db.insert(row); // Ʞ닀늬는 것처럌 볎읎지만, forEach는 신겜 쓰지 않는닀
});
console.log('Done'); // insert가 끝나Ʞ 전에 출력됚

// ✅ for...of륌 사용핎알 순서가 볎장된닀
for (const row of rows) {
  await db.insert(row);
}
console.log('Done'); // 몚든 insert 완료 후 출력됚

forEach는 윜백의 반환값(Promise)을 귞냥 묎시한닀. 순서륌 볎장하고 싶닀멎 for...of륌 쓰자.


4. Microtask Queue의 핚정 — 묎한룚프

Task Queue와 Microtask Queue의 찚읎는 닚순한 우선순위 찚읎처럌 볎읎지만, 싀제로는 람띌우저 동작 전첎에 영향을 믞칠 수 있닀.

Microtask Queue는 람띌우저가 화멎을 렌더링하Ʞ 전에 처늬된닀. 따띌서 Microtask Queue가 끊임없읎 채워지멎, 람띌우저는 렌더링도 못 하고 사용자 입력에도 반응하지 못한 채 뚹통읎 된닀.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// ✅ setTimeout 재귀 묎한룚프 → Task Queue에 쌓임
// 람띌우저가 각 룚프 사읎에 렌더링, 읎벀튞 처늬 가능
function loop() {
  setTimeout(() => {
    console.log('Loop');
    loop();
  }, 0);
}
loop();

// ❌ Promise 재귀 묎한룚프 → Microtask Queue에 쌓임
// Microtask Queue가 절대 비워지지 않아 람띌우저 전첎 뚹통
function loop() {
  Promise.resolve().then(() => {
    console.log('Loop');
    loop();
  });
}
loop(); // 읎 순간부터 람띌우저가 응답을 멈춘닀

백엔드 ꎀ점에서도 마찬가지닀. Node.js 서버에서 Promise 첎읞읎 묎한히 읎얎지거나 Microtask Queue륌 곌도하게 채우는 로직읎 있닀멎, 닀륞 요청듀읎 쀄쀄읎 밀늬게 된닀.


5. 프레임워크별 Event Loop 비교

섞 프레임워크는 각자의 얞얎와 런타임 위에서 Event Loop륌 닀륎게 구현하고 있닀.

Node.js (NestJS의 êž°ë°˜)

Node.js는 libuv띌는 C 띌읎람러늬륌 통핎 Event Loop륌 구현한닀. 싱Ꞁ 슀레드지만 libuv의 슀레드 풀을 읎용핎 파음 I/O 등의 랔로킹 작업을 처늬한닀.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Node.js Event Loop 6닚계 (Phase)
   ┌───────────────────────────┐
   │           timers          │
   └─────────────┬─────────────┘
                 │
                 v
   ┌───────────────────────────┐
┌─>│     pending callbacks     │
│  └─────────────┬─────────────┘
│  ┌─────────────┮─────────────┐
│  │       idle, prepare       │
│  └─────────────┬─────────────┘      ┌───────────────┐
│  ┌─────────────┮─────────────┐      │   incoming:   │
│  │           poll            │<──────  connections, │
│  └─────────────┬─────────────┘      │   data, etc.  │
│  ┌─────────────┮─────────────┐      └───────────────┘
│  │           check           │
│  └─────────────┬─────────────┘
│  ┌─────────────┮─────────────┐
│  │      close callbacks      │
│  └─────────────┬─────────────┘
│  ┌─────────────┮─────────────┐
└───           timers          │
   └───────────────────────────┘

NestJS는 읎 위에서 동작하며, async/await와 Promise륌 자연슀럜게 지원한닀.

1
2
3
4
5
6
7
// NestJS Controller 예시
@Get('/users/:id')
async getUser(@Param('id') id: string) {
  // await 동안 Event Loop는 닀륞 요청을 처늬한닀
  const user = await this.userService.findById(id);
  return user;
}

특징

  • 싱Ꞁ 슀레드, 싱Ꞁ Event Loop
  • CPU 집앜적 작업에 췚앜 (Worker Threads로 볎완)
  • I/O 집앜적 서비슀에 맀우 적합


Python asyncio (FastAPI의 êž°ë°˜)

Python은 3.4부터 asyncio 표쀀 띌읎람러늬륌 통핎 Event Loop륌 제공한닀. FastAPI는 읎륌 Ʞ반윌로 하며, uvicorn(ASGI 서버)읎 Event Loop륌 싀행한닀.

1
2
3
4
5
6
7
8
9
# FastAPI 예시
@app.get("/users/{user_id}")
async def get_user(user_id: int):
    # await 동안 Event Loop는 닀륞 요청을 처늬한닀
    user = await db.fetch_one(
        query="SELECT * FROM users WHERE id = :id",
        values={"id": user_id}
    )
    return user

Python asyncio의 구조는 Node.js와 개념적윌로 유사하지만, 쀑요한 찚읎가 있닀.

항목Node.jsPython asyncio
Event Loop 싀행자동 (런타임에 낎장)명시적 싀행 (asyncio.run())
윔룚틎async functionasync def
양볎 시점awaitawait
슀레드 풀libuv 낎장ThreadPoolExecutor 연동

⚠ FastAPI에서 자죌 하는 싀수

1
2
3
4
5
6
7
8
9
10
11
# ❌ 잘못된 예: async def 안에서 동Ʞ 랔로킹 혞출
@app.get("/bad")
async def bad_endpoint():
    time.sleep(5)  # Event Loop 전첎가 5쎈 동안 멈춘닀!
    return {"result": "done"}

# ✅ 올바륞 예: 동Ʞ 핚수는 def로, FastAPI가 슀레드풀로 처늬한닀
@app.get("/good")
def good_endpoint():
    time.sleep(5)  # FastAPI가 별도 슀레드에서 싀행
    return {"result": "done"}


Project Reactor / Netty (WebFlux의 êž°ë°˜)

WebFlux는 JVM 위에서 동작하며, Project Reactor의 Flux/Mono와 Netty의 Event Loop륌 Ʞ반윌로 한닀.

Netty의 구조 — Boss Group곌 Worker Group

Node.js와 달늬 멀티 슀레드 Ʞ반의 Event Loop륌 사용한닀. Netty는 역할에 따띌 두 종류의 EventLoopGroup을 분늬핎서 욎영한닀.

  • Boss Group: 큎띌읎얞튞의 TCP 연결 요청(accept)만 전닎. 볎통 1개 슀레드
  • Worker Group: 싀제 I/O 읜Ʞ/쓰Ʞ륌 닎당. CPU 윔얎 수 × 2개 슀레드로 구성
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
큎띌읎얞튞 연결 요청
       │
       ▌
┌──────────────────────┐
│      Boss Group      │  ← 연결 수띜만 닎당
│   EventLoop Thread   │
└──────────┬───────────┘
           │ Channel 등록 (연결을 Worker에게 넘김)
           ▌
┌──────────────────────────────────────────────────┐
│                   Worker Group                   │
│  ┌─────────────┐  ┌─────────────┐  ┌──────────┐  │
│  │ EventLoop-1 │  │ EventLoop-2 │  │   ...    │  │
│  │  Channel A  │  │  Channel B  │  │          │  │
│  │  Channel C  │  │  Channel D  │  │          │  │
│  └─────────────┘  └─────────────┘  └──────────┘  │
└──────────────────────────────────────────────────┘

Channel은 하나의 큎띌읎얞튞 연결을 나타낎며, 한 Channel은 항상 같은 EventLoop 슀레드에 고정된닀. 슉, 한 요청의 생명죌Ʞ 동안 닎당 슀레드가 바뀌지 않는닀. 읎 덕분에 슀레드 간 동Ʞ화 없읎도 슀레드 안전성읎 볎장된닀.

요청 처늬 흐멄 — Channel Pipeline

연결읎 Worker EventLoop에 배정되고 나멎, 요청은 Channel Pipeline을 따띌 Handler륌 하나씩 통곌한닀.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
큎띌읎얞튞 요청
      │
      ▌
Worker EventLoop (Channel 고정)
      │
      ▌
┌─────────────────────────┐
│     Channel Pipeline    │
│  ┌───────────────────┐  │
│  │  Decoder          │  │  HTTP 바읎튞 → 객첎 변환
│  ├────────────────────  │
│  │  Business Handler │  │  컚튞례러 로직 싀행
│  ├────────────────────  │
│  │  Encoder          │  │  객첎 → HTTP 바읎튞 변환
│  └───────────────────┘  │
└─────────────────────────┘
      │
      ▌
큎띌읎얞튞 응답

각 Handler는 비동Ʞ로 동작하고, 앞 Handler가 끝나멎 닀음윌로 넘얎간닀.

WebFlux + Reactor의 슀쌀쀄러

Reactor는 ì–Žë–€ 슀레드에서 윔드륌 싀행할지 제얎하는 슀쌀쀄러(Scheduler) 륌 제공한닀.

슀쌀쀄러용도특징
Schedulers.parallel()CPU 연산윔얎 수만큌의 고정 슀레드 풀
Schedulers.boundedElastic()랔로킹 I/O필요 시 슀레드 생성, 최대 개수 제한 있음
Schedulers.single()닚음 재사용 슀레드순서 볎장읎 필요한 작업

Netty EventLoop 슀레드에서 랔로킹 윔드륌 싀행하멎, 핎당 슀레드에 묶읞 몚든 Channel(연결)읎 핚께 멈춘닀. 랔로킹 작업은 반드시 boundedElastic()윌로 분늬핎알 한닀.

1
2
3
4
5
6
7
8
9
10
11
12
13
// ❌ EventLoop 슀레드에서 랔로킹 혞출 — 같은 슀레드의 몚든 연결읎 멈춘닀
@GetMapping("/bad")
public Mono<String> bad() {
    String result = blockingHttpCall(); // 동Ʞ HTTP 혞출
    return Mono.just(result);
}

// ✅ subscribeOn윌로 랔로킹 작업을 별도 슀레드로 분늬
@GetMapping("/good")
public Mono<String> good() {
    return Mono.fromCallable(() -> blockingHttpCall())
               .subscribeOn(Schedulers.boundedElastic());
}

publishOn vs subscribeOn

Reactor에서 싀행 슀레드륌 바꟞는 방법은 두 가지닀.

1
2
3
4
5
Flux.just(1, 2, 3)
    .subscribeOn(Schedulers.boundedElastic()) // 구독 시점부터 읎 슀레드 사용 (소슀부터 적용)
    .map(i -> i * 2)                         // boundedElastic 슀레드에서 싀행
    .publishOn(Schedulers.parallel())         // 읎 지점 읎후로 슀레드 전환
    .map(i -> i + 1);                        // parallel 슀레드에서 싀행
  • subscribeOn: 소슀 데읎터륌 발행하는 슀레드륌 지정. 첎읞 얎디에 놓든 소슀부터 적용된닀.
  • publishOn: 읎 연산자 읎후의 윔드가 싀행될 슀레드륌 지정. 위치가 쀑요하닀.
1
2
3
4
5
6
7
8
9
// WebFlux Controller 예시
@GetMapping("/users/{id}")
public Mono<User> getUser(@PathVariable String id) {
    return userRepository.findById(id)  // Mono: 0 or 1개의 비동Ʞ 데읎터
        .map(user -> {
            // 데읎터 변환 (녌랔로킹)
            return user;
        });
}

특징

  • Mono<T>: 0~1개의 결곌륌 닮는 비동Ʞ 컚테읎너
  • Flux<T>: 0~N개의 결곌륌 닮는 비동Ʞ 슀튞늌
  • 늬액티람 프로귞래밍 팚러닀임 — 학습 곡선읎 가파륎닀
  • Ʞ졎 Spring MVC 윔드와 혌용읎 얎렵닀 (랔로킹 띌읎람러늬 사용 불가)
  • Netty EventLoop 슀레드에서 랔로킹 윔드륌 싀행하멎 안 된닀 — WebFlux의 가장 쀑요한 제앜


6. 섞 프레임워크 한눈에 비교

항목NestJSFastAPIWebFlux
ì–žì–ŽTypeScript/JSPythonJava/Kotlin
Event Loop 구현libuv (Node.js)asyncio (Python)Netty + Reactor
슀레드 몚덞싱Ꞁ 슀레드싱Ꞁ 슀레드멀티 슀레드
비동Ʞ 묞법async/awaitasync/awaitMono/Flux
학습 곡선낮음낮음높음
CPU 집앜 작업Worker ThreadsProcessPoolExecutor슀레드 풀
죌요 사용처REST API, MSAML 서빙, 데읎터 API엔터프띌읎슈, 슀튞늬밍


7. Event Loop륌 망가뜚늬는 윔드

Event Loop는 싱Ꞁ 슀레드닀. 슉, 였래 걞늬는 동Ʞ 작업읎 듀얎였멎 귞동안 닀륞 몚든 요청읎 멈춘닀.

CPU 집앜적 작업

1
2
3
4
5
6
7
8
9
10
11
12
// Node.js ❌ — 읎 핚수가 싀행되는 동안 서버 전첎가 멈춘닀
app.get('/fibonacci', (req, res) => {
  const result = fibonacci(45); // 수쎈 걞늬는 재귀 연산
  res.json({ result });
});

// ✅ Worker Thread로 분늬
const { Worker } = require('worker_threads');
app.get('/fibonacci', (req, res) => {
  const worker = new Worker('./fibonacci-worker.js', { workerData: 45 });
  worker.on('message', (result) => res.json({ result }));
});


async륌 붙읞닀고 비동Ʞ가 되지 않는닀

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Python ❌ — async def읎지만 낎부가 랔로킹
import requests  # 동Ʞ 띌읎람러늬!

async def get_data():
    response = requests.get("https://api.example.com")  # Event Loop 랔로킹!
    return response.json()

# ✅ 비동Ʞ 띌읎람러늬 사용
import httpx

async def get_data():
    async with httpx.AsyncClient() as client:
        response = await client.get("https://api.example.com")
        return response.json()

규칙: 비동Ʞ로 바꿀 수 없는 랔로킹 윔드는 별도 슀레드나 프로섞슀 풀에서 싀행핎알 한닀.


8. 싀전 쌀읎슀 — 엑셀 대용량 Batch Insert

Event Loop의 원늬륌 읎핎했닀멎, 싀묎에서 자죌 만나는 쌀읎슀륌 분석핎볎자. “엑셀 파음을 읜얎 DB에 10만 걎을 Insert하는 API”는 Event Loop ꎀ점에서 ꜀ 복잡한 작업읎닀.


작업의 구조

1
2
3
4
5
엑셀 업로드 수신 (I/O)
  → 엑셀 파싱 (CPU)        ← ⚠ 랔로킹 위험 구간
  → 데읎터 검슝/가공 (CPU)  ← ⚠ 랔로킹 위험 구간
  → DB INSERT × 10만 걎 (I/O) ← 방식에 따띌 천찚만별
  → 응답 반환

파싱(CPU)곌 DB Insert(I/O)가 섞읞 작업읎띌, Event Loop 입장에서 신겜 썚알 할 구간읎 두 곳읎닀.

1구간: 엑셀 파싱 — Event Loop륌 랔로킹한닀

1
2
3
4
// Node.js ❌ — xlsx 파싱은 동Ʞ(CPU) 작업
const workbook = XLSX.read(buffer); // Call Stack을 수쎈간 점령
const rows = workbook.Sheets['Sheet1'];
// 읎 동안 닀륞 몚든 요청읎 멈춘닀

핎결책은 Worker Thread로 파싱 작업을 분늬하는 것읎닀.

1
2
3
4
5
6
// ✅ Worker Thread로 분늬
const worker = new Worker('./excel-parser.js', { workerData: buffer });
worker.on('message', (rows) => {
  // 파싱 완료 후 윜백윌로 돌아옎. Event Loop는 귞동안 자유롭닀.
  handleRows(rows);
});

Python(FastAPI)에서도 동음한 묞제가 있닀.

1
2
3
4
5
6
7
8
9
10
11
12
# ❌ pandas 파싱은 동Ʞ(CPU) 작업 → Event Loop 랔로킹
@app.post("/upload")
async def upload(file: UploadFile):
    content = await file.read()
    df = pd.read_excel(io.BytesIO(content))  # 랔로킹!

# ✅ run_in_executor로 별도 슀레드에서 처늬
@app.post("/upload")
async def upload(file: UploadFile):
    content = await file.read()
    loop = asyncio.get_event_loop()
    df = await loop.run_in_executor(None, pd.read_excel, io.BytesIO(content))


2구간: DB Insert — 방식에 따띌 성능읎 극닚적윌로 갈늰닀

같은 10만 걎 Insert띌도 얎떻게 처늬하느냐에 따띌 Event Loop 동작읎 완전히 달띌진닀.

❌ 최악: 걎별 await (직렬)

1
2
3
for (const row of rows) {
  await db.insert(row); // 10만 번 순서대로 DB 왕복 대Ʞ
}
1
2
3
4
Timeline:
[await insert 1]──[await insert 2]──[await insert 3]── ... × 100,000
각 await마닀 Event Loop가 닀륞 요청을 처늬할 수 있지만,
읎 API 요청 자첎는 수분읎 걞늰닀.

await가 있얎서 Event Loop륌 랔로킹하진 않지만, DB 왕복 지연읎 10만 번 쌓읞닀.

⚠ 죌의: Promise.all (묎제한 병렬)

1
2
3
await Promise.all(rows.map(row => db.insert(row)));
// 10만 개의 Promise륌 동시에 생성 → DB 컀넥션 풀 슉시 고갈
// 였히렀 더 느렀지거나 에러 발생

✅ 싀묎 정석: Chunk 닚위 Bulk Insert

1
2
3
4
5
6
const CHUNK_SIZE = 1000;

for (let i = 0; i < rows.length; i += CHUNK_SIZE) {
  const chunk = rows.slice(i, i + CHUNK_SIZE);
  await db.bulkInsert(chunk); // 1000걎을 당 한 번의 DB 쿌늬로
}
1
2
3
Timeline:
[bulk 1~1000]──[bulk 1001~2000]── ... × 100회
DB 왕복읎 10만 번 → 100번윌로 쀄얎든닀

낎부적윌로 읎런 쿌늬 한 번윌로 처늬된닀.

1
2
3
4
INSERT INTO users (name, email) VALUES
  ('Alice', 'a@a.com'),
  ('Bob',   'b@b.com'),
  ... (1000걎)


진짜 대용량읎띌멎: Event Loop 밖윌로 꺌낎띌

수십만, 수백만 걎 수쀀읎 되멎 API 서버의 Event Loop 안에서 처늬하는 것 자첎가 잘못된 섀계닀. Message Queue륌 사용핎 처늬륌 완전히 분늬한닀.

1
2
3
4
5
6
7
Client
  │
  ▌
API 서버 ──→ Message Queue (Redis, RabbitMQ) ──→ Worker 프로섞슀
  │          (파음 ID만 전달)                      (싀제 처늬 닎당)
  │
  └──→ 202 Accepted + jobId 슉시 반환
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// NestJS: API는 큐에 작업을 넣고 슉시 반환
@Post('/upload')
async uploadExcel(@UploadedFile() file) {
  const fileId = await this.storage.save(file);
  await this.queue.add('batch-insert', { fileId });
  return { status: 'accepted', jobId: fileId }; // 슉시 반환
}

// 별도 Worker: Event Loop 걱정 없읎 처늬
@Process('batch-insert')
async handleBatchInsert(job: Job) {
  const rows = await parseExcel(job.data.fileId);
  for (let i = 0; i < rows.length; i += 1000) {
    await db.bulkInsert(rows.slice(i, i + 1000));
  }
}

읎렇게 하멎 API 서버의 Event Loop는 항상 가볍고, 묎거욎 작업은 Worker가 전닎한닀.


정늬

구간Event Loop 영향핎결책
엑셀 파싱 (CPU)⛔ 랔로킹Worker Thread / run_in_executor
걎별 INSERT (직렬)🐢 느늬지만 비랔로킹Chunk Bulk INSERT로 교첎
묎제한 Promise.all⛔ 컀넥션 풀 고갈Chunk 닚위 처늬
수백만 걎 대용량⛔ 섀계 묞제Message Queue + Worker 분늬


9. ì–Žë–€ 상황에 ì–Žë–€ 프레임워크?

1
2
3
I/O 집앜적 + 빠륞 개발 + JS 생태계       →  NestJS
I/O 집앜적 + ML/데읎터 + Python 생태계  →  FastAPI
엔터프띌읎슈 + 슀튞늬밍 + Java 생태계    →  WebFlux


🌿 마치며

Event Loop는 처음에는 추상적읞 개념처럌 느껎지지만, ê²°êµ­ “Ʞ닀늬지 말고, 끝나멎 알렀쀘” 띌는 닚순한 아읎디얎의 구현읎닀.

NestJS, FastAPI, WebFlux는 각자의 방식윌로 읎 아읎디얎륌 싀현하고 있닀. ì–Žë–€ 프레임워크륌 선택하든, Event Loop가 ì–žì œ 랔로킹되는지륌 읎핎하고 있닀멎 성능 묞제의 절반은 읎믞 핎결한 셈읎닀.

귞늬고 묎거욎 작업 앞에서는 “얎떻게 비동Ʞ로 잘 처늬할까”볎닀 “읎걞 Event Loop 밖윌로 꺌낌 수 있을까” 륌 뚌저 떠올늬는 것읎 성숙한 섀계의 시작읎닀.

This post is licensed under CC BY 4.0 by the author.