Node.js는 싱글스레드인데 CPU Bound 연산이 들어오면 어떻게 되나요?
꼬리 질문
- Event Loop가 블로킹되면 구체적으로 어떤 작업들이 대기하게 되나요?
 - Worker Threads와 Child Process의 차이점은 무엇인가요?
 - CPU Bound 작업을 판단하는 기준은 무엇인가요?
 - Worker Pool의 최적 Worker 개수를 어떻게 결정하나요?
 
답변 보기
- 핵심 포인트 💡
- Node.js는 싱글스레드 Event Loop 기반으로 동작하여 CPU Bound 연산 시 Event Loop이 블로킹됨
 - CPU 집약적 작업은 Worker Threads로 분리하여 메인 스레드 블로킹 방지
 - setImmediate()를 활용한 작업 분할로 작은 CPU 작업의 블로킹 최소화
 - Worker Pool 패턴으로 효율적인 리소스 관리 (최적 개수: CPU 코어 수 - 1)
 
 - 블로킹 발생 원리
- Event Loop는 하나의 메인 스레드에서 순차적으로 작업 처리
 - CPU 작업 완료까지 다음 작업들이 모두 대기
- HTTP 요청 처리
 - 타이머 콜백 실행
 - I/O 이벤트 처리
 - 새로운 연결 수락
 
 - 실제 성능 영향
- 정상 처리: 약 4ms
 - CPU 블로킹 시: 약 900ms
 - 성능 저하: 225배
 
 
 - 블로킹을 일으키는 Node.js API
- 암호화: 
crypto.randomBytes(),crypto.pbkdf2Sync() - 압축: 
zlib.inflateSync(),zlib.deflateSync() - 파일 동기 API: 
fs.readFileSync(),fs.writeFileSync() - 프로세스: 
child_process.spawnSync(),child_process.execSync() - JSON 처리: 50MB JSON 파싱 약 1.3초, stringify 약 0.7초
 - 출처: Node.js 공식 문서 - Don't Block the Event Loop (opens in a new tab)
 
 - 암호화: 
 - ✅ 해결 방법 1: Worker Threads (권장)
 
import {
  Worker,
  isMainThread,
  parentPort,
  workerData,
} from 'node:worker_threads';
 
if (!isMainThread) {
  // Worker 스레드에서 실행되는 CPU 집약적 작업
  const { parse } = await import('some-js-parsing-library');
  const script = workerData;
  parentPort.postMessage(parse(script));
}
 
export default function parseJSAsync(script) {
  return new Promise((resolve, reject) => {
    const worker = new Worker(new URL(import.meta.url), {
      workerData: script,
    });
    worker.on('message', resolve);
    worker.on('error', reject);
    worker.on('exit', (code) => {
      if (code !== 0)
        reject(new Error(`Worker stopped with exit code ${code}`));
    });
  });
}- 출처: Node.js 공식 문서 - Worker Threads (opens in a new tab)
 - ✅ 해결 방법 2: 작업 분할 (setImmediate)
 
function processLargeData(data) {
  let index = 0;
  const chunkSize = 1000;
 
  function processChunk() {
    const end = Math.min(index + chunkSize, data.length);
 
    // 청크 처리
    for (let i = index; i < end; i++) {
      // 데이터 처리 로직
    }
 
    index = end;
 
    if (index < data.length) {
      // Event Loop에 제어권 반환
      setImmediate(processChunk);
    }
  }
 
  processChunk();
}- 출처: Preventing Event Loop Blocking for CPU-Intensive Tasks (opens in a new tab)
 - ✅ 해결 방법 3: Child Process
 
import { fork } from 'node:child_process';
 
const child = fork('./cpu-intensive-worker.js');
 
child.on('message', (result) => {
  console.log('받은 결과:', result);
});
 
child.send({ data: 'process this' });- 출처: Handling CPU-Bound Tasks in Node.js (opens in a new tab)
 - Worker Threads 사용 기준
- ✅ 사용해야 하는 경우
- 이미지 처리
 - 동영상/오디오 인코딩/디코딩
 - 데이터 압축
 - 암호화 연산
 - 복잡한 수학 계산
 - 대용량 데이터 파싱
 
 - ❌ 사용하지 말아야 하는 경우
