요즘IT
위시켓
최근 검색어
전체 삭제
최근 검색어가 없습니다.

프론트엔드 개발자 면접에서 자주 받는 질문이다. 개발자라면 비동기 통신과 동기 통신에 대한 개념을 정확하게 알아야 하고, 이를 바탕으로 효율적인 프로그램을 만들어야 하기 때문이다. 특히 자바스크립트는 싱글 스레드로 작동하기 때문에, 효율적인 프로그램을 만들기 위해서는 비동기 처리를 적절하게 사용하는 것이 중요하다. 이번 글에서는 필자가 면접에서 받은 질문을 토대로 비동기 통신, 동기 통신에 대한 개념을 소개한다. 그리고 비슷한 개념을 가진 블로킹과 논블로킹에 대해서도 살펴보고자 한다.

회원가입을 하면 원하는 문장을
저장할 수 있어요!

다음

회원가입을 하면
성장에 도움이 되는 콘텐츠를
스크랩할 수 있어요!

확인

개발

주니어 웹 개발자가 알아야 할 ‘비동기 통신’

년차,
어떤 스킬
,
어떤 직무
독자들이 봤을까요?
어떤 독자들이 봤는지 궁금하다면?
로그인

“비동기 통신에 대해 설명해 보세요.”

프론트엔드 개발자 면접에서 자주 받는 질문이다. 개발자라면 비동기 통신과 동기 통신에 대한 개념을 정확하게 알아야 하고, 이를 바탕으로 효율적인 프로그램을 만들어야 하기 때문이다. 특히 자바스크립트는 싱글 스레드로 작동하기 때문에, 효율적인 프로그램을 만들기 위해서는 비동기 처리를 적절하게 사용하는 것이 중요하다. 이번 글에서는 필자가 면접에서 받은 질문을 토대로 비동기 통신, 동기 통신에 대한 개념을 소개한다. 그리고 비슷한 개념을 가진 블로킹과 논블로킹에 대해서도 살펴보고자 한다.

 

동기, 비동기 통신이란?

비동기 처리는 프로세스의 완료를 기다리지 않고 동시에 다른 작업을 처리하는 방식이다. 현실 세계의 복잡한 일들은 대부분 비동기 방식으로 처리되기 때문에, 프로그래밍 세계에서도 여러 기능들이 비동기 방식으로 동작한다고 생각할 수 있다. 하지만 프로그래밍 세계에서는 각 개별 기능들이 동기적으로(순차적으로) 수행되는 것이 더 자연스럽다. 프로세스 또는 함수는 컴퓨터의 메모리나 CPU를 점유하면서 동작하는데, 기본적으로 하나의 컴퓨터는 각각의 자원을 하나씩만 가지기 때문이다.

 

프로그래밍 세계에서 비동기적으로(동시에) 여러 일을 수행하기 위해 점차 컴퓨터의 메모리 공간이 커졌고, 각 프로세스에 할당되는 메모리 공간이 분리되었다. 또한 CPU가 여러 프로세스와 스레드를 동시에 실행하는 멀티스레딩 방식이 생겨났다. 하지만 자바스크립트는 싱글스레드로 작동되는 프로그래밍 언어이기 때문에 동시에 두 개의 함수가 실행될 수 없다. 따라서 작업의 효율을 높이기 위해 비동기 처리가 필요하다.

 

2022년 AWS re:Invent 키노트 영상에서 비동기 방식에 대해 친절히 설명하고 있으니, 영상을 보고 이번 글을 읽으면 이해하기 더 쉬울 것이다.

 

 

그동안 면접에서 받은 질문들

1) 모던 브라우저는 어떻게 비동기 로직을 처리하나요?

