비동기 프로그래밍의 장점과 Node.js의 내부

맛없는콩두유·2023년 2월 13일
0


이번 노트에서는 Node.js에 구현된 '비동기 실행'에 대해서 더 깊게 공부해보겠습니다.
일단 '비동기 실행'과 '동기 실행'이 뭔지 확실하게 이해한 분들만 이 노트를 읽어주시고, 아직 이해하지 못한 분들은 꼭 이전 영상의 내용을 확실하게 이해하고 와주세요.

1. '비동기 실행'의 장점

'비동기 실행'이 뭔지 이해했다면, 이제 '비동기 실행'이 '동기 실행'에 비해 어떤 점이 좋은지 배워봅시다. 이전 영상에서 예로 들었던 '식당 종업원' 예시를 다시 생각해봅시다. 그때 저는 식당에서 이루어지는 하나의 작업은

'손님으로부터 주문을 받고,
해당 메뉴를 만든 다음,
그것을 다시 손님에게 전달하는 것'

이라고 했는데요. 하나의 작업을 이렇게 3가지의 작은 단위로 쪼갰을 때, 각각 다음과 같은 시간이 걸린다고 해봅시다.

(1) 손님으로부터 주문을 받는 시간 - 1분

(2) 해당 메뉴를 요리하는 시간 - 쉬운 메뉴는 5분 / 어려운 메뉴는 10분

(3) 메뉴를 손님에게 전달하는 시간 - 1분

이런 가정하에 , '동기 실행'과 '비동기 실행'의 차이를 알아봅시다.

만약 식당에 손님 총 3명이 온 경우, 종업원이 '동기 실행' 방식으로 작업을 한다고 했을 때, 모든 손님이 자신의 메뉴를 주문하고 받기까지 총 몇 분이 걸릴까요?

동기 실행의 경우, 손님 한 명에 관한 작업 하나(주문받기, 요리하기, 전달하기)가 완료되지 않으면 다음 손님에 관한 작업을 처리할 수 없습니다. 지금 작업 하나당 걸리는 시간은 손님이 쉬운 메뉴를 시킨 경우 7분이고, 손님이 어려운 메뉴를 시킨 경우 12분입니다.

만약 '쉬운 메뉴를 시킨 손님 - 어려운 메뉴를 시킨 손님 - 쉬운 메뉴를 시킨 손님', 이 순서라면

위 그림처럼 '7분 + 12분 + 7분'으로 총 26분이 걸립니다.

그렇다면 '비동기 실행' 방식으로 종업원이 작업하는 경우는 어떨까요? 바로 아래 그림을 보세요.

'비동기 실행' 방식의 경우 종업원은 주문을 받고, 그 메뉴가 준비될 때까지 기다리지 않습니다. 바로 그다음 손님의 주문을 받아버리죠. 그 대신, 요리가 완성되면 요리사가 전광판을 통해 알려주기 때문에 종업원이 완성된 메뉴를 손님에게 전달하는 데는 문제가 없습니다. 이렇게 비동기 실행에서는 요리사들이 요리를 하는 도중에도 종업원은 자신의 일을 진행할 수 있기 때문에 전체 시간이 훨씬 단축됩니다. 조금 전과 같은 세 명의 손님이라도 13분밖에 걸리지 않죠.

자, 어떤가요? '동기 실행'에 비해 '비동기 실행'이 훨씬 더 빠르죠?

Node.js가 등장하기 이전에는 동기 실행되는 서버 프로그램이 많았습니다. 그래서 개발자들도 동기 실행만 생각하고 코드를 작성하는 경우가 많았죠. 하지만 Ryan Dahl이 동기 실행 방식으로만 프로그래밍하는 것의 문제점을 제기하고 Node.js를 발표하면서, 개발자들은 이제 '비동기 실행'되는 프로그램을 훨씬 쉽게 만들 수 있게 되었습니다.

2. Node.js에서의 비동기 실행

이제 Node.js에서 '비동기 실행'이 구체적으로 어떤 방식으로 이루어지는지 간단히 살펴보겠습니다.
사실 Node.js에서는 작업의 종류에 따라 '비동기 실행'이 다른 방식으로 구현되어 있습니다. 하지만 이번 노트에서는 일단 여러분이 이해하기 쉬운 한 가지만 설명하도록 하겠습니다.

그 전에 한 가지 알아야 하는 개념이 있습니다. 바로 컴퓨터 공학에서 배우는 중요 개념인