- I/O 집약적 작업 (파일 읽기/쓰기, HTTP 요청, 데이터베이스 쿼리)
 - 간단한 비동기 작업 (Promise, async/await으로 충분)
 
 - 출처: Better Stack - Node.js Multithreading Guide (opens in a new tab)
 
 - ✅ 사용해야 하는 경우
 - 실제 벤치마크 (2024년)
- Worker Threads 사용 시 10초 동안 약 435,000개의 동시 요청 처리
 - 메인 Event Loop 블로킹 완전히 방지
 - 출처: Dealing with CPU-bound Tasks in Node.js - AppSignal Blog (opens in a new tab)
 
 
GraphQL이란 무엇이고 어떤 장단점이 있을까요?
답 보기
- GraphQL은 Facebook이 개발한 API용 쿼리 언어
 - 장점
- 필요한 리소스만 요청할 수 있어 네트워크 비용 감소
 - 하나의 엔드포인트로 관리할 수 있어서 관리 포인트 감소
 
 - 단점
- 보안에 안좋음
 - 캐싱이 기본적으로 불가능함
 - 러닝 커브
 
 - 예시
- 아래와 같이 하나의 graphql이라는 하나의 endpoint에 직접 쿼리를 전송함
 
 
curl -X POST \
-H "Content-Type: application/json" \
-d '{
  "query": "query { getAllBooks(orderBy: \"title\", page: 0, size: 5) { id title author } }"
}' \
http://localhost:8080/graphql- 구현 방법
- 구현할 endpoint는 따로 없고 type을 따로 정의한 후 component로 사용 가능한 method를 정의해줌
 - service에는 GraphQL과 Repository를 이어주는 로직
 - type 정의
- 
type Query { getAllBooks: [Book] getBookById(id: ID!): Book } type Mutation { createBook(title: String!, author: String!): Book updateBook(id: ID!, title: String, author: String): Book deleteBook(id: ID!): Boolean } type Book { id: ID! title: String! author: String! } 
 - 
 - Java 코드
- 
@Component public class BookResolver { private final BookRepository bookRepository; public BookResolver(BookRepository bookRepository) { this.bookRepository = bookRepository; } // Query Resolvers public List<Book> getAllBooks() { return bookRepository.findAll(); } public Optional<Book> getBookById(Long id) { return bookRepository.findById(id); } // Mutation Resolvers public Book createBook(String title, String author) { Book book = new Book(); book.setTitle(title); book.setAuthor(author); return bookRepository.save(book); } public Book updateBook(Long id, String title, String author) { Book book = bookRepository.findById(id).orElseThrow(() -> new RuntimeException("Book not found")); if (title != null) book.setTitle(title); if (author != null) book.setAuthor(author); return bookRepository.save(book); } public boolean deleteBook(Long id) { if (bookRepository.existsById(id)) { bookRepository.deleteById(id); return true; } return false; } } @Component public class BookGraphQLController implements GraphQLQueryResolver, GraphQLMutationResolver { private final BookResolver bookResolver; public BookGraphQLController(BookResolver bookResolver) { this.bookResolver = bookResolver; } // Query public List<Book> getAllBooks() { return bookResolver.getAllBooks(); } public Book getBookById(Long id) { return bookResolver.getBookById(id).orElse(null); } // Mutation public Book createBook(String title, String author) { return bookResolver.createBook(title, author); } public Book updateBook(Long id, String title, String author) { return bookResolver.updateBook(id, title, author); } public boolean deleteBook(Long id) { return bookResolver.deleteBook(id); } } 
 - 
 
 
왜 Redis 쓰셨나요?
답 보기
- Memcached와의 비교
- Memcached 단점
- pub/sub, sorted set, hyperloglog 등을 지원하지 않음
 - 캐시 교체정책을 lru밖에 지원하지 않음
 - replication, sharding을 지원하지 않음
 - 트랜잭션이 안됨
- Redis는 트랜잭션, Lua Script 등으로 Atomic 연산을 수행하는 다양한 방법이 있음
 
 
 - Memcached 장점
- 다중 스레드를 사용해서 동작하기 때문에 대규모 데이터 세트를 저장하고 관리할 때 더 빠른 성능을 보임
 
 - Reference
 
 - Memcached 단점