자바스크립트는 싱글스레드 언어이기 때문에 멀티스레딩을 지원하지 않는다. 하지만 자바스크립트 함수로 호출한 브라우저의 Web API는 서로 다른 스레드에서 동시에 실행된다. 물론 서버에 요청한 API 역시 서버에서 실행되므로 자바스크립트 스레드를 방해하지 않고 수행된다. 브라우저의 콜 스택에는 호출된 함수들이 스택 구조로 쌓이는데, 비동기 함수는 처리되는 동안 콜 스택에서 기다리는 것이 아니라 곧바로 사라진다.

 

비동기 함수가 완료된 이후의 동작은 콜백 함수를 통해서 이루어진다. 비동기 함수를 호출할 때 인자로 콜백 함수를 넘겨주면, 콜백 함수가 별도의 메시지큐에 쌓여 다시 콜 스택에 쌓이기를 기다린다. 브라우저의 이벤트 루프는 주기적으로 콜 스택을 확인하고 콜 스택이 비면 메시지큐의 태스크를 콜 스택으로 이동시킨다. 이러한 원리로 콜 스택은 비동기 함수가 처리되는 동안 정체되지 않고 다음 태스크를 수행할 수 있다.

 

2) 자바스크립트는 어떻게 비동기 로직을 지원해왔나요?

전통적으로 비동기 처리는 콜백 함수를 통해 이루어졌다. 그러나 이 방식은 호출하는 비동기 함수의 깊이가 깊어질수록 에러 핸들링이 까다롭다. 콜 스택을 기준으로 안쪽 스택에서 발생한 에러는 바깥쪽의 호출자 방향으로 전파되는데, 함수의 깊이가 깊어지면 안쪽 콜 스택이 먼저 제거되어 에러가 바깥까지 전파되지 않을 수 있기 때문이다.

 

또한 콜백 함수를 중첩해서 호출하다 보면 소위 말하는 ‘콜백 지옥 현상’이 발생한다. 콜백 지옥이란 비동기 함수를 호출하는 과정에서 콜백 함수의 깊이가 감당하기 힘들 정도로 깊어지는 현상이다.

 

주니어 개발자 동기, 비동기 통신
콜백 지옥 현상 <출처:Callback Hell>

 

자바스크립트 ES6에서는 비동기 함수의 성공과 실패 여부를 then과 catch라는 별도의 블록으로 구분해서 받을 수 있는 Promise 패턴을 지원한다. Promise 객체는 Promise 생성자 함수를 사용하여 만들며, 이 생성자 함수는 콜백 함수를 인자로 받는다.

 

이 함수는 resolve, reject라는 인자를 순서대로 받고, 각각의 함수를 통해 성공과 실패 케이스에 대해 처리할 수 있다. resolve 시킨 결과는 호출자 입장에서 then 블록으로 받을 수 있고, reject 시킨 결과는 호출자 입장에서 catch 블록으로 받을 수 있다. Promise 객체는 프로퍼티로 여러 개의 비동기 함수를 병렬로 수행하도록 하는 all, 모든 비동기 함수가 모두 완료된 후에 수행하도록 하는 allSettled 등의 정적 메서드도 가지고 있어서 활용도가 높다.

 

만약 반드시 여러 개의 비동기 함수 호출이 모두 완료된 후 수행해야 할 태스크가 있다면, 이 부분은 동기적으로 수행되어야 하는 상황으로 볼 수 있다. 이런 상황을 위해 자바스크립트 ES8에서는 async/await 구문이 도입되었다. async 예약어를 붙여서 선언한 함수는 리턴 타입이 Promise 객체이다. Promise 객체의 최종 결괏값을 받기 위해서는 then-catch 블록으로 받거나, async 함수를 호출할 때 await 예약어를 붙여서 호출하면 된다. await로 호출한 비동기 함수는 동기 함수처럼 동작하여, 해당 코드 라인이 완료되어 콜백 함수의 결괏값이 받아진 이후에 다음 코드 라인이 실행된다.

 

3) 동기와 비동기, 블로킹과 논블로킹은 어떻게 다른가요?