'프로세스(Process)'와
'스레드(Thread)'

라는 개념입니다. 프로세스와 스레드는 프로그램의 실행 흐름에 관한 개념인데요. 프로세스가 하나의 실행 흐름이라면, 스레드는 그 안에 있는, 더 작은 단위의 실행 흐름입니다.

예를 하나 들어볼게요.

우리가 노트북에서 크롬 브라우저를 실행했을 때를 잠깐 생각해봅시다.
크롬 브라우저는 노트북에 저장된 하나의 '프로그램(Program)'인데요.
우리가 크롬 브라우저 아이콘을 더블 클릭하면, 이 프로그램이 '실행'됩니다.
이 '실행'이라는 건

(1) 하드디스크(hard-disk)나 SSD에 저장되어 있던 프로그램의 내용을,
(2) 메모리(memory)에 올려서
(3) CPU(Central Processing Unit)가 실행하도록 만드는 것을 의미하는데요.

크롬 브라우저를 실행하면, 그 실행 흐름으로

하나의 크롬 '프로세스'라는 것이 생성되고,
그 안에서 하나의 '스레드'가 실행 중인 상태가 됩니다.

바로 이런 상태죠.

그 뒤에 만약 크롬 브라우저에서 영화 파일 하나를 다운로드하고(Thread-2), 최신 유행 음악을 재생하면서(Thread-3), 구글 검색(Thread-4)을 하는 등 새로운 작업을 시작할 때마다, 크롬 프로세스 안의 스레드 수는 하나씩 늘어납니다. 아래 그림처럼요.

컴퓨터가 가끔 먹통이 되면, 우리는 '프로세스 강제 종료'라는 걸 하죠? 이때 우리가 '강제 종료'를 하는 단위가 프로세스인 것이고, 프로세스 안에서 만들어낼 수 있는 '실행의 최소 단위'가 바로 스레드인 겁니다. 프로세스와 스레드가 뭔지 이해되시나요? 일단은 이 정도까지만 이해하고 넘어갑시다. 혹시 더 깊이 알고 싶은 분은 '운영체제(OS)' 과목을 공부해보세요.

자, 이제 우리는 스레드라는 단위에 집중해보겠습니다. 스레드를 '식당 종업원 예시'에 적용해볼게요. 예시로 봤던 식당에서,

'주문을 받는 종업원 1명',
'요리사 10명'이 일하는 중이라고 해봅시다.

이 식당에서 '비동기 실행' 방식으로 일을 하는 경우, 각자의 역할을 정리해보면

1) 카운터 종업원 1명

손님 주문받기
메뉴 요리 시작해달라고 요리사에게 요청하기
바로 다음 손님의 주문받기
메뉴가 완성되었다는 알림을 받으면
완성된 메뉴를 손님에게 전달해주기

2) 요리사 10명

메뉴 요리하기(오래 걸리는 일)
요리 완료되면 종업원에게 전광판으로 알리면서 메뉴 전달하기

이렇게 되는데요. 위 내용을 사람 한 명이 스레드 하나라고 가정하고 설명해볼게요.

  • Node.js 내부에서의 비동기 실행 구현 방법 중 한 가지

    1) 스레드 1개(이 스레드를 '메인 스레드'라고 합니다)

    자바스크립트 코드 실행하기
    이때 오래 걸리는 작업(작업 A)은 다른 스레드에 넘기기(ex. fs.readFile('new', callback)에서 파일 읽기는 별도 스레드에 넘기기)
    그리고 일단 그다음 작업 B를 시작하기
    작업 A가 완료되었다는 알림과 그 작업 결과를 받으면
    작업 결과를 가지고 콜백 실행하기(ex. 읽어 들인 파일 내용을 인자로 넣고, callback 실행)

    2) 스레드 10개

    메인 스레드가 요청한 작업 처리하기(ex. 파일 읽기)
    작업이 완료되면 끝났다고 메인 스레드에 알려주고, 작업 결과 전달하기(ex. 파일 내용을 다 읽고, 그 내용을 메인 스레드에 전달)

비유가 이해되시나요? 이 구조의 특징을 정리하자면,

  1. 메인 스레드는 빠르게 처리할 수 있는 작업들을 집중해서 '혼자' 처리하고,
  2. 파일 읽기와 같이 시간이 오래 걸리는 작업은 다른 스레드에 맡긴다는 점인데요.

