백엔드 입문 (1) - Node.js로 백엔드 시작해보기
Node.js와 Express에 대한 것은 간략하게 아래 글에서 작성했었다.
https://november-itstime.tistory.com/80
Node.js + express로 백엔드 서버 구축 도전기 (1) - 소개
회사에서 사내 랜딩 페이지 백엔드를 구축해보라는 과제를 받아서 도전해본 것들을 블로그에 정리하며 개념을 학습하고자 한다.사실 개발하면서 책과 각종 자료들을 참고하긴 했으나 개념 학
november-itstime.tistory.com
이번 포스팅에서는 조금 더 구체적으로 Node.js에 대해 살펴보고자 한다.
1. Node.js가 서버에서 자바스크립트를 실행하는 방법
: Node.js는 V8 자바스크립트 엔진과 C/C++ 등에 의존성을 가진 자바스크립트 런타임이다. 이 때 런타임이란 자바스크립트로 된 프로그램을 실행할 수 있는 프로그램을 말한다. (반대로 C언어는 런타임 없이 코드를 실행하는 컴파일 언어이다.)
1) Node.js의 구성 요소
: Node.js의 소스 코드는 C++와 JS , Python 등으로 이루어져 있다.
이 때 Node.js는 각 계층이 각 하단에 있는 API를 사용하는 계층의 집합으로 설계되어 있다.
- 사용자 코드(JavaScript)는 Node.js의 API를 사용함
- Node.js API는 C++에 바인딩 되어 있는 소스이거나 직접 만든 C++ 애드온을 호출한다.
- C++에서는 V8를 사용해 자바스크립트를 해석 및 최적화하고 어떤 코드냐에 따라 C/C++ 종속성이 있는 코드를 실행한다.
- 또한 DNS , HTTP 파서 , OpenSSL, zlib 이외의 C/C++ 코드들은 libuv의 API를 사용해 해당 운영체제 알맞는 API를 사용한다.
특히 V8과 libuv가 제일 중요한데 V8은 자바스크립트 코드를 실행하도록 해주고, libuv는 이벤트 루프 및 운영체제 계층 기능을 사용하도록 API를 제공한다.
2) V8 엔진이란?
: V8은 C++로 만든 오픈 소스 자바스크립트 엔진이다. 여기서 엔진이란 사용자가 작성한 코드를 실행하는 프로그램을 말하는데, 엔진은 파서, 컴파일러, 인터프리터, 가비지 컬렉터, 콜 스택 , 힙으로 구성되어 있다.
: 인터프리터 역할을 하는 이그니션과 컴파일러 역할을 하는 터보팬을 사용해 컴파일 한다.
* 콜 스택 : 콜 스택은 현재 실행 중인 서브 루틴에 관한 정보를 저장하는 스택이다.
3) V8엔진의 자바스크립트 코드 컴파일 단계
1. 파서에 전달 됨
2. 추상 구문 트리로 만들어짐
3. 이그니션 인터프리터에 전달 됨
4. 이그니션은 추상 구문 트리를 바이트 코드로 만든다.
5. 최적화가 필요한 경우이면 터보팬으로 넘긴다.
6. 터보팬에서 컴파일 과정을 걸쳐 바이너리 코드가 된다.
7. 최적화가 잘 안 된 경우는 다시 최적화를 해제하고 이그니션의 인터프리터 기능 사용
=> 이처럼 인터프리터와 컴파일러의 장점을 동시에 가지고 있는 프로그램을 JIT라고 한다. 빠르지만 메모리를 더 많이 잡아 먹는다.
4) 이벤트 루프와 운영체제 단 비동기 API 및 스레드 풀을 지원하는 libuv
: V8 엔진을 사용해 서버에서 JS를 실행할 수 있단 것은 알았지만 어떻게 JS에 없는 HTTP 파일, 소켓 통신 IO 기능 등을 Node.js는 실행할 수 있을 지 살펴본다.
=> libuv 라는 C++ 라이브러리를 사용해 해결한다. (JS 언어에서 C++ 코드를 감싸서 사용하는 방식)
* libuv : 비동기 입출력, 이벤트 기반에 초점을 맞춘 라이브러리.
5) Node.js 아키텍처
: 정리하자면 V8 엔진으로 코드를 실행하며, 런타임에 필요한 이벤트 루프 및 운영체제 시스템 API는 libuv 라이브러리를 사용한다.
1. 애플리케이션에서 요청이 발생하면 V8 엔진은 자바스크립트로 된 코드를 기계어나 바이트 코드로 변경
2. JS로 작성된 Node.js의 API는 C++로 작성된 코드를 사용한다.
3. V8엔진은 이벤트 루프로 libuv를 사용하고 전달된 요청을 libuv 내부의 이벤트 루프에 추가한다.
4. 이벤트 큐에 쌓인 요청은 이벤트 루프에 전달되고, 운영체제 커널에 비동기 처리를 맡긴다. 이 때 운영체제 내부적으로 비동기 처리가 힘든 경우 워커 스레드에서 처리한다.
5. 운영체제의 커널 또는 워커 스레드가 완료한 작업은 다시 이벤트 루프로 전달 된다.
6. 이벤트 루프에서는 콜백으로 전달된 요청에 대한 완료 처리를 하고 넘긴다.
7. 완료 처리된 응답을 Node.js 애플리케이션으로 전달 한다.
* Node.js의 프로세스는 싱글 스레드 하나와 비동기 처리를 지원하는 스레드 풀로 구성되어 있다.
6) Node.js의 특징
1. 싱글 스레드
: 콜 스택이 하나이므로 한 번에 하나의 작업만 가능하다.
2. 이벤트 기반 아키텍처
: 싱글스레드임에도 동시 요청을 처리하는 방법은 이벤트 기반 아키텍처를 적용한다. 즉, 콜 스택에 쌓인 작업을 다른 곳에서 처리한 다음 처리가 완료 되었을 때 알림을 받으면 스레드가 하나라도 빠르게 처리할 수 있다.
ex) 커피숍에서 주문 시 주문은 제조를 하는 직원에게 건네짐 , 이 기간동안 주문은 기다리지 않고 계속 다음 주문 받음
1) JS 코드는 V8의 콜 스택에 쌓이고 I/O 처리가 필요한 코드는 이벤트 루프로 보내게 된다.
2) 이벤트 루프에서는 말 그대로 루프를 실행하며 운영체제 또는 스레드 워커에 I/O 처리를 맡긴다.
3) 스레드 워커와 운영체제는 받은 요청에 대한 결과를 이벤트 루프로 돌려줌
4) 이벤트 루프에서는 결괏값에 대한 코드를 콜 스택에 다시 추가함
=> 결론적으로 Node.js는 오래 걸리는 일을 이벤트 루프에 맡긴다.
7) 이벤트 루프
: 여러 개의 FIFO 큐로 이루어져 있음.
2. Node.js 예시
const http = require("http"); // http 객체 생성
let count = 0;
const server = http.createServer((req,res) => { // 서버 인스턴스를 만드는 함수
log(count);
res.statusCode = 200;
res.setHeader("Content-Type", "text/plain"); // 헤더 설정
res.write("hello world");
setTimeout(() => {
res.end("Node.js");
}, 2000);
});
function log(count) {
console.log(count+=1);
}
server.listen(8000); // 8000번 포트로 서버 실행
* 포트 번호 : 16비트로 이루어져 있음, 0~1023 포트는 루트 권한이 필요하며 , 1024 ~ 49151번 구간은 기관이나 사업자들을 위해 IANA에 등록되어 있지만 임의로 사용 가능 , 49152 ~ 65353번의 구간은 일반 사욪아들이 자유롭게 사용할 수 있다.
ex) 20 - FTP 데이터 포트 , 22 - SSH , 24 - SMTP - 이메일 전송, 110 - POP3 , 80 - HTTP , 53 - DNS , 443 - HTTPS
* curl : 기본적으로 브라우저에서는 GET 요청만 확인할 수 있는데 curl을 사용 하면 POST , DELET ,PUT 등을 테스트 할 수 있다.
3. 실제 성능 테스트 하기
: 현업에서는 새로 만든 API 중 성능에 문제가 있을 것 같은 API 들을 개별로 또는 묶어서 실제 유저의 트래픽이 들어오는 것처럼 테스트 한다.
: K6 이라는 성능 테스트 도구를 사용함(자바스크립트로 테스트 가능)
: brew install k6 or https://dl.k6.io/msi/ 에서 파일 설치
import http from "k6/http";
export const options = { // 테스트 옵션 (100명이 10초동안 동시에 계속해서 요청을 보낸 단 뜻)
vus: 100,
duration: "10s",
};
export default function () {
http.get("http://localhost:8000"); // 테스트에 사용할 함수 지정
}