Blog
Anki
소프트웨어 공학

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}`));
    });
  });
}
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();
}
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' });

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 쓰셨나요?

답 보기