비동기 프로그래밍

일반적으로 파일 I/O나 네트워크 I/O는 메모리 I/O보다 현저히 느리다. 예를 들어 파일 4개의 입출력을 하나씩 처리한다면, 처리 시간은 길어지고 CPU의 사용 효율성은 떨어진다. 이를 개선하고자 비동기 프로그래밍을 사용하는데, 순차적으로 I/O를 처리하는 것이 아니라 I/O 처리 요청만 운영체제에 전달하고 CPU는 다른 연산을 수행한다. I/O 처리가 완료되면 운영체제에서 I/O 처리를 완료했다는 메시지를 전달받아 이후 작업을 처리하는 방식이다.

동기와 비동기의 차이

동기와 비동기의 차이

function func(callback) {    // ➊ func 함수 선언
    callback("callback!!");  // ➋ 인자 값으로 전달된 callback 함수 호출
}

func((param) => {            // ➌ 익명 함수를 인자로 func 함수 호출
    console.log(param);
});

func 함수를 선언했고 인자로 콜백 함수를 받았음(➊). 전달받은 콜백 함수에 “callback!!“이라는 인자 값을 전달하도록 구현(➋). 익명 함수를 인자로 func 함수를 호출(➌). 실행하면 “callback!!”을 화면에 출력.

function func(callback) {
    // ➊ nextTick을 사용해 인자 값으로 전달된 callback 함수 호출
    process.nextTick(callback, “callback!!”);
}
try {                             // ➋ 예외 처리를 위해 try~catch 문 선언
    func((param) => {
        a.a = 0;                  // ➌ 의도적으로 예외 발생
    });
} catch (e) {
    console.log(“exception!!”);    // ➍ 같은 스레드일 경우 호출
}

의도적으로 콜백 이후에 예외가 발생하도록 선언되지 않은 변수에 접근하게 함(➌). func 함수에서는 process.nextTick 함수를 이용해 비동기로 동작하도록 코드를 수정(➊). try~catch 문을 적용했으니 “exception!!”이라는 문자를 화면에 표시해야 함(➋, ➍). 하지만 try~catch 문이 실행되지 않고 프로세스 실행 에러가 발생. process.nextTick 함수는 비동기 처리를 위해 Node.js 내부의 스레드 풀로 다른 스레드 위에서 콜백 함수를 동작. try~catch 문은 같은 스레드 위에서만 동작하기 때문에 서로 다른 스레드 간의 예외 처리가 불가능. 이처럼 process.nextTick 함수를 이용하면 Node.js가 CPU를 효율적으로 사용하는 대신 try~catch 문만으로는 예외 처리가 불가능.

싱글 스레드 프로그래밍

CPU는 한 번에 하나의 명령만 수행 가능. 그래서 CPU의 클록 수에 따라 처리 속도가 결정. 이러한 한계를 극복하려고 스레드 개념을 도입. CPU는 한 번에 하나의 명령만 수행할 수 있지만, 운영체제의 스케줄러가 매우 짧은 주기로 각기 다른 명령을 우선순위에 따라 실행시키면 동시에 여러 로직도 수행 가능. 이를 멀티스레드 프로그래밍이라고 함.

스레드 2개를 실행하고 있는 프로세스

스레드 2개를 실행하고 있는 프로세스

멀티스레드 프로그래밍은 대용량 처리에서 필수적으로 사용하는 프로그래밍 방식이나, 오류를 찾아내기 어렵고 구현할 때 고려할 사항이 많았다. Node.js는 이러한 복잡한 멀티스레드 대신 싱글 스레드 프로그래밍만으로도 멀티스레드 프로그래밍 성능을 구현하도록 프레임워크가 구성되어 있다.

Node.js는 싱글 스레드 기반으로 동작. 여기서 주의할 점은 싱글 스레드라고 해서 모두 같은 스레드 위에서 동작하지 않는다. 밑의 코드와 같이 비동기 호출을 할 경우 함수를 호출한 영역과 콜백을 처리하는 영역이 각기 다른 스레드 위에서 동작. 이때 try~catch 문으로 모든 예외 처리를 하기에는 무리가 있음. Node.js는 모든 스레드에서 예외 처리를 할 수 있도록 uncaughtException 이벤트를 제공.

function func(callback) {
    process.nextTick(callback, “callback!!”);
}

try {
    func((param) => {
        a.a = 0;
    });
} catch (e) {
    console.log(“exception!!”);
}

process.on(“uncaughtException”, (error) => { // 모든 스레드에서 발생하는 예외 처리
    console.log(“uncaughtException!!”);
});

try~catch 문으로는 제어할 수 없던 예외 처리를 uncaughtException 이벤트로 제어

Node.js로 서버와 클라이언트 만들기

Node.js는 기본적으로 고성능 네트워크를 손쉽게 처리할 수 있는 네트워크 프레임워크이다. 고성능 I/O 서버를 구현하는 것은 많은 학습과 경험이 필요한 영역이었지만, Node.js는 이러한 복잡한 영역을 효과적으로 은닉화(encapsulation)해 손쉽게 고성능 I/O 시스템을 구현할 수 있게 한다.

네트워크 시스템에서 데이터를 요청하는 쪽을 클라이언트, 응답하는 쪽을 서버라고 한다. 일반적으로 사용자 영역에 가까울수록 클라이언트라고 생각하며, 서비스를 제공하는 시스템 영역에 가까울수록 서버라고 생각한다. 그러나 사용자 영역 안에서도 서버와 클라이언트가 존재할 수 있고, 시스템 영역 안에서도 서버와 클라이언트 역할이 구분될 수 있다.

위치에 따라 구분되는 서버와 클라이언트 역할

위치에 따라 구분되는 서버와 클라이언트 역할

마이크로서비스는 기본적으로 서버이지만 다른 마이크로서비스에 정보를 요청해야 하는 클라이언트가 되기도 한다.