4장 http 모듈로 서버 만들기
1. 요청과 응답 이해하기
클라이언트 -요청(request)-> 서버 -응답(response)-> 클라이언트
=> 서버) 요청을 받는 부분과 응답을 보내는 부분 필요
요청과 응답: 이벤트 방식 -> 어떤 작업 수행할지 이벤트 리스너 등록
// server1.js
const http = require('http');
http.createServer((req, res) => {
// 어떻게 응답할지 적어줌
res.write(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.write('<h1>Hello Node!</h1>');
res.end('<p>Hello Server!</p>');
})
.listen(8080, () => { // 서버 연결
console.log('8080번 포트에서 서버 대기 중');
});
- request -> `req`: 요청에 관한 정보
- response -> `res`: 응답에 대한 정보'
- `res.writeHead`: 응답에 대한 정보를 기록하는 메서드 = 헤더(header)
- 첫 번째 인수: 성공적인 요청(200)
- 두 번째 인수: 응답에 대한 정보 - 콘텐츠의 형식 HTML/ 한글 표시를 위해 charset을 utf-8로 지정
- `res.write`: 데이터가 기록되는 부분 = 본문(body)
- 첫 번째 인수: 클라이언트로 보낼 데이터 - 문자열/ 버퍼, 여러 번 호출해서 데이터 여러 개 O
- `res.end`: 응답을 종료하는 메서드/ 인수 O -> 데이터를 클라이언트로 보내고 응답 종료
브라우저) 응답 내용 받아서 렌더링
- `res.writeHead`: 응답에 대한 정보를 기록하는 메서드 = 헤더(header)
$ node server1
8080번 포트에서 서버 대기 중
=> `http://localhost:8080` or `http://127.0.0.1:8080`
-> `Ctrl + C`: 서버 종료
`listen` 메서드에 콜백 함수 넣는 대신, `listening` 이벤트 리스너 + `error` 이벤트 리스너
// server1-1.js
const http = require('http');
http.createServer((req, res) => {
res.write(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.write('<h1>Hello Node!</h1>');
res.end('<p>Hello Server!</p>');
});
server.listen(8080);
server.on('listening', () => {
console.log('8080번 포트에서 서버 대기 중');
});
server.on('error', (error) => {
console.error(error);
});
서버 소스 코드 변경 -> 서버 종료 후 다시 실행(자동으로 변경 사항 반영 X)
한 번에 여러 서버 실행
// server1-2.js
const http = require('http');
http.createServer((req, res) => {
res.write(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.write('<h1>Hello Node!</h1>');
res.end('<p>Hello Server!</p>');
})
.listen(8080 () => // 서버 연결
console.log('8080번 포트에서 서버 대기 중');
});
http.createServer((req, res) => {
res.write(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.write('<h1>Hello Node!</h1>');
res.end('<p>Hello Server!</p>');
})
.listen(8080 () => // 서버 연결
console.log('8081번 포트에서 서버 대기 중');
});
=> `http://localhost:8080`와, `http://localhost:8081` 주소로 서버에 접속할 수 있음
포트 번호 같을 경우 `EADDRINUSE` 에러
=> `res.write`와 `res.end`에 일일이 `HTML` 적는 것은 비효율적 -> 미리 `HTML` 파일 만들어두기
`HTML` 파일은 `fs` 모듈로 읽어서 전송할 수 있음
// server2.html
<!DOCTYPE html>
<html>
<meta charset="utf-8" />
<title>Node.js 웹 서버</title>
<head>
</head>
<body>
<h1>Node.js 웹 서버</h1>
<p>두근</p>
</body>
</html>
// server2.js
const http = require('http');
const fs = require(async (req, res) => {
try {
const data = await fs.readFile('./server2.html');
res.writeHead(200, { 'Content-Type': 'text/plain' charset=utf-8' });
res.end(data);
}
catch (err) {
console.error(err);
res.writeHead(500, { 'Content-Type': 'text/plain' charset=utf-8' });
res.end(err.message);
}
})
.listen(8081, () => {
console.log('8081번 포트에서 서버 대기 중');
});
- 요청 -> `fs` 모듈로 `HTML` 파일 읽음
- `data` 변수에 저장된 버퍼 -> 클라이언트
- 예기치 못한 에러 발생 -> 에러 메시지
- 에러 메시지: 일반 문자열 -> `text/plain`
node server2
8081번 포트에서 서버 대기 중
HTTP 상태 코드
- 2XX: 성공
- 200(성공)/ 201(작성됨)
- 3XX: 리다이렉션(다른 페이지로 이동)
- 301(영구 이동)/ 302(임시 이동)/ 304(수정되지 않음)_요청의 응답으로 캐시 사용
- 4XX: 요청 오류
- 400(잘못된 요청)/ 401(권한 없음)/ 403(금지됨)/ 404(찾을 수 없음)
- 5XX: 서버 오류
- 500(내부 서버 오류)/ 502(불량 게이트웨이)/ 503(서비스를 이용할 수 없음)
2. REST와 라우팅 사용하기
REST: REpresentational State Transfer
- 서버의 자원을 정의하고 자원에 대한 주소를 지정하는 방법
- 자원: 꼭 파일일 필요 X 서버가 행할 수 있는 것 의미
- 일종의 약속
HTTP 요청 메서드
- GET: 서버 자원을 가져오고자 할 때 사용
- 요청의 본문(body): 데이터 넣지 X
- 데이터를 서버로 보내야 한다면 쿼리스트링 사용
- 브라우저에서 캐싱(기억) O -> 서버에서 가져오는 것이 아니라 캐시에서 가져올 수 있음
=> 성능 좋아짐
- POST: 서버에 자원을 새로 등록하고자 할 때 사용
- 요청의 본문: 새로 등록할 데이터
- PUT: 서버의 자원을 요청에 들어 있는 자원으로 치환하고자 할 때 사용
- 요청의 본문: 치환할 데이터
- PATCH: 서버 자원의 일부만 수정하고자 할 때 사용
- 요청의 본문: 일부 수정할 데이터
- DELETE: 서버의 자원을 삭제하고자 할 때 사용
- 요청의 본문: 데이터 넣지 X
- OPTIONS: 요청을 하기 전에 통신 옵션을 설명하기 위해 사용
=> 주소 하나가 요청 메서드 여러 개 가질 수 있음
- GET 메서드의 /user: 사용자 정보를 가져오는 요청
- POST 메서드의 /user: 새로운 사용자 등록 (ex. 로그인)
클라이언트가 누구든 상관 X 같은 방식으로 서버와 소통 O
= iOS, 안드로이드, 웹, 다른 서버 모두 같은 주소로 요청보낼 수 O
=> 서버와 클라이언트 분리 -> 서버 확장 시 클라이언트에 구애되지 X
REST를 사용한 주소 체계로 RESTful한 웹 서버
REST를 따르는 서버 = RESTful 하다
HTTP 메서드 | 주소 | 역할 |
GET | / | restFront.html 파일 제공 |
GET | /about | about.html 파일 제공 |
GET | /users | 사용자 목록 제공 |
GET | 기타 | 기타 정적 파일 제공 |
POST | /user | 사용자 등록 |
PUT | /user/사용자 id | 해당 id의 사용자 수정 |
DELETE | /user/사용자 id | 해당 id의 사용자 제거 |
nodejs-book/ch4/4.2 at master · ZeroCho/nodejs-book
Node.js교과서 소스 코드. Contribute to ZeroCho/nodejs-book development by creating an account on GitHub.
github.com
// restServer.js
const http = require('http');
const fs = require('fs').promises;
const path = require('path');
const users = {}; // 데이터 저장용
http.createServer(async (req, res) => {
try {
if (req.method === 'GET') {
if (req.url === '/') {
const data = await fs.readFile(path.join(__dirname, 'restFront.html'));
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
return res.end(data);
} else if (req.url === '/about') {
const data = await fs.readFile(path.join(__dirname, 'about.html'));
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
return res.end(data);
} else if (req.url === '/users') {
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
return res.end(JSON.stringify(users));
}
// /도 /about도 /users도 아니면
try {
const data = await fs.readFile(path.join(__dirname, req.url));
return res.end(data);
} catch (err) {
// 주소에 해당하는 라우트를 못 찾았다는 404 Not Found error 발생
}
} else if (req.method === 'POST') {
if (req.url === '/user') {
let body = '';
// 요청의 body를 stream 형식으로 받음
req.on('data', (data) => {
body += data;
});
// 요청의 body를 다 받은 후 실행됨
return req.on('end', () => {
console.log('POST 본문(Body):', body);
const { name } = JSON.parse(body);
const id = Date.now();
users[id] = name;
res.writeHead(201, { 'Content-Type': 'text/plain; charset=utf-8' });
res.end('등록 성공');
});
}
} else if (req.method === 'PUT') {
if (req.url.startsWith('/user/')) {
const key = req.url.split('/')[2];
let body = '';
req.on('data', (data) => {
body += data;
});
return req.on('end', () => {
console.log('PUT 본문(Body):', body);
users[key] = JSON.parse(body).name;
res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
return res.end(JSON.stringify(users));
});
}
} else if (req.method === 'DELETE') {
if (req.url.startsWith('/user/')) {
const key = req.url.split('/')[2];
delete users[key];
res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
return res.end(JSON.stringify(users));
}
}
res.writeHead(404);
return res.end('NOT FOUND');
} catch (err) {
console.error(err);
res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' });
res.end(err.message);
}
})
.listen(8082, () => {
console.log('8082번 포트에서 서버 대기 중입니다');
});
`res.end` 앞에 `return` 붙이는 이유
입문자들은 `res.end` 호출 -> 함수가 종료된다고 착각
But, 노드도 자바스크립트 문법 따르므로 `return` 붙이지 않는 한 함수가 종료되지 X
=> `return`으로 명시적으로 함수 종료
`return` 붙이지 않아 `res.end` 메서드 여러 번 실행 -> `Error: Can't render headers after they are sent to the client`
받은 데이터 문자열 -> JSON으로 만드는 `JSON.parse` 과정 필요
Name: 요청 주소/ Method: 요청 메서드/ Status: HTTP 응답 코드/ Protocol: 통신 프로토콜/ Type: 요청의 종류
3. 쿠키와 세션 이해하기
클라이언트에서 보내는 요청 누가 보내는지 모름
요청을 보내는 IP 주소나 브라우저 정보 받아올 수 있음
But, 여러 컴퓨터가 공통으로 IP 주소 갖거나 한 컴퓨터 여러 사람이 사용할 수 있음
-> 로그인 구현 => 쿠키와 세션 사용
ex. 로그인 후 새로 고침(새로운 요청) -> 로그아웃 X => 클라이언트) 사용자 정보 지속적으로 알려줌 -> 서버
쿠키: 유효 시간 O name=zerocho와 같이 '키-값' 쌍
서버로부터 쿠키 -> 웹 브라우저) 쿠키를 저장해뒀다가 다음에 요청할 때마다 쿠키 동봉해서 보냄
서버) 요청에 들어 있는 쿠키 읽어서 사용자가 누구인지 파악
요청의 헤더(Cookie)에 담겨서 전송, 브라우저) 응답의 헤더(Set-Cookie)에 따라 쿠키 저장
서버에서 쿠키 만들어 요청자의 브라우저에 넣기
// cookie.js
const http = require('http');
http.createServer((req, res) => {
console.log(req.url, req.headers.cookie);
res.writeHead(200, { 'Set-Cookie': 'mycookie=test' });
res.end('Hello Cookie');
})
.listen(8083, () => {
console.log('8083번 포트에서 서버 대기 중');
});
$ node cookie
8083번 포트에서 서버 대기 중
쿠키는 name=zerocho;year=1994처럼 문자열 형식으로 존재, 쿠키 간에 세미콜론으로 구분
- `createServer` 메서드의 콜백: req 객체에 담겨 있는 쿠키 가져옴
- 쿠키는 `req.headers.cookie`에 들어 있음
req.headers: 요청의 헤더
- 쿠키는 `req.headers.cookie`에 들어 있음
- 응답의 헤더에 쿠키를 기록 -> `res.writeHead` 메서드 사용
- `Set-Cookie`: 브라우저에 다음과 같은 값의 쿠키를 저장
- 실제로 응답을 받은 브라우저) `mycookie=test`라는 쿠키 저장
/ undefined
/favicon.ico { mycookie: 'test' }
- 첫 번째 요청('/'): 쿠키에 대한 정보 없음
- 두 번째 요청('/favicon.ico'): `{ mycookie: 'test' }`가 기록
- favicon(파비콘): 웹 사이트 탭에 보이는 이미지
- 브라우저) 파비콘이 뭔지 HTML에서 유추 X -> 서버에 파비콘 정보에 대한 요청 보냄
사용자를 식별하는 방법
// cookie2.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>쿠키 & 세션</title>
</head>
<body>
<form action="/login">
<input id="name" name="name" placeholder="이름 입력" />
<button id="login">로그인</button>
</form>
</body>
</html>
// cookie2.js
const http = require('http');
const fs = require('fs').promise;
const patch = require('patch');
const parseCookies = (cookie = '') =>
cookie
.split(';')
.map(v => v.split('='))
.reduce((acc, [k, v]) => {
acc[k.trim()] = decodeURIComponent(v);
return acc;
}, {});
http.createServer(async (req, res) => {
const cookies = parseCookies(req.headers.cookie);
// 주소가 /login으로 시작
if (req.url.startsWith('/login')) {
const url = new URL(req.url, 'http://localhost:8084');
const name = url.searchParmas.get('name');
const expires = new Date();
// 쿠키 유효 시간: 현재 시간 + 5분
expires.setMinutes(expires.getMinutes() + 5);
res.writeHead(302, {
Location: '/',
'Set-Cookie': `name=${encodeURIComponent(name)}; Expires=${expires.toGMTString()}; HttpOnly; Path=/`,
});
res.end();
// 주소가 /이면서 name이라는 쿠키가 있는 경우
} else if (cookies.name) {
res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
res.end(`${cookies.name}님 안녕하세요`);
} else { // 주소가 /이면서 name이라는 쿠키가 없는 경우
try {
const data = await fs.readFile(path.join(__dirname, 'cookie2.html'));
res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
res.end(data);
} catch (err) {
res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' });
res.end(err.message);
}
}
})
.listen(8084, () => {
console.log('8084번 포트에서 서버 대기 중');
});
- `http://localhost:8084` 접속 -> `/`로 요청
- `cookie2.html`에서 `form` ~> 로그인 요청 -> `/login`
=> 실제로 제일 먼저 실행되는 곳 `else`: 주소가 /이면서 `name`이라는 쿠키가 없는 경우
`parseCookies` 함수
- 쿠키 문자열을 쉽게 사용하기 위해 자바스크립트 객체 형식으로 바꾸는 함수
- 문자열을 객체로 바꿔줌
- 쿠키: `mycookie=test` 같은 문자열 -> `{ mycookie: 'test' }` 객체
로그인 요청(GET `/login`) 처리
- `form`은 GET 요청인 경우: 데이터 -> URL 객체로 쿼리스트링 부분 분석
- 쿠키의 만료 시간: 현재 시간 + 5분
- 302 응답 코드를 보고 페이지를 해당 주소로 리다이렉트
=> 302 응답 코드 + 리다이렉트 주소 + 쿠키 -> 헤더
- 헤더: 한글 설정 X -> `name` 변수를 `encodeURIComponent` 메서드로 인코딩
- `Set-Cookie` 값: 제한된 ASCII 코드만 가능 -> 줄바꿈 X
그 외의 경우(/로 접속했을 때 등) 쿠키가 있는지 없는지 확인
- 쿠키 X -> 로그인할 수 있는 페이지를 보냄
- 처음 방문한 경우 쿠키 X -> cookie2.html 전송
- 쿠키 O -> 로그인 상태로 간주 -> 인사말 보냄
`Set-Cookie`로 쿠키 설정 -> 옵션(만료 시간(Expires), HttpOnly, Path) 부여
각종 옵션 넣을 수 O, 옵션 사이에 세미콜론(;)으로 구분
쿠키에는 한글과 줄바꿈 들어갈 수 X/ 한글 -> encodeURIComponent로 감싸서 넣음
- 쿠키명=쿠키값: 기본적인 쿠키값
- `mycookie=test` or `name=zerocho`와 같이 설정
- Expires=날짜: 만료 기한/ 기한 지나면 쿠키가 제거 (기본값: 클라이언트가 종료될 때까지)
- Max-age=초: Expries와 비슷하지만 날짜 대신 초를 입력할 수 있음
- 해당 초가 지나면 쿠키 제거/ Expires보다 우선
- Domain=도메인명: 쿠키가 전송될 도메인을 특정할 수 있음 (기본값: 현재 도메인)
- Path=URL: 쿠키가 전송될 URL을 특정할 수 있음 (기본값: '/' 모든 URL에서 쿠키 전송할 수 있음)
- Secure: HTTPS일 경우에만 쿠키가 전송됨
- HttpOnly: 설정 시 자바스크립트에서 쿠키에 접근할 수 없음
- 쿠키 조작을 방지하기 위해 설정하는 것이 좋음
$ node cookie 2
8084번 포트에서 서버 대기 중
=> 새로 고침을 해도 로그인이 유지
Application 탭에서 보이는 것처럼 쿠키 노출 -> 쿠키가 조작될 위험 O
=> 민감한 개인정보를 쿠키에 넣어두는 것은 적절 X
서버가 사용자 관리
// session.js
const http = require('http');
const fs = require('fs').promies;
const path = require('path');
const parseCookies = (cookie = '') =>
cookie
.split(';')
.map(v => v.split('='))
.reduce((acc, [k, v]) => {
acc[k.trim()] = decodeURIComponent(v);
return acc;
}, {});
const session = {};
http.createServer(async (req, res) => {
const cookies = parseCookies(req.headers.cookie);
if (req.url.startsWith('/login')) {
const url = new URL(req.url, 'http://localhost:8085');
const name = url.searchParams.get('name');
const expires = new Date();
expires.setMinutes(expires.getMinutes() + 5);
const uniqueInt = Date.now();
session[uniqueInt] = {
name,
expires,
};
res.writeHead(302, {
Location: '/',
'Set-Cookie': `session=${uniqueInt}; Expires=${expires.toGMTString()}; HttpOnly; Path=/`,
});
res.end();
// 세션에 쿠키가 존재, 만료 기간 지나지 X
} else if (cookies.session && session[cookies.session].expires > new Date()) {
res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
res.end(`${session[cookies.session].name}님 안녕하세요`);
} else {
try {
const data = await fs.readFile(path.join(__dirname, 'cookie2.html'));
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(data);
} catch (err) {
res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(err.message);
}
}
})
.listen(8085, () => {
console.log('8085번 포트에서 서버 대기 중');
});
=> 쿠키에 이름을 담아서 보내는 대신, `uniqueInt`라는 숫자 값 보냄
사용자의 이름과 만료 시간 -> `uniqueInt` 속성명 아래 있는 `session`이라는 객체에 대신 저장
$ node session
8085번 포트에서 서버 대기 중
서버에 사용자 정보를 저장하고 클라리언트와는 세션 아이디로만 소통
새션 아이디는 꼭 쿠키를 사용해서 주고받지 않아도 됨
실제 배포용 서버: 세션을 변수에 저장 X
-> 서버 멈추거나 재시작되면 메모리에 저장된 변수 초기화/ 서버 메모리 부족 -> 세션 저장 X
=> 레디스(Redis) or 멤캐시드(Memcahced)같은 데이터베이스에 넣어둠
4. http와 https
`https` 모듈: 웹 서버 + `SSL` 암호화
GET이나 POST 요청을 할 때 오가는 데이터를 암호화 -> 중간에 다른 사람이 요청을 가로채도 내용 확인할 수 없게 함
`http` 서버 코드
const http = require('http');
http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.write('<h1>Hello Node</h1>');
res.end('<p>Hello Server</p>');
})
.listen(8080, () => { // 서버 연결
console.log('8080번 포트에서 서버 대기 중');
});
서버에 암호화 적용 -> `https` 모듈 사용해야 함 -> 인증해줄 수 있는 기관 필요
인증서는 인증 기관에서 구입해야 하는데, Let's Encrypt에서 무료로 발급해줌
Let's Encrypt
Jul 23, 2024 Intent to End OCSP Service Moving to a more privacy-respecting and efficient method of checking certificate revocation. Read more Jun 24, 2024 NTP is critical to how TLS works, and now it’s memory safe at Let’s Encrypt. Read more May 30, 2
letsencrypt.org
`https` 서버 코드
const https = require('https');
const fs = require('fs');
https.createServer({
cert: fs.readFileSync('도메인 인증서 경로'),
key: fs.readFileSync('도메인 비밀 키 경로'),
ca: [
fs.readFileSync('상위 인증서 경로'),
fs.readFileSync('상위 인증서 경로'),
],
}, (req, res) => {
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.write('<h1>Hello Node</h1>');
res.end('<p>Hello Server</p>');
})
.listen(443, () => {
console.log('443번 포트에서 서버 대기 중');
});
=> `createServer` 메서드가 인수 2개 받음
- 첫 번째 인수: 인증서에 관련된 옵션 객체
- 두 번째 인수: `http` 모듈과 같이 서버 로직
인증서 구입 -> `pem`, `crt`, `key` 확장자 가진 파일 제공 -> `fs.readFileSync` 메서드로 읽어서 옵션에 넣으면 됨
실제 서버에서는 80번포트 대신 443번 포트 사용
`http2` 모듈: `SSL` 암호화 + 최신 HTTP 프로토콜인 http/2
요청 및 응답 방식이 기존 http/1.1보다 개선 -> 효율적 요청 보냄 + 속도 개선
`https2` 서버 코드
const http2 = require('http2');
const fs = require('fs');
http2.createSecureServer({
cert: fs.readFileSync('도메인 인증서 경로'),
key: fs.readFileSync('도메인 비밀 키 경로'),
ca: [
fs.readFileSync('상위 인증서 경로'),
fs.readFileSync('상위 인증서 경로'),
],
}, (req, res) => {
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.write('<h1>Hello Node</h1>');
res.end('<p>Hello Server</p>');
})
.listen(443, () => {
console.log('443번 포트에서 서버 대기 중');
});
=> `https` 모듈과 거의 유사
- `https` -> `http2`
- `createServer` 메서드 -> `createSecure Server` 메서드
5. cluster
- 싱글 프로세스로 동작하는 노드가 CPU 코어를 모두 사용할 수 있게 해주는 모듈
- 포트를 공유하는 프로세스 여러 개 둘 수 있음
-> 요청 ↑ 들어왔을 때 병렬로 실행된 서버의 개수만큼 요청이 분산되게 할 수 있음 => 서버에 무리가 덜 감
ex. 코어 8개인 서버 -> 노드) 코어 1개만 활용
- -> (해결) `cluster` 모듈 설정 -> 코어 하나당 노드 프로세스 하나가 돌아가게 할 수 있음
- (+) 코어 하나만 사용할 때에 비해 성능 개선(성능 8배 X)
- (-) 메모리 공유 X => 세션을 메모리에 저장하는 경우 문제
-> (해결) 레디스 등의 서버 도입
server1.js 클러스터링
const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').cpus().length;
if (cluster.isMaster) {
console.log(`마스터 프로세스 아이디: ${process.pid}`);
// CPU 개수만큼 워커 생산
for (let i = 0; i < numCPUs; i += 1) {
cluster.fork();
}
// 워커가 종료되었을 때ㅔ
cluster.on('exit', (worker, code, signal) => {
console.log(`${worker.process.pid}번 워커가 종료되었습니다.`);
console.log('code', code, 'signal', signal);
cluster.fork(); // 워커 프로세스가 종료되었을 때 새로 하나 생성
});
} else {
// 워커들이 포트에서 대기
http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.write('<h1>Hello Node</h1>');
res.end('<p>Hello Cluster</p>');
setTimeout(() => { // 워커가 존재하는지 확인하기 위해 1초마다 강제 종료
process.exit(1);
}, 1000);
}).listen(8086);
console.log(`${process.pid}번 워커 실행`);
}
클러스터
- 마스터 프로세스
- CPU 개수만큼 워커 프로세스 만들고, 8086번 포트에서 대기
- 요청 들어오면 만들어진 워커 프로세스에 요청을 분배
- 워커 프로세스: 실질적인 일을 하는 프로세스
$ node cluster
마스터 프로세스 아이디: 21360
7368번 워커 실행
11040번 워커 실행
9004번 워커 실행
16452번 워커 실행
17272번 워커 실행
16136번 워커 실행
=> 코어 개수에 맞게 워커 실행
`http://localhost:8086`에 접속 -> 1초 후 워커 종료 메시지 => 6번 새로 고침하면 모든 워커 종료되어 서버 응답 X
16136번 워커가 종료되었습니다.
code 1 signal null
17272번 워커가 종료되었습니다.
code 1 signal null
16452번 워커가 종료되었습니다.
code 1 signal null
9004번 워커가 종료되었습니다.
code 1 signal null
11040번 워커가 종료되었습니다.
code 1 signal null
7368번 워커가 종료되었습니다.
code 1 signal null
- 코드(code): `process.exit`의 인수로 넣어준 코드가 출력
- 신호(signal): 존재하는 경우 프로세스를 존재한 신호의 이름이 출력
28592번 워커가 종료되었습니다.
code 1 signal null
10520번 워커 실행
10520번 워커가 종료되었습니다.
code 1 signal null
23248번 워커 실행
=> 워커 하나가 종료될 때마다 새로운 워커 하나가 생성
But, 이런 방식으로 오류 처리 좋지 X 오류 자체 원인을 찾아 해결해야 함
그래도 예기치 못한 에러로 인해 서버가 종료되는 현상 방지 O -> 클러스터링 적용
직접 `cluster` 모듈로 클러스터링 구현할 수 있지만 실무에서는 `pm2` 등의 모듈로 `cluster` 기능 사용
출처
[개정3판] Node.js 교과서 - 기본부터 프로젝트 실습까지 강의 | 제로초(조현영) - 인프런
제로초(조현영) | 노드가 무엇인지부터, 자바스크립트 최신 문법, 노드의 API, npm, 모듈 시스템, 데이터베이스, 테스팅 등을 배우고 5가지 실전 예제로 프로젝트를 만들어 나갑니다. 클라우드에 서
www.inflearn.com
'BE > NodeJS' 카테고리의 다른 글
[NodeJS] Express / Passport / Middleware / Sequelize (0) | 2025.02.23 |
---|---|
[Node.js 교과서 - 개정 3판] 1장 노드 시작하기 (0) | 2024.08.16 |