과목명 : 웹 서버 프로그래밍(Web Server-side programming with Node.js)
수업일자 : 2023년 05월 04일 (목)
1. 예외 처리(Exception handling)
1-1. 오류와 예외는 비슷해 보이는데 어떤 차이점이 있을까?
(1) 오류(Error)는 개발자가 잘못 작성한 구문으로 인한 문법 오류(Syntax error)와 문법상 문제가 없더라도 프로그램이 수행되는 과정에서 예상하지 못한 오류가 발생하여 실행 중인 *프로세스가 중지되는 것을 말합니다.
* 프로세스(Process) : 프로그램이 실제 메모리로 로드되어 실행되고 있는 프로그램의 독립적인 인스턴스
(2) 이러한 오류들은 운영체제 레벨에서 치명적인 범주로 다루고 있고 개발자가 미리 예측하여 이러한 오류의 처리를 진행할 수는 없습니다.
(3) 그러나, 예외(Exception)의 경우 불러와야 하는 데이터가 없거나, 메모리가 부족한 상황 여러 가지 예외가 발생할 수 있고 이로 인해 실행 중인 프로세스가 비정상 종료되지만 발생할 수 있는 예외 상황을 미리 예측하여 처리할 수 있습니다. 따라서 개발자는 미리 발생할 수 있는 예외 상황에 대한 별도의 처리를 통해 코드의 흐름이 유지될 수 있도록 해 주어야 합니다.
1-2. 프로그램(프로세스)의 오류 3가지
- 간단하게 정리해 보겠습니다.
(1) 문법(구문) 오류(Syntax error) 또는 컴파일 타임 에러(Compile-time error)
- 개발자가 잘못 작성한 구문으로 인해 발생하는 오류, 잘못 작성한 구문으로 컴파일러가 이를 잡아내어 컴파일 오류를 발생시킵니다.
(2) 논리적인 오류(Logical error)
- 개발자가 의도한 작업을 프로그램 내부에서 수행하지 못하는 오류, 코드 자체의 문법상 오류는 없어서 컴파일 오류 없이 실행될 수 있지만 개발자가 의도한 결과와 프로그램이 수행한 연산 결과가 다른 오류를 의미합니다. 별도의 오류 메시지가 나오지 않기 때문에 논리적 오류를 교정하는 작업이 어려울 수 있으며 디버깅 과정이 필요합니다.
(3) 런타임 오류(Runtime error)
- 말 그대로 프로그램 실행 도중 발생한 오류, 일반적으로 프로그램에서 수행할 수 없는 작업을 시도할 때 발생합니다. 개발자의 소프트웨어적인 설계 미숙으로 발생하는 오류가 대부분입니다.
- 0으로 나누는 경우, 무한 루프(Infinite loop)를 도는 경우, Null point Error 등
1-3. 예외 처리(Exception handling)
(1) 예외 처리(Exception handling)는 프로그램이 비정상 종료될 수 있는 예외를 미리 예측하고 이에 대한 처리 방법을 코드상으로 미리 작성하여 실행 중인 프로그램이 예외로 인해 실행이 중지되는 상황을 방지하는 것을 의미합니다.
(2) Node의 경우 싱글 스레드 기반으로 동작하고 있고, 프로세스에 대한 예외가 발생한다면 이를 담당하는 노드의 싱글 스레드 자체가 멈춰버리기 때문에 프로세스 및 서비스 자체가 중지됩니다.
(3) 이처럼 노드 기반의 서비스들은 예외가 발생하면 서비스 운영에 심각한 지장을 주기 때문에 예외 처리는 필수적이라고 볼 수 있습니다.
2. 예외 처리(Exception handling) : try-catch문
2-1. JavaScript에서는 try-catch 구문을 통해 예외를 처리한다.
(1) 예외가 발생할 수 있다고 예측할 수 있는 구문을 try문으로 넘겨주고 이를 처리할 수 있는 구문을 catch문으로 넘깁니다.
(2) 일반적인 try-catch문의 구조
- 매개변수 e의 경우 예외에 대한 정보를 담고 있는 객체입니다.
try {
// 예외가 발생할 수 있는 구문들
} catch(e) {
// try 구문에서 발생한 예외에 대한 처리 진행
// 해당 블록에서 예외를 처리할 수도 있고 아무것도 하지 않고 예외를 무시할 수도 있음
}
(3) 예제 코드 : error0.js
setInterval(() => {
console.log('start');
try {
throw new Error('server stop!!');
} catch(error) {
console.log(error);
}
}, 1000)
<실행 결과>
3. 예외 처리(Exception handling) : try-catch-finally문
3-1. JavaScript에서는 try-catch-finally 구문을 통해서도 예외를 처리할 수 있다.
(1) finally 구문이 추가된 경우 try 구문 내부에서의 예외 발생과 관계 없이 무조건 해당 구문의 내용이 실행됩니다.
(2) return, break 등의 키워드를 만나더라도 무조건 실행되며 일반적으로 catch 구문과 함께 사용됩니다.
(3) finally 구문은 선택적인 옵션으로 예외 처리에서 필수적인 요소는 아니며, 특정한 상황을 설계해야 할 때 자주 사용합니다.
- Ex) 특정 예외를 처리한 후 해당 예외 처리에 대한 반드시 로그 내역 삭제를 진행하는 경우 try-catch문으로 예외 처리 이후에 finally 구문 내부에 해당 로그 삭제 로직을 추가할 수 있습니다.
try {
// 예외가 발생할 수 있는 구문들
} catch(e) {
// try 구문에서 발생한 예외에 대한 처리 진행
// 해당 블록에서 예외를 처리할 수도 있고 아무것도 하지 않고 예외를 무시할 수도 있음
} finally {
// 예외 상황과 관계 없이 무조건 실행됨
}
4. Node의 비동기 메소드(Asynchronous method)의 예외 처리
4-1. Node의 비동기 메소드 예외 처리
(1) Node의 비동기 메소드는 별도의 try-catch 구문을 통한 예외 처리를 하지 않을 수 있습니다.
(2) 콜백 함수(Callback function)에서 별도의 예외에 대한 객체를 제공합니다.
(3) 예제 코드 : error2.js
const fs = require('fs');
setInterval(() => {
fs.unlink('./abcdefg.js', (err) => {
if (err) {
console.error(err);
}
});
}, 1000);
<실행 결과>
4-2. 예측할 수 없는 예외 처리 - uncaughtException 객체 사용
(1) 예측할 수 없는 예외 상황이 존재한다면 process 모듈의 uncaughtException 객체를 사용할 수 있습니다.
(2) 해당 객체를 사용하면 콜백 함수의 실행이 보장될 수 없기 때문에 사용이 권장되지 않습니다.
(3) 사용한다면 별도의 로그 기록의 용도로 쓸 수 있고, 일반적인 상황에선 모든 예외를 예측하여 처리합니다.
(4) 예제 코드 : error4.js (1)
process.on('uncaughtException', (err) => {
console.error('예측하지 못한 Exception', err);
});
setInterval(() => {
throw new Error('Server stop!');
}, 1000);
setTimeout(() => {
console.log('Execution');
}, 2000);
<실행 결과>
(4) 예제 코드 : error4.js (2) - uncaughtException 객체를 사용하지 않는다면?
// process.on('uncaughtException', (err) => {
// console.error('예측하지 못한 Exception', err);
// });
setInterval(() => {
throw new Error('Server stop!');
}, 1000);
setTimeout(() => {
console.log('Execution');
}, 2000);
<실행 결과>
- uncaughtException 객체를 사용하지 않아 예외 처리가 진행되지 않았습니다. 동작 과정에서 예외가 발생함에 따라 싱글 스레드로 작업을 수행하는 스레드가 죽게 되면서 setTimeout 함수의 내용이 실행되지 않았습니다.
5. 요청(Request)과 응답(Response) : 서버와 클라이언트(Server & Client)
5-1. 서버와 클라이언트
(1) 클라이언트에서 요청을 보내면 서버는 요청을 처리하고 클라이언트에게 적절한 응답 결과를 보낼 수 있습니다.
5-2. Node로 HTTP 서버 생성 - HTTP 요청에 응답하는 Node 서버 (createServer() 함수 사용)
(1) 익스프레스 없이 서버를 생성하기 위해 http 모듈의 createServer() 함수를 사용하며 사용 시, 요청 이벤트에 대기합니다.
(2) createServer() 함수의 전달인자로 req, res 객체를 전달하는데 req 객체는 클라이언트의 요청(Request)에 대한 정보를, res 객체는 서버의 응답(Response)에 대한 정보를 담고 있습니다.
(3) 예제 코드 : createServer.js
const http = require('http');
http.createServer((req, res) => {
// 해당 영역에 어떻게 응답할지 작성합니다.
});
5-3. 8080번 Port를 사용해 서버 열어주기 : res 객체의 write(), end() 메소드
(1) res 객체의 write() 메소드로 응답 내용을, end() 메소드로 응답을 마무리합니다.
(2) 일반적으로 write() 메소드는 클라이언트로 내용을 전달할 때 사용하고, end() 메소드는 서버-클라이언트 간 연결된 connection을 해제할 때 사용하기 때문에 write() 메소드에 전달할 내용을 적고 end() 메소드엔 내용을 적지 않는 것이 대부분입니다.
(3) listen() 메소드로 접속하고자 하는 서버에 대한 포트 번호를 명시합니다.
- localhost:8080으로 접속했을 때 서버(로컬)의 응답 결과를 확인할 수 있도록 코드를 작성합니다.
(3) 예제 코드 : server1.js
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번 포트에서 서버 대기 중입니다!');
});
<실행 결과>
- localhost:8080 또는 http://127.0.0.1:8080으로 접속하면 응답 결과를 확인할 수 있습니다.
5-4. localhost와 포트 번호(Port Number)
(1) localhost
- 쉽게 말하면 개발 작업이 이루어지고 있는 자신의 컴퓨터(로컬 환경)를 의미합니다. 컴퓨터 네트워크에서 사용하는 루프백 호스트명으로 IP 주소로 변환 시 127.0.0.1입니다. 이러한 로컬 환경에 대해 외부에서는 따로 접근할 수 없습니다.
(2) 포트(Port)
- 운영체제 통신의 종단점으로, 컴퓨터끼리 상호 간 데이터를 주고받을 때는 포트라는 추상적인 통로를 이용합니다.
- 서버와 클라이언트의 입장에서 보면, 서버의 IP나 URL을 입력하고 ":(Colon)"을 이용해 포트 번호를 명시함으로써(localhost:8080 또는 http://127.0.0.1:8080), 클라이언트가 서버로 접속하여 8080번 포트를 통해 서로 통신을 하겠다는 의미입니다.
- 또한 포트는 위에서 운영체제 통신의 종단점이라고 설명을 했습니다. 클라이언트의 요청을 받는 서버도 일종의 컴퓨터이기 때문에 웹 서비스를 운영하기 위한 서버 프로세스 외에도 다양한 프로세스가 실행 중일 수 있습니다. 이에 따라 프로세스별로 각각의 형식에 맞게 클라이언트와 서버가 데이터를 주고받아야 하기 때문에 실행되고 있는 프로세스를 구분지어야 하며 이를 위해 포트라는 개념이 사용됩니다.
(3) 기본적으로 HTTP 서버는 80번 포트를 사용합니다. (HTTPS의 경우 443)
5-5. HTML 파일을 읽어서 서버의 응답 데이터로 보여주기
(1) fs 모듈을 통해 HTML 문서를 읽어들인 뒤 이를 서버의 응답 데이터로 사용할 수 있습니다. 위의 server1.js 코드처럼 res.write() 메소드에 응답 데이터로 HTML 태그를 직접 적는 것은 매우 비효율적입니다.
(2) write() 메소드가 버퍼도 따로 전송 가능합니다.
(3) 예제 코드 : server2.html, server2.js
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Node.js WS</title>
</head>
<body>
<h1>Node.js Web Server</h1>
<p>Create Node.js Web Server</p>
</body>
</html>
(1) fs.readFile() 메소드 사용
const http = require('http');
const fs = require('fs');
http.createServer(async (req, res) => {
fs.readFile('./server2.html', (err, data) => {
if (err) throw err;
return res.end(data);
});
}).listen(8081, () => {
console.log('8081번 포트에서 서버 대기 중입니다.');
});
(2) fs.readFileSync() 메소드 사용
const http = require('http');
const fs = require('fs');
http.createServer(async (req, res) => {
try {
const data = fs.readFileSync('./server2.html', (err, data) => {
if (err) throw err;
return res.end(data);
});
res.writeHead(200, {'Content-Type' : 'text/html; 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번 포트에서 서버 대기 중입니다.');
});
<실행 결과>
- server1.js를 종료했다면 동일하게 8080번 포트를 사용할 수 있으며 사용 중이라면 8081번 포트를 사용해서 서버(로컬 호스트)로 접속합니다.
6. Reference
Node.js 교과서(Node.js Textbook) - 저자 : 조현영
https://www.zerocho.com/book/1
- 학부에서 수강했던 전공 수업 내용을 정리하는 포스팅입니다.
- 내용 중에서 오타 또는 잘못된 내용이 있을 시 지적해 주시기 바랍니다.
댓글