프로그램은 여러 함수들의 집합이다. 규모가 작은 프로그램은 메인함수에서 모든 일을 처리할 수 있지만, 규모가 커지면 여러 함수에 역할이 분배되고, 메인 함수가 큰 태스크를 처리하기 위해 작은 태스크를 담당하는 서브 함수를 호출하거나 외부 서비스 API를 호출하게 된다. 이때 메인 함수를 호출자, 서브 함수나 API를 피호출자라고 부른다.

 

주니어 개발자 동기, 비동기 통신
동기와 비동기 <출처:wikidocs>

 

동기와 비동기는 호출자와 피호출자의 수행 시점과 관련이 있다. 만약 메인 함수가 첫 번째 태스크를 처리하기 위해 서브 함수를 호출하고, 서브 함수의 처리결과를 받아서 확인한 후에 두 번째 태스크를 수행한다면 이는 동기적인 로직이다.

 

반면 메인 함수가 서브 함수를 호출만 할 뿐, 서브 함수의 처리 결과를 확인하지 않고 두 번째 태스크를 수행한다면 이는 비동기적인 로직이다. 메인 함수가 서브 함수의 처리 결과를 신경 쓰지 않기 때문에, 대체로 비동기 로직에서는 메인 함수의 다음 태스크와 서브 함수가 동시에 수행된다. 결론적으로 어떤 함수가 동기적인지 비동기적인지는 메인 함수가 서브 함수의 처리 결과를 확인한 후, 다음 태스크로 넘어가는지와 관련이 있다.

 

주니어 개발자 동기, 비동기 통신
블로킹과 논블로킹 <출처:wikidocs>

 

다음으로 블로킹과 블로킹과 논블로킹에 대해 알아보자. 블로킹과 논블로킹은 호출자와 피호출자의 제어권과 관련이 있다. 제어권이란 함수를 실행할 권리이다. 함수의 실행할 권리가 존재한다는 것을 직관적으로 받아들이기 어려울 수 있다. 아래 소스 코드를 통해 살펴보자.

 

function print() {		console.log(1);	console.log(2);	console.log(3);}

 

print 함수를 실행하면 순서대로 1,2,3이 출력된다. 그동안 함수의 제어권은 처음에는 print 함수가 가지고 있다가 차례대로 출력 함수에 넘겨주고, 넘겨받기를 반복한다. 두 번째 출력 함수가 제어권을 갖기 위해서는 먼저 첫 번째 출력 함수가 종료되고, print 함수가 제어권을 돌려받아야 한다.

 

또한 세 번째 출력 함수가 제어권을 갖기 위해서는 먼저 두 번째 출력 함수가 종료되고, print 함수가 제어권을 돌려받아야 한다. 모든 피호출자가 수행을 마치면 최종적으로 print 함수가 제어권을 돌려받고 print 함수가 종료되면서 콜 스택이 비워진다.

 

위와 같은 블로킹 방식에서는 피호출자가 함수를 끝까지 실행한 후, 제어권을 호출자에게 함수에게 돌려준다. 반면, 논블로킹 방식에서는 피호출자가 호출자에게 제어권을 바로 돌려준다. 그렇다면 피호출자가 제어권을 잃어버렸는데 어떻게 태스크를 수행할 수 있을까?

 

이를 위해 멀티 스레드 또는 콜백 함수가 존재한다. 예를 들어, 리눅스의 I/O 환경 같은 멀티 스레드 환경이라면, 사용자 스레드가 시스템 콜로 요청을 보내면 커널 스레드가 생성되어 태스크가 수행되고, 사용자 스레드가 보내준 제어권은 바로 다시 돌려준다. 스레드가 두 개가 되어 각각이 제어권을 가지고 함수를 수행할 수 있게 되는 것이다.

 

