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 단점