2020. 3. 8. 23:02

동기와 비동기방식의 차이점(콜백함수와 프로미스)

오늘은 자바보다는 자바스크립트, 그중에서도 노드를 사용한 서버 프로그래밍을 다룰 때 처리하는 동기와 비동기 처리방식에 대해서 알아보려고 한다. 물론 동기와 비동기 자체의 개념은 모든 언어에서 공통적으로 사용되므로 동기와 비동기 방식의 차이점을 분명히 하고 그 개념에 대해서 알아두면 좋다.

우선적으로 동기와 비동기 처리 방식을 비유할 때 커피 주문을 예로 든다. 흔히 카페에서 커피를 주문하려고 하면 한줄로 서서 한번에 한명씩만 주문이 가능하다. 마치 큐처럼 선입선출의 과정의 코드실행의 처리 순서를 우리는 동기 방식이라고 부른다.

일단 주문이 들어가면 카페 내부의 점원들은 열심히 음료를 만들기 시작한다. 하지만 이때 한명의 점원이 있는 것이 아니라 여러명의 점원이 있다면 주문받은 순서대로 일을 처리하는 것이 아니라 각자 파트별로 음료를 나누어서 먼저 완성된 쪽이 음료를 내놓게된다.

이렇게 되면 1번 2번 순서대로 주문을 했더라도 2번의 음료가 먼저 완성이 되었다면 순서와 상관없이 2번의 진동벨이 울리고 먼저 음료를 받아갈 수 있는 것이다. 여기서 점원들을 비동기식, 즉 병렬적으로 테스크를 수행했다고 말할 수 있다.

동기는 순차적, 직렬적으로 태스크를 수행하고, 비동기는 병렬적으로 태스크를 수행한다.

동기(Synchronous)


앞서 간략하게 예를들어 설명했지만 동기는 요청을 보낸 후 응답(결과물)을 받아야지만 다음 동작이 이루어지는 방식을 말한다. 즉, 모든 일은 순차적으로 실행되며 어떤 작업이 수행 중이라면 다음 작업은 대기하게 된다.

function func1() {
	console.log('첫번째 펑션!');
	func2();
}

function func2() {
	console.log('두번째 펑션!');
	func3();
}

