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의 사용자 제거 |
// 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에서 무료로 발급해줌
`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` 기능 사용
출처
'한국경제신문 with toss bank > BE' 카테고리의 다른 글
[Node.js 교과서 - 개정 3판] 1장 노드 시작하기 (0) | 2024.08.16 |
---|