Notice
Recent Posts
Recent Comments
Link
«   2026/04   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30
Tags more
Archives
Today
Total
관리 메뉴

꾸르빅 블로그

[디자인 패턴 바이블] 3장. 콜백과 이벤트 (1) 본문

도서/Node.js

[디자인 패턴 바이블] 3장. 콜백과 이벤트 (1)

꾸르빅 2023. 6. 4. 00:28

들어가기에 앞서 Key Point

 - 콜백 패턴 : 어떻게 동작하며, Node.js에서 어떤 방식으로 자주 사용되는지, 자주 발생하는 위험요소를 어떻게 다룰지

 - 관찰자패턴 : Node.js에서 EventEmitter 클래스를 사용하여 구현을 어떻게 할 것인지

3-1. 콜백 패턴

 - 콜백 : 비동기 작업을 처리할 때 필요한 '작업의 결과를 전달하기 위해 호출되는 함수'로, Javasciprt는 콜백에 이상적인 언어이다.

 - 콜백을 구현하는 방법으로는 함수(일급 클랙스 객체)와 클로저(생성된 함수의 환경을 참조)가 있다.

※ 일급 객체(first-class object)란 다른 객체들에 일반적으로 적용 가능한 연산을 모두 지원하는 객체를 가리킨다. 보통 함수에 인자로 넘기기, 수정하기, 변수에 대입하기와 같은 연산을 지원할 때 일급 객체라고 한다.(출처 : 위키백과)

 

3-1-1. 연속 전달 방식

 - Javascript에서 콜백은 다른 함수에 인자로 전달되는 방식으로 사용된다. 이러한 방식을 연속전달방식(CPS: Continuation-Passsing Style) 이라고 하며, 이는 단순히 결과를 호출자에게 직접 반환하는 대신 다른 함수(콜백)로 전달하는 것을 의미한다. 반대로, return문을 통해 호출자에게 전달되는 방식을 직접스타일(direct style)이라고 한다.

//Dircet style
fucntion add(a,b) {
	return a+b
}

//CPS
fuction addCps(a, b, callback) {
	callback(a + b)
}
function additionAsync(a, b, callback) {
	setTimeout(() => callback(a + b), 100)
}

console.log('before')
additionAsync(1, 2, result => console.log('result: ${result}'))
console.log('after')
/*
결과
before
after
result: 3
/*

 - setTimeout()은 비동기 작업을 실행시키기 때문에 콜백의 실행이 끝날 때까지 기다리지 않는 대신, 즉시 반환되어 additionAsync()로 제어를 돌려주어, 제어가 호출자에게 반환된다. 따라서, 최종 출력은 [ after ] 가 찍힌 후 [ result : 3 ]이 출력된다.

 - 위의 코드에서 Javascript의 클로저 덕분에 콜백이 다른 시점과 다른 위치에서 호출되더라도, 비동기 함수의 호출자 컨텍스트를 유지한다. 따라서, additionAsync() 함수가 종료되었음에도 이벤트 루프의 다음 사이클에서 콜백함수가 수행될 때, 변수값을 유지한 상태로 결과값이 출력된다.

※ 클로저(Closer)란, 내부 함수가 참조하는 외부함수의 지역변수가 외부함수가 리턴된 이후에도 유효성이 유지될 때, 이 내부함수를 클로저라고 한다 ( 출저 : 초보자를 위한 Node.js 200제)

 

- 비 연속 전달(Non-CPS) 콜백 : 콜백 인자가 있는 경우에도 직접적인 방식으로 전달할 수 있다.

(ex. 단순 배열 접근을 위한 map 함수의 경우 요소를 반복하는데 사용될 뿐, 비동기적으로 연산 결과를 전달하지 않기 때문에 결과는 직접적인 방식으로 동기적으로 반환됨)  

Non-CPS 콜백과 CPS 콜백은 문법적 차이가 없기 때문에 API 문서에 분명하게 명시해야 한다.

const result = [1,5,7].map(element => element -1)
console.log(result) // [0, 4, 6]

 

3-1-2. 콜백의 위험요소

- 특정 조건에서 동기적으로 동작하고, 다른 조건에서는 비동기적으로 동작하는 API를 개발하지 말자

import { readFile } from 'fs'

const cache = new Map()

function inconsistentRead(filename, cb) {
	if (cache.has(filename)) {
    	//동기적으로 호출되는 부분에서 콜백 사용
        cb(cache.get(filename))
    } else {
    	//비동기 함수 내에서 콜백 사용
      	readFile(Filename, 'utf8', (err, data) => {
        	cache.set(filename, data)
        	cb(data)
        })
    }
}
function createFileReader(filename) {
	const listeners = []
    inconsistentRead(filename, value => {
    	listeners.forEach(listener => listener(value))
    })
    
    return {
    	onDataReady: listener => listenenrs.push(listener)
    }
}


const reader1 = createFileReader('data.txt')
reader1.onDataReady(data => {
	console.log('First call data : ${data}')
    
    //일정 시간 후 같은 파일 다시 읽기 시도
	const reader2 = createFileReader('data.txt')
    reader2.onDataReady(data => {
	console.log('Second call data : ${data}')
    })
})

/* 결과
First call data : some data
*/

 - 파일을 처음 읽을 때는 readFile의 callback함수로 cb가 전달되기 때문에, 파일을 읽고 listener에 data가 전달되는 시간동안 리스너가 생성 및 등록되어 reader1의 onDataReady callback이 수행된다. 그러나 두번째 읽을 때는 동기적으로 수행되기 때문에 reader2.onDataReady의 callback은 수행되지 않을 수 있다.

 