반면 싱글 스레드 언어인 자바스크립트의 경우에는 별도의 스레드를 생성할 수 없으므로, 브라우저가 지원하는 이벤트 루프와 메시지 큐라는 공간을 활용한다. 피호출자는 곧바로 제어권을 돌려주고, 콜백 함수는 메시지 큐에 등록되어 대기하다가 이벤트 루프에 의해 콜 스택으로 올라간다.

 

논블로킹 방식에서는 호출자가 피호출자의 처리결과를 원칙적으로 받을 수 없다. 그렇기 때문에 호출자가 피호출자의 처리결과를 확인하고 싶다면 지속적으로 완료 상태를 확인하는 요청을 보내서 확인해야 하고, 피호출자는 그때마다 현재 수행 중인 작업이 완료되었는지를 여부를 응답해야 한다.

 

 

동기-비동기, 블로킹-논블로킹을 조합한 경우

주니어 개발자 동기, 비동기 통신
<출처: freepik>

 

이렇듯 동기-비동기와 블로킹-논블로킹을 조합하면 네 가지 케이스가 나올 수 있다. 좀 더 쉽게 설명하기 위해, 두 명의 요리사가 각각 피자와 파스타를 만들어 파는 레스토랑을 예로 들어보자.

 

1) 동기 - 블로킹

주니어 개발자 동기, 비동기 통신
동기-블로킹 <출처:wikidocs>

 

두 요리사는 각각 피자 또는 파스타 요리를 담당하고 있다. 이들이 요리할 때 반드시 순차적으로 처리해야 하는 부분이 있다. 가령 도우 반죽이 완성되어야 반죽을 오븐에 구울 수 있다. 요리사는 도우 반죽이 완성될 때까지 밀대를 놓을 수 없으며(블로킹), 반죽이 다 된 것을 확인한 후에 오븐에 넣을 수 있다(동기).

 

이를 프로그래밍에 적용해 보면, 하나의 스레드가 일련의 연산을 순차적으로 수행하는 것에 해당한다. 가령 연속된 덧셈 연산은 순차적으로 이루어진 후에 결괏값이 도출된다.

 

2) 동기 - 논블로킹

주니어 개발자 동기, 비동기 통신
동기-논블로킹 <출처:wikidocs>

 

피자 담당 요리사는 오븐에서 도우가 다 구워진 것을 확인한 다음, 꺼내서 토핑을 얹을 수 있다(동기). 하지만 그동안 다른 일을 할 수 없는 것은 아니다. 요리사는 주기적으로 남은 오븐 시간을 확인하고, 완료되지 않았다면 그동안 토핑을 만들 수 있다(논블로킹). 마침내 시간이 되어 도우가 구워졌다면 꺼내어 토핑을 얹는다.

 

리눅스에서는 유저의 시스템 콜 요청이 발생하면, 커널 스레드가 제어권을 넘겨받아 일을 시작하고 곧바로 유저 스레드에게 제어권을 돌려준다. 유저 스레드는 주기적으로 커널 스레드에게 작업 완료 여부를 물어보고, 커널 스레드의 응답이 ‘완료’로 돌아오기 전까지 별개의 작업을 수행하다가 ‘완료’ 응답이 돌아오면 이와 관련된 다음 태스크를 시작한다.

 

3) 비동기 - 논블로킹

주니어 개발자 동기, 비동기 통신
비동기-논블로킹 <출처:wikidocs>

 

피자 담당 요리사와 파스타 담당 요리사는 각자 맡은 요리만 할 뿐, 서로의 요리 과정에는 별 관심이 없다. 각자가 어떤 단계를 수행하고 있는지 확인하지 않고(비동기), 서로의 작업을 방해하지도 않는다(논블로킹). 다만 두 요리가 함께 나가야 한다면, 먼저 완성된 요리와 나중에 완성된 요리를 모아줄 주체가 필요하다.

 