잠깐 '비동기 실행'을 설명한 이미지를 다시 보면서 설명할게요.

여기서

'손님으로부터 주문받기(분홍색)', '손님에게 완성된 메뉴 전달하기(초록색)' 작업, 이 2가지는 비교적 빠르게 처리할 수 있는 작업,
'메뉴 요리하기(파란색)'가 파일 내용 읽기 작업처럼 비교적 시간이 오래 걸리는 작업

이라고 생각하면 됩니다.

비동기 실행이 Node.js에서 어떤 식으로 이루어지는지, 조금은 이해되시죠?
그런데 사실 방금은 Node.js가 File I/O(File Input/Output, 파일 입출력)라는 특정 작업에 관한 비동기 실행을 어떻게 지원하는지에 대해서만 설명한 건데요. 다른 종류의 작업에 대해서 Node.js는 또다른 방식으로 비동기 실행을 지원합니다. 하지만 여기서 그런 것들까지 다 설명하려면 내용이 길어지고 난이도가 높아지기 때문에 생략하겠습니다. Node.js의 비동기 실행은 libuv라는 라이브러리를 통해서 이루어지기 때문에 따로 더 공부하고 싶은 분들은 libuv 공식 문서의 내용을 참조하세요.

3. Node.js로 개발할 때의 주의점

이제 Node.js로 개발할 때 주의해야할 점을 하나 말씀드리겠습니다. 다시 '식당 종업원 예시'를 들어볼게요.
만약 비동기 방식으로 작업을 하고 있는 종업원이 손님과 다툼이 났거나, 손님의 주문 내용이 너무 많다면 어떻게 될까요?
그럼 제아무리 비동기 작업 방식이라고 해도 전체 서비스 속도는 급속도로 저하될 겁니다.
그 뒤의 손님들은 '왜 빨리 내 주문을 받지 않느냐'며 불평을 늘어놓을 거구요.

사실 이 경우는 개발자가 Node.js로 개발을 할 때 메인 스레드가 하는 작업에 큰 부하를 주는 경우를 비유한 겁니다.
Node.js에서 메인 스레드는

CPU로 하는 수치 계산 작업이나
네트워크로 들어오는 클라이언트의 요청을 받아들이고 응답하는 작업

을 수행하는데요. 예를 들어, 무거운 CPU 수치 계산 작업을 생각해봅시다.
고화질의 이미지를 처리하거나, 복잡한 수식 처리를 할 때는 CPU를 집중적으로 사용하게 됩니다. 이런 작업을 CPU intensive job이라고 하는데요. Node.js에서는 CPU 수치 계산 작업을 메인 스레드가 처리하기 때문에, CPU intensive job은 Node.js의 성능을 극도로 저하시키는 원인이 됩니다.

예를 들어, 아래 이미지에서 가장 마지막의 14분짜리 작업을 봅시다.

이 작업이 14분이 걸리는 CPU 작업이라고 해봅시다.
그럼 Node.js의 메인 스레드는 이 14분짜리 작업을 끝내기 전에는 다른 클라이언트의 요청을 처리하지 못하게 됩니다. File I/O 작업만큼은 빠르게 다른 스레드에 맡길 수 있었지만, CPU 수치 계산 작업은 메인 스레드에서 수행하도록 설계되었기 때문입니다.
그래서 다른 클라이언트의 요청이 그만큼 늦게 처리(이미지에서 깃발이 꽂힌 시점부터 가능)되는 결과를 가져옵니다.

정리하면, Node.js는 CPU-intensive job(고화질 이미지 처리, 복잡한 시뮬레이션 계산, 딥러닝 작업 등)에는 적절하지 않습니다. 그리고 혹시라도 이런 job을 처리해야하는 경우라면 해당 작업을 최적화하도록 노력해야하구요. 만약 CPU-intensive job을 메인으로 처리해야하는 서버라면 Node.js보다는 다른 서버 프레임워크 등을 사용하는 게 좋습니다. 하지만 우리가 일반적으로 만들 서비스(채팅 서버 등)에서는 CPU intentsive job을 해야할 경우가 많지 않기 때문에 너무 걱정하지는 마세요.

방금 배운 것처럼 Node.js로 개발할 때는 메인 스레드에 부하를 주면 안 된다는 사실, 잘 기억하세요!

출처: 코드잇

profile
하루하루 기록하기!

0개의 댓글