function func3() {
	console.log('세번째 펑션!');
func1();

// 출력값으로
// 첫번째 펑션!
// 두번째 펑션!
// 세번째 펑션!
// 을 띄운다.

 

비동기(Asynchronous)


비동기 처리는 왜 필요할까? 만약 데이터를 서버로 부터 받아오는 앱을 만든다고 한다면, 서버로부터 데이터를 받아와서 해당 데이터를 뿌려줘야 하므로 맨 처음에 서버로 부터 데이터를 받아오는 코드가 실행되어야 할 것 이다.

하지만 비동기로 처리하지 않고 동기적으로 이를 구성한다면 데이터를 받아오기까지 기다린 다음에서야 앱이 실행이 될 것이고 서버에 가져오는 데이터의 양이 늘어날 수록 기하급수적으로 앱의 실행 속도는 느려지게 될 것이다. 데이터를 가져오기 까지 앱이 대기하는 상태가 벌어지는 것이다. 이런 사용자들의 불편을 없애기 위해 데이터를 수신하는 코드와 페이지를 표시하는 것과는 비동기적으로 처리를 해야하는 것이다. 그래서 이 비동기처리를 코드로 가장 많이 드는 예는 setTimeout이나 AJAX이다.

function func1() {
	console.log('func1');
	func2();
}
function func2() {
	setTimeout(function() {
		console.log('func2');
 }, 0);
	func3();
 }
function func3() {
	console.log('func3');
}

func1();

/*
func1
func3
func2
*/

자바스크립트는 흔히 알고있듯이 싱글스레드로 프로그램이 동작한다. 그런데 비동기 처리방식은 필연적으로 다중에 스레드가 동작하는 멀티태스킹 작업일 수 밖에 없는데, 어떻게 해서 비동기처리 방식이 가능하게 된 것일까?

자바스크립트는 웹 브라우저나 Node.js의 자바스크립트 엔진에서 실행이 된다. 이 엔진에는 자바스크립트를 돌리는 하나의 쓰레드가 존재한다. 또한 이 엔진 뿐만이 아니라 비동기식 처리 모델인 Web API라는 것이 함께 동작하면서 여기에서 setTimeout 이나 AJAX로 http 데이터를 가져오는 시간이 소요되는 일들을 처리한다.

이 Web API들이 자바스크립트 엔진 스레드와는 따로 비동기 처리를 따로 돌면서 콜백함수를 가지고 이벤트 루프에 들어가 처리되는대로 콜백함수를 다시 자바스크립트 엔진으로 돌려보내준다.

 

콜백함수


콜백함수는 특정 함수에 매개변수로 전달된 함수를 의미한다. 그 콜백함수는 함수를 전달받은 함수안에서 호출된다.

코드로 표현하면 다음과 같다.

function Callback(callback) {
  console.log("콜백 함수 안에");
  callback()
};
 
Callback(function(){console.log("이 부분이 콜백")});
/*
실행결과
콜백 함수 안에
이 부분이 콜백
*/

그렇다면 대체 왜 콜백함수를 사용하는 것일까? 만약 콜백 함수에서 콜백을 받지 않는다면 콜백 함수의 과정이 끝나기도 전에 다음 프로세스를 진행하게 되는 경우가 있다. 실제로 현업에서 DB의 값을 읽어드리는 작업을 했는데 콘솔에 띄워보니 undefined 값만 떠있던 경험이 있다. es7~8이 보편화된 지금은 콜백이 아닌 promise나 await를 붙여줘서 해결하지만 예전에는 이 콜백함수를 통해서 구현하는 것이 일반적이었다.

왜 콜백함수로 짜기 지양해야하는지. 그것은 바로 가독성에 있다. 위에 콜백은 단 하나만 넣은 콜백 함수지만 여러 데이터를 가져와야하는 경우에는 콜백안에 콜백을 이어서 구성을 하는 경우가 있다.

function Callback(callback) {
  function Callback2(callback2) {
	    function Callback3(callback3) {
			  console.log('여기가 지옥');
		};
	};
};

흔히 실무자들이 말하는 '콜백지옥'에 빠질 수 있는 가능성이 높아진다. 이 콜백지옥은 가독성도 떨어지고 실수 위험도 커지기 때문에 최근 es7이상에 와서는 다양한 방법으로 이를 해결할 수 있는 것들을 지원한다. es7에서는 promise를 es8에서는 async와 await를 지원한다.

 

Promise


프로미스는 자바스크립트 비동기 처리에 사용되는 객체다. 프로미스는 주로 서버에서 받아온 데이터를 화면에 표시할 때 사용한다.

function 데이터 가져오는 함수 (data) {
	return new Promise(function (resolve, reject) {
		ajax(url + "info/" + data,
		function (response) {
			resolve(response);
		});
	});
}

서버에 데이터를 가져오는 함수를 간단히 프로미스식으로 구현했다. 이 프로미스가 콜백을 구현하는 방식은 .then 으로 여러개의 프로미스를 연결해서 사용할 수 있는데 이 메서드를 호출하면 새로운 프로미스 객체가 반환된다. 계속해서 콜백을 받아서 다음 콜백으로 넘길 필요가 없이 데이터를 받아오길 기다렸다가 다음 함수를 실행하는 방식으로 이어서 그것도 보기편한 방식으로 바뀐 것이다.

new Promise(function(resolve, reject){
  setTimeout(function() {
    resolve(1);
  }, 2000);
})
.then(function(result) {
  console.log(result); // 1
  return result + 10;
})
.then(function(result) {
  console.log(result); // 11
  return result + 20;
})
.then(function(result) {
  console.log(result); // 31
});

1을 받기를 기다렸다가 거기에 10을 더하고 11을 받기 기다렸다가 다시 20을 더하고 마지막으로 31을 출력하게 된다. 비동기 작업을 순차적으로 처리를 할 수 있게 된 것이다. 콜백함수보다 훨씬 직관적이기 때문에 많이 사용하게 되나 polyfill등의 라이브러리 없이는 익스플로러에서는 동작하지 않으니 주의해야한다.

 

async/await


es8에서는 async/await를 지원한다. 프로미스로 제공하던 함수들을 더 간결하고 직관적이게 실행할 수 있게 됐다. C#이나 dart같은 언어들에서도 사용되니 자바스크립트 개발자가 아니더라도 알아두면 유용하다.

async function getUserinfo() {
	let userInfo = await userCollection.find({});
	console.log("await 다음에 오는 콘솔", userInfo");
}

위 코드는 몽고디비에서 유저 정보를 가져오는 비동기 함수이다.

function 앞에 async로 이어진 함수들 안에 await란 코드를 달면 코드의 진행하다 멈추고 그 await 코드가 끝이난 뒤에 그 다음 작업을 실행하게 된다. 여기서 await를 기다렸다가 정보를 받아온 뒤에 밑의 코드를 실행하게 되는 것이다.

만약 await를 안붙인다면 데이터를 다 받아오기도 전에 밑에 코드로 넘어갈 것이고 콘솔에는 undefine만 뜨게 될 것이다.