프로그래밍 관점에서는 메인 스레드에서 파생된 두 개의 스레드가 동시에 실행되는 상황을 생각해 볼 수 있다. 인터넷 브라우저의 경우에는 메인 스레드에서 두 개의 API를 호출하면서, 콜백 함수로 각각의 API 처리가 완료된 이후의 동작을 예약할 수 있을 것이다. 이 경우에 비동기를 동기로 처리하고 싶다면 앞서 설명한 async / await을 사용하여 처리할 수 있다.

 

4) 비동기 - 블로킹

주니어 개발자 동기, 비동기 통신
비동기-블로킹 <출처:wikidocs>

 

비동기와 블로킹은 특수한 상황으로 볼 수 있다. 피자 요리사와 파스타 요리사는 서로의 작업에 대해 확인할 필요가 없지만, 모종의 이유로 작업을 번갈아서 수행한다. 피자 요리사가 도우를 반죽하는 동안 파스타 요리사는 쉬고, 파스타 요리사가 면을 삶는 동안 피자 요리사는 쉬는 것이다.

 

이 경우는 아직 경험해 본 적이 없다. 의도적으로 스레드를 쿨 다운 시켜야 하는 것이 아니라면 굳이 필요한 상황은 아닐 것으로 보인다. (만약 이러한 상황을 겪어 보신 분은 댓글로 의견을 남겨주셔도 좋습니다.)

 

 

비동기 개념의 중요성

지금까지 동기, 비동기의 개념과 블로킹, 논블로킹에 대해 알아봤다. 비동기 개념에 대해 주니어 개발자들이 종종 헤매는 것을 보았는데, 정확하게 숙지하고 있어야 할 중요한 부분이다. 실무에서 비동기 처리 문법을 사용할 줄 알아야 하며, 면접에서도 잘 답변하기 위해 이론적으로 알고 있어야 한다. 만약 개발자 면접을 앞두고 있다면, 비동기 개념에 대해 정확하게 설명할 수 있는지 짚어볼 필요가 있다. 직접 다양한 사례를 만들어 생각해 보는 것도 도움이 된다.

 

나는 최근 수행한 프로그래밍 과제에서 async/await을 써야 하는 상황인지, 아닌지에 대해 고민한 적이 있었다. 무의식적으로 async/await 패턴을 사용하려고 했는데, 생각해보니 동기 처리는 불필요하다는 것을 깨달았다. 이렇듯 익숙함에 따라 async/await을 사용하는 것이 아니라, 꼭 필요한 상황인지 판단 후에 사용해야 한다. 사소한 로직에서 불필요한 지연이 발생하기 시작하면 점차 큰 성능 저하로 이어질 수도 있기 때문이다.

 

zwoo

편집 오신엽 객원 에디터


<참고 자료>

[10분 테코톡] 우의 Block vs Non-Block & Sync vs Async

동기 vs 비동기 (feat. blocking vs non-blocking)

JavaScript Event Loop Explained

[도서] 모던 자바스크립트 Deep Dive

 

요즘IT의 모든 콘텐츠는 저작권법의 보호를 받는 바, 무단 전재와 복사, 배포 등을 금합니다.

좋아요

댓글

공유

공유

댓글 2
kseop7
            잘 읽었습니다!
          
2023.04.21. 오전 07:35
98sangbin
            아주 중요한 내용이네요
          
2023.05.03. 오전 08:26
프론트엔드 엔지니어
201
명 알림 받는 중

작가 홈

프론트엔드 엔지니어
201
명 알림 받는 중
2년차 프론트엔드 개발자입니다. 리액트와 자바스크립트를 좋아합니다.

좋아요

댓글

스크랩

공유

공유

요즘IT가 PICK한 뉴스레터를 매주 목요일에 만나보세요

요즘IT가 PICK한 뉴스레터를
매주 목요일에 만나보세요

뉴스레터를 구독하려면 동의가 필요합니다.
https://auth.wishket.com/login