수정방안 1)  동기 API를 이용하여 함수가 전체적으로 직접 스타일로 구현되게 수정

//동기API 사용
import { readFileSync } from 'fs'

const cache = new Map()

function consistentReadSync(filename) {
	if(cache.has(filename)) {
    	return cache.get(filename)
    } else {
    	const data = readFileSync(filename, 'utf8')
        cache.set(filename, data)
        return data
    }
}

 -  비동기 → 동기로 변경 시 아래 사항들을 고려해야 한다.

   1) 대체할 수 있는 동기API가 있는지 확인한다. (지원하는 API가 없을 수 있음)

   2) 동기API는 이벤트루프를 block하고 동시요청을 보류하기 때문에 전체 애플리케이션 속도를 떨어뜨릴수 있다.

 

수정방안 2) process.nextTick()을 이용하여 실행을 지연시켜 완전한 비동기로 수행되게 수정

import { readFile } from 'fs'

const cache = new Map()

function consistentReadAsync(filename, callback) {
	if (cache.has(file)) {
    	//지연된 콜백 호출
        process.nextTick(() => callback(cache.get(filename)))
    } else {
    	//비동기 함수 사용
      	readFile(Filename, 'utf8', (err, data) => {
        	cache.set(filename, data)
        	cb(data)
        })
    }
}

 - process.nextTick() : 현재 진행중인 작업의 완료 시점 뒤로 함수의 실행을 지연시킴.

 process.nextTick() vs  setImmediate() vs setTimeout(callback,0) 

    1. process.nextTick()은 현재 작업이 완료 된 후 바로 실행되어 다른 I/O 이벤트가 발생하기 전 실행됨(마이크로 테스크, 콜백을 인수로 받아 대기중인 I/O 이벤트 대기열의 앞으로 밀어넣고 즉시 반환시키는 형태) . 따라서 특정 I/O가 계속 수행되지 않는 기아 상태에 빠질 수 있다.

    2. setImmediate()은 이미 큐에 있는 I/O 이벤트들의 뒤에 넣어 대기하는 형태로 실행됨.

    3.setTimeout(callback,0)은 setImmediate()와 비슷하나 이벤트 루프의 다른 Phase에서 실행된다. setTimeout은 Timer에서, setImmediate는 check에서 실행되기 때문에, I/O 콜백과 연관되어 수행된다면 setImmediate()로 예약된 콜백이 먼저 수행된다.

 

 - 이처럼 CPS ↔ 직접 스타일, 비동기 ↔ 동기 로 변경하게 된다면, 연결된 모든 코드들의 동작방식을 수정해야 되기 때문에 주의가 필요하다. 

 

 

3-1-3. Node.js 콜백 규칙

 1. 콜백은 맨 마지막에

 - Node.js 코어 함수 표준 규칙에 따라, 콜백을 사용하는 API의 경우 콜백이 마지막 로 전달되어야 한다. 함수 호출의 가독성이 좋아지기 때문!

2. 오류는 맨 처음에

- 콜백함수에 오류를 인자로 전달해야되는 경우, 콜백의 첫번째 인자로 전달한다.

또한 오류는 항상 Error 타입으로 전달한다(간단한 문자열이나 숫자를 오류 객체로 전달하지 말자)

3. 에러 처리 방법

 - 동기식 직접 스타일 함수 : try/catch문과 throw문을 사용

 - 비동기식 CPS 함수 : 호출 체인의 콜백으로 전달.

   1) 에러를 다시 밖으로 발생시키거나 리턴하지 않고, 다른 결과처럼 콜백 사용

   2) 콜백함수 내부 동작 코드는 try/catch문을 이용하여 에러 캐치  

    - 콜백에서 에러 발생 시 콜백을 수행시키는 함수의 에러로 감지되지 않는다. callback -> stack -> event loop -> console로 포착되기 때문에 외부에서 잡을 수 없어 내부에서 try/catch로 에러를 캐치해야 한다.

try {
 readJSONThrows('invalid_json.json', (err) => console.error(err))
} catch (err){
//동작하지 않음.
console.log('This will not catch the JSON parsing exception')
}

   3) try문 안에서 콜백 호출 지양(콜백이 자체적으로 발생시키는 에러도 잡히기 때문)

import { readFile } from 'fs'

function readJSON(filename, callback) {
	readFile(filename, 'utf8', (err, data) => {
    	if(err){
        //에러를 전파하고 현재의 함수에서 빠져 나옴
        	return callback(err)
        }
        try {
        	//파일 내용 파싱
            parsed = JSON.parse(data)
        } catch (err) {
        	//파싱 에러 캐치
            return callback(err)
        }
        //에러없음. 데이터 전파
        callbcak(null, parsed)
    })
}

 

 

 

Comments