...
동기/비동기 & 블로킹/논블록킹
프로그래밍에서 웹 서버 혹은 입출력(I/O)을 다루다 보면 동기/비동기 & 블로킹/논블로킹 이러한 용어들을 접해본 경험이 한번 쯤은 있을 것이다. 대부분 사람들은 용어들이 나타내고자 하는 행위에 대해선 멀티 태스킹과 밀접한 관련이 있다는 것을 알고 있다. 그래서 두 개념을 비슷한 것으로 오해하는 사람들이 꽤 많다. 😵💫
동기/비동기 와 블로킹/논블로킹 이 두 개념은 표현 형태는 비슷해 보일지라도, 서로 다른 차원에서 작업의 수행 방식을 설명하는 개념이다. 동기/비동기는 요청한 작업에 대해 완료 여부를 신경 써서 작업을 순차적으로 수행할지 아닌지에 대한 관점이고,블로킹/논블록킹은 단어 그대로 현재 작업이 block(차단, 대기) 되느냐 아니냐에 따라 다른 작업을 수행할 수 있는지에 대한 관점이다.
이처럼 두 개념에 대한 의미 차이는 명확하지만, 프로그래밍에서는 종종 혼용되어 사용되기도 한다. 대표적으로 자바스크립트의 setTimeout() 함수를 일반적으로 비동기 함수라고만 부르지만 동시에 논블로킹 함수이기도 하다. 즉, 우리가 편의상 부르는 자바스크립트 비동기 함수는 사실 비동기 + 논블로킹 함수인 것이다. 따라서 이들을 정확하게 구분하고 이해하는 것이 컴퓨터 아키텍쳐를 이해하는데 있어 중요하다.
동기(Synchronous) / 비동기(Asynchronous)
동기(同期)와 비동기(非同期)를 알아보기 앞서, 이 두 개념을 한글이 아닌 영단어로 학습 하길 강력히 권장하는 바다. 왜냐하면 한글로 직역하자면 '같은 기간' 또는 '같은 주기'라는 뜻의 한자를 사용하는데 오히려 의미를 이해하는데 있어 더욱 혼동만 주기 때문이다. 그래서 영단어를 파보자면 Synchronous의 Syn는 그리스어로 '함께'이란 뜻이고 chrono는 '시간'이라는 뜻이다.
즉, Synchronous는 작업 시간을 함께 맞춰서 실행한다 라는 뜻으로 해석된다. 작업을 맞춰 실행한다는 말은 요청한 작업에 대해 완료 여부를 따져 순차대로 처리하는 것을 말한다. Asynchronous는 앞에 A라는 접두사가 붙어 부정하는 형태이다. 그래서 동기와 반대로 요청한 작업에 대해 완료 여부를 따지지 않기 때문에 자신의 다음 작업을 그대로 수행하게 된다.
비동기의 성능 이점
보통 비동기 특징을 이용하여 성능과 연관지어 말한다. 왜냐하면 요청한 작업에 대하여 완료 여부를 신경쓰지 않고 자신의 그다음 작업을 수행한다는 것은, I/O 작업과 같은 느린 작업이 발생할 때, 기다리지 않고 다른 작업을 처리하면서 동시에 처리하여 멀티 작업을 진행할수 있기 때문이다. 이는 전반적인 시스템 성능 향상에 도움을 줄 수 있다.
예를 들어, 웹 애플리케이션에서 데이터베이스 쿼리를 수행하는 작업이 있다고 가정해보자. 이 작업을 만일 동기적으로 수행하면, 데이터베이스에서 응답이 올 때까지 기다려야 한다. 그러면 이 때 웹 애플리케이션은 다른 요청을 처리하지 못하므로, 대규모 트래픽이 발생할 경우 웹 애플리케이션의 성능이 저하될 수 있다. 하지만 비동기 방식으로 데이터베이스 쿼리를 수행하면, 데이터베이스에서 응답이 올 때까지 기다리는 동안에도 다른 요청을 '동시에 처리'할 수 있게 된다. 이렇게 비동기 방식을 사용하면, 대규모 트래픽에서도 안정적으로 동작할 수 있는 웹 애플리케이션을 만들 수 있다.
여기서 '동시 처리' 라는 개념은 두 개 이상의 작업이 동시에 실행되는 것을 의미 한다. 이는 멀티 스레드나 멀티 프로세싱과 같은 방식으로 구현될 수 있다. 자바스크립트 같은 경우 비동기로 작업을 요청하면 브라우저에 내장된 멀티 스레드로 이루어진 Web API에 작업이 인가되어 메인 Call Stack과 작업이 동시에 처리되게 된다. 쉽게 비유하자면 작업을 백그라운드에 인가한다고 생각하면 된다. 대표적인 비동기 작업의 종류로는 애니메이션 실행, 네트워크 통신, 마우스 키보드 입력, 타이머 등 많다. 다만 자바스크립트 코드 실행 자체는 Web API가 아닌 Call Stack에서 실행된다.
동기와 비동기는 작업 순서 처리 차이
위에서 동기와 비동기를 요청한 작업에 대해 완료 여부에 대한 차이라고 말했었다. 이를 쉽게 말하자면 여러개의 요청 작업을 순차적으로 처리하느냐 아니냐에 따른 차이로 보면 된다. 요청한 작업에 대한 완료 알림을 반드시 받아야 다음 작업을 수행한다는 말은 작업을 순서대로 처리한다는 말이기 때문이다. 따라서 동기 작업은 요청한 작업에 대해 순서가 지켜지는 것을 말하는 것이고, 비동기 작업은 순서가 지켜지지 않을 수 있다는 것을 말한다.
예를들어 A, B, C 라는 3 개의 작업(Task)이 있다고 가정하자. 동기 방식으로 실행하면 A → B → C 순서대로 실행되게 되고, 비동기 방식으로 실행하면 A → C → B 또는 C → A → B 등의 무작위의 순서로 실행되게 되는 것이다.
정리하자면 작업 3개를 요청했는데 응답에서 그 순서가 지켜진다면 동기이고 어떤 게 먼저 올지 모른다면 비동기라고 보면 된다.
Blocking / Non-Blocking
블로킹과 논블록킹은 단어에서 알 수 있듯이 다른 요청의 작업을 처리하기 위해 현재 작업을 block(차단, 대기) 하냐 안하냐의 유무를 나타내는 프로세스의 실행 방식이다.
동기/비동기가 전체적인 작업에 대한 순차적인 흐름 유무라면, 블로킹/논블로킹은 전체적인 작업의 흐름 자체를 막냐 안 막냐로 볼 수 있는 것이다. 예를 들어, 파일을 읽는 작업이 있을 때, 블로킹 방식으로 읽으면 파일을 다 읽을 때까지 대기하고, 논블로킹 방식으로 읽으면 파일을 다 읽지 않아도 다른 작업을 할 수 있다.
비동기와 논블로킹 개념 차이
아마 새내기 개발자들이 가장 혼동하는 부분이 비동기와 논블로킹의 차이 부분일 것이다. 포스팅 초반에서 자바스크립트의 setTimeout 함수를 비동기 함수이며 동시에 논블로킹 함수이라고 했다. 이는 어떠한 관점으로 setTimeout 을 바라보느냐에 따라 각기 부르는게 달라지기 때문이다. 예를들어 아래 코드는 setTimeout 함수를 사용하여 1초 후에 "Hello, world!"를 출력하는 비동기 논블로킹 코드의 예제이다. 코드를 실행하면, "시작"과 "끝"이 먼저 출력되고, 1초 후에 "1초 후에 실행됩니다!"가 출력되게 된다.
console.log("시작");
setTimeout(() => {
console.log("1초 후에 실행됩니다!");
}, 1000);
console.log("끝");
즉, 출력 순서와 정의된 코드 라인 순서가 맞지 않은 것이다. 이는 setTimeout 함수에 대해 타이머 작업 완료 여부를 신경 쓰지 않고 바로 그 다음 콘솔 작업을 수행하였기 때문이다. 그리고 setTimeout 함수의 타이머 작업 완료 알람을 콜백 함수를 통해 값을 받아 출력하였다. 따라서 setTimeout 은 비동기(Asynchronouse)이다.
다른 시각으로 보면 메인 함수 작업에 대해서 setTimeout 함수는 자신의 타이머 작업을 수행하기 위해 메인 함수를 블락하지 않고 백그라운드에서 별도로 처리되었다. 메인 함수를 블락하지 않으니 setTimeout 함수를 호출하고 바로 그 다음 콘솔 함수를 호출한 것이다. 따라서 setTimeout 은 논블로킹(Non-blocking)이다.
그런데 결국은 비동기나 논블로킹이나 추구하는 결과로만 보자면 동시에 다른 작업을 수행한다는 점에서 그게 그것이라고 볼 수도 있다. 이는 시점과 관련된 이론적인 개념이라 실제 코드에서 경계를 구분하기가 애매하기도 하다. 그래서 프로그래밍에선 이를 혼용하여 사용하는 것 같다. 그러나 엄연한 나타내는 의미는 다르다. 😵
우리가 동기/비동기, 블로킹/논블로킹을 혼동하는 이유는, 자바스크립트를 처음 배울 때 프로그래밍 서적이나 강의에서setTimeout과 같은 함수를 '비동기 함수' 라고 지칭하고 외워버려서 그렇다고 생각한다. 그래서 '비동기' 라고 하면 자연스럽게 병렬 실행을 떠올라 비동기와 논블로킹을 혼동하는 것이다.
비동기는 출력 순서와 관련된 개념이고 논블로킹이 병렬 실행과 관련된 개념이다. 따라서 글쓴이는 개인적으로 이 부분은 잘못되었다고 비판하고 싶다. 아무리 조금 난해한 개념이라도 처음부터 비동기 + 논블로킹 함수라고 가르치는것이 추후를 위해 개념을 다잡는데 옳다고 본다.
비동기 논블로킹과 콜백 함수
자바스크립트 비동기 프로그래밍에서 빠지지 않는 개념이 바로 콜백(callback) 이다. 비동기 논블로킹이란 다른 작업의 결과를 기다리지 않고 병렬적으로 실행되는 방식을 말하는데, 이때 다른 작업의 완료 여부나 결과에 대한 후처리를 위해 이용되는 방식이 콜백 함수이다.
그럼 콜백 함수는 비동기와 관련이 있는 것일까? 논블로킹과 관련이 있는 것일까? 정답은 둘다 관련이 있다. 엄밀히 따지자면 콜백 함수는 비동기 논블로킹을 구현하는 하나의 기술이지 개념이 아니다. 콜백 지옥(callback hell) 때문에 콜백 함수 방식으로 비동기 논블로킹을 구현하기 싫다면 Promise 객체를 이용하여도 되고 아예 이벤트로 emit 하여 처리하여도 된다. 방법은 여러가지이다. 핵심은 비동기 논블로킹을 구현하는 기술은 콜백 외에 여러가지가 있다는 점이고 콜백은 그 중 하나일 뿐이라는 것이지 동기/비동기 & 블로킹/논블로킹 개념과 직접적인 관련은 없다.
/* 콜백 함수 방식으로 구현한 비동기 + 논블로킹 서버 요청 작업 */
$.ajax({
url: 'https://jsonplaceholder.typicode.com/todos/1',
type: 'GET',
dataType: 'json',
success: function(data) { // 요청이 성공하면 호출될 콜백 함수
console.log(data);
},
error: function(err) { // 요청이 실패하면 호출될 콜백 함수
throw err;
}
});
// 요청을 보내는 동시에 다른 작업을 수행할 수 있습니다.
console.log('Hello');
/* 프로미스 객체 방식으로 구현한 비동기 + 논블로킹 서버 요청 작업 */
fetch('https://jsonplaceholder.typicode.com/todos/1')
.then((response) => {
return response.json();
})
.then((data) => {
console.log(data);
})
.catch((err) => {
throw err;
});
// 요청을 보내는 동시에 다른 작업을 수행할 수 있습니다.
console.log('Hello');
/* 이벤트 방식으로 구현한 비동기 + 논블로킹 파일 다운 작업 */
const fileReader = new FileReader(); // FileReader 객체를 생성
const input = document.querySelector('input');
const file = input.files[0]; // input 태그에서 선택된 파일을 가져옵니다
fileReader.addEventListener('load', function() {
console.log(fileReader.result); // 파일이 load가 완료되면 내용을 출력합니다.
});
fileReader.addEventListener('error', function(err) {
throw err;
});
// 요청을 보내는 동시에 다른 작업을 수행할 수 있습니다.
console.log('Hello');
누가 제어권을 가지고 있느냐
다른 서적에선 동기/비동기와 블로킹/논블로킹을 보다 명확히 구분하기 위해서 '제어권' 이라는 어려운 단어를 쓴다. 제어권은 간단히 말해서 함수의 코드나 프로세스의 실행 흐름을 제어할 수 있는 권리 같은 것이다. 이 개념은 운영체제(OS)의 커널(kernel)에서 따온 개념으로 I/O 동작에서 설명하는 부분이다. 즉, Blocking과 Non-Blocking은 호출된 함수(callee)가 호출한 함수(caller)에게 제어권을 바로 주느냐 안주느냐로 구분된다. 제어권이 넘어가버리면 해당 스레드는 블로킹되게 된다.
다음은 A 함수와 B 함수 작업에 대해 A 함수가 B 함수를 Blocking 방식으로 호출할시 제어권 상태를 나타낸 그림이다.
- A 함수가 B 함수를 호출하면 B에게 제어권이 넘어간다
- 제어권을 넘겨받은 B는 함수를 실행한다.
- 이때 A는 B에게 제어권을 넘겨주었기 때문에 A 함수 실행을 잠시 멈춘다. (Block)
- B 함수가 실행이 끝나면 자신을 호출한 A에게 제어권을 돌려준다.
- 제어권을 다시 받은 A 함수는 그다음 작업을 실행한다.
다음은 A 함수와 B 함수 작업에 대해 A 함수가 B 함수를 Non-Blocking 방식으로 호출할시 제어권 상태를 나타낸 그림이다.
- A 함수가 B 함수를 호출한다.
- 호출된 B 함수는 실행되지만, 제어권은 A 함수가 그대로 가지고 있는다.
- A 함수는 계속 제어권을 가지고 있기 때문에 B 함수를 호출한 이후에도 자신의 코드를 계속 실행한다.
동기/비동기 + 블로킹/논블로킹 조합
지금까지 동기/비동기 와 블로킹/논블로킹 각각의 차이점에 대해 알아보았다. 이처럼 둘은 유사하지만 엄연히 다른 개념이다. 그래서 프로그램 아키텍쳐에서는 이 두 개념이 함께 조합되어 사용된다. 예를들어 다음 4가지로 조합이 가능하다.
- Sync Blocking (동기 + 블로킹)
- Async Blocking (비동기 + 블로킹)
- Sync Non-Blocking (동기 + 논블로킹)
- Async Non-Blocking (비동기 + 논블로킹)
이러한 동기/비동기와 블로킹/논블로킹의 조합은 프로그램이 작동하는 방식에 영향을 미치는 중요한 요소다. 어떠한 요소를 조합하여 사용하느냐에 따라 프로그램의 성능과 효율성을 높일 수 있기 때문이다.
대표적인 예로 Node.js 를 들 수 있다. Node.js에서 비동기 방식으로 파일을 읽거나 네트워크 요청을 보낼 때는 비동기 & 논블로킹 방식을 사용하여 작업이 완료될 때까지 다른 작업을 수행할 수 있도록 한다. 반면에, Node.js에서 코드 실행 시점을 늦춰주거나 순차적인 의존성이 있는 작업을 처리할 때는 동기 & 블로킹 방식을 사용하여 작업의 순서와 타이밍을 제어할 수 있도록 한다.
아마 우리는 이미 개념이 적용된 프레임워크를 이용해 편리하게 사용해 왔어서 와닿지 않을 수 있다. 하지만 이러한 이론적인 부분을 이해하고 적용한다면 직접 설계할때 더욱 효율적인 어플리케이션을 개발에 도움이 될 수 있으며, 개념들을 명확히 알고 있으면 Node.js의 동작 원리와 장점을 더 잘 이해하고 활용할 수 있을 것이다. 따라서 동기/비동기와 블로킹/논블로킹의 개념을 이해하고 적절한 조합을 선택하는 것이 중요하다.
Sync Blocking 조합
Sync Blocking 조합은 다른 작업이 진행되는 동안 자신의 작업을 처리하지 않고 (Blocking), 다른 작업의 완료 여부를 바로 받아 순차적으로 처리하는 (Sync) 방식이다. 다른 작업의 결과가 자신의 작업에 영향을 주는 경우에 활용할 수 있다.
실생활 동작 예시
어느 개발팀에서 사원1, 사원2, 사원3 와 팀장이 있다고 하자. 개발팀장은 개발팀 작업의 흐름을 조율하고, 사원들에게 업무를 지시한다고 한다.
- 팀장 : 사원1씨 업무 A 좀 해주세요
- 사원1 : 네 알겠습니다. (A를 처리중)
- 팀장 : (사원1이 A를 다할때까지 아무일도 하지 않고 기다린다)
- 사원1 : 팀장님 A 업무 완료했습니다.
- 팀장 : 수고했어요. 사원2씨 업무 B좀 해주세요.
- 사원2 : 네 알겠습니다. (B를 처리중)
- 팀장 : (사원2가 B를 다할때까지 아무일도 하지 않고 기다린다)
- 사원2 : 팀장님 B 업무 완료했습니다.
- 팀장 : 수고했어요. 사원3씨 업무 C좀 해주세요.
- ...생략
코드 동작 예시
Sync Blocking 의 대표적인 예로 파일을 읽어 내용을 처리하는 로직을 들 수 있다. 파일을 먼저 읽어야 그다음 작업을 처리할 수 있기 때문이다.
const fs = require('fs'); // 파일 시스템 모듈 불러오기
// 동기적으로 파일 읽기
const data1 = fs.readFileSync('file1.txt', 'utf8'); // file1을 sync으로 read 함
console.log(data1); // 파일 내용 출력하고 적절한 처리를 진행
const data2 = fs.readFileSync('file2.txt', 'utf8');
console.log(data2);
const data3 = fs.readFileSync('file3.txt', 'utf8');
console.log(data3);
이러한 Sync Blocking 은 코드가 순차적으로 실행되는 특성을 가지고 있다. 그래서 Sync Blocking 조합은 일반적으로 작업이 간단하거나 작업량이 적은 경우에 사용된다. 작은 데이터를 처리하거나 파일 하나를 읽고 쓰는 경우에는 Sync Blocking 방식이 더 간단하고 직관적일 수 있다. 하지만 작업량이 많거나 시간이 오래 걸리는 작업을 처리해야 하는 경우에는 Sync Blocking 방식을 사용하면 독이 된다. 왜냐하면 작업을 처리하면 작업이 끝날 때까지 다른 작업을 처리하지 못하므로, 전체 처리 시간이 오래 걸리게 되어 비효율적이게 되게 된다. 따라서 이러한 경우에는 바로 뒤에서 다룰 Async Non-Blocking 방식을 사용하여 작업을 처리하는 것이 좋다.
적용 프로그램 예시
대표적으로 C나 JAVA의 코드 실행 후 커맨드에서 입력을 받는 경우가 이에 해당된다. 사용자로부터 입력을 받아야 그 입력값을 가지고 내부 처리를 하여 결과값을 콘솔에 출력해주기 때문에 순차적인 작업이 요구된다. 내부적으로 본다면 실행 코드가 콘솔창을 띄우고 Please enter your name 텍스트를 치고 난 다음 사용자의 리턴값이 필요하기 때문에 제어권을 시스템에서 사용자로 넘겨 사용자가 값을 입력할때까지 기다리는 것이다.
Async Non-Blocking 조합
Async Non Blocking 조합은 다른 작업이 진행되는 동안에도 자신의 작업을 처리하고 (Non Blocking), 다른 작업의 결과를 바로 처리하지 않아 작업 순서가 지켜지지 않는 (Async) 방식이다. 다른 작업의 결과가 자신의 작업에 영향을 주지 않은 경우에 활용할 수 있다.
실생활 동작 예시
- 팀장 : 사원1씨 A업무좀 해주세요. (동시에 지시)
- 팀장 : 사원2씨 B업무좀 해주세요. (동시에 지시)
- 팀장 : 사원3씨 C업무좀 해주세요. (동시에 지시)
- 팀장 : 다른일을 해야지 ~
- 사원2 : 팀장인 B 모두 처리했습니다. (업무량에 따라 각 사원마다 완료하는 시간이 제각기 다를 수 있다)
- 사원1 : 팀장인 A 모두 처리했습니다.
- 사원3 : 팀장인 C 모두 처리했습니다.
코드 동작 예시
위에서 파일 읽기 코드를 Sync Blocking 방식으로 구현하였는데 이를 그대로 Async Non-Blocking 방식으로도 코드를 구현할 수 있다. Sync Blocking 방식과 두드러진 차이점은 호출 함수에 콜백 함수를 넣었다는 점이다. 이를 통해 비동기 논블로킹 방식대로 처리된 작업의 결과를 후처리 할 수 있게 된다. 그리고 비동기 이기 때문에 작업의 순서를 고려하지 않아 동기 블로킹과는 거꾸로 가장 먼저 'done' 이라는 콘솔 로그가 찍히게 되고 그 후에 파일 내용이 출력될 것이다.
// 비동기적으로 파일 읽기
const fs = require('fs'); // 파일 시스템 모듈 불러오기
fs.readFile('file.txt', 'utf8', (err, data) => { // 파일 읽기 요청과 콜백 함수 전달
if (err) throw err; // 에러 처리
console.log(data); // 파일 내용 출력
});
fs.readFile('file2.txt', 'utf8', (err, data) => {
if (err) throw err;
console.log(data);
});
fs.readFile('file3.txt', 'utf8', (err, data) => {
if (err) throw err;
console.log(data);
});
console.log('done'); // 작업 완료 메시지 출력
fs.readFileSync와fs.readFile함수는 둘다 똑같이 파일을 읽는 함수이지만 동기냐 비동기냐에 따라 차이가 있다.
즉,fs.readFileSync함수는 동기적으로 파일을 읽고fs.readFile함수는 비동기적으로 파일을 읽는다.
이처럼 Async Non Blocking 조합은 작업량이 많거나 시간이 오래 걸리는 작업을 처리해야 하는 경우에 적합하다. 예를 들어, 대용량 데이터를 처리하거나 많은 요청을 처리하는 서비스에서는 Async Non Blocking 방식을 사용하여 한 작업이 처리되는 동안 다른 작업을 처리할 수 있으므로 전체 처리 시간을 줄일 수 있어 어플리케이션 처리 성능을 향상시킬 수 있게 된다.
활용 예시 프로그램
비동기 논블로킹을 활용하는 프로그램은 많이 있다. 그중 예를 들자면, 웹 브라우저의 파일 다운로드가 비동기 논블로킹을 활용하는 예시 라고 할 수 있다. 웹 브라우저는 웹 사이트에서 파일을 다운로드할 때, 파일의 전송이 완료될 때까지 다른 작업을 하지 않고 기다리는 것이 아니라, 다른 탭이나 창을 열거나 웹 서핑을 할 수 있다. 이는 웹 브라우저가 파일 다운로드를 비동기적으로 처리하고, 콜백 함수를 통해 다운로드가 완료되면 알려주는 방식으로 구현되어 있기 때문이다.
Sync Non-Blocking 조합
Sync Non-Blocking 조합은 다른 작업이 진행되는 동안에도 자신의 작업을 처리하고 (Non Blocking), 다른 작업의 결과를 바로 처리하여 작업을 순차대로 수행 하는 (Sync) 방식이다.
실생활 동작 예시
팀장이 업무 A, B, C를 사원 1, 2, 3 에게 시키려고 한다. 그런데 업무 특성상 업무 A를 완료해야 업무 B를 할 수 있고, 업무 B를 완료해야 업무 C를 할 수 있다고 한다.
- 팀장 : 사원1씨 업무 A 좀 해주세요
- 사원1 : 네 알겠습니다. (A를 처리중)
- 팀장 : 다음 업무 B를 하려면 A가 완료되야 하는데.. 사원1씨 다했어요?
- 사원1 : 아직이요 A 처리중입니다
- 팀장 : 사원1씨 다했어요?
- 사원1 : 아직이요 A 처리중입니다
- 사원1 : 팀장님 A 모두 완료했습니다
- 팀장 : 수고했어요. 사원2씨 업무 B좀 해주세요.
- 사원2 : 네 알겠습니다. (B를 처리중)
- 팀장 : 다음 업무 C를 하려면 B가 완료되야 하는데.. 사원2씨 다했어요?
- ...생략
코드 동작 예시
동기 + 논블로킹 코드를 표현하는데 적합한 대중적인 언어로 자바(Java)를 들 수있다. 스레드(Thread) 객체를 만들어 요청 작업을 백그라운드에 돌게 하고 메인 메서드에서 while문을 통해 스레드가 모두 처리되었는지 끊임없이 확인하고, 처리가 완료되면 다음 메인 작업을 수행한다.
// Runnable 인터페이스를 구현하는 클래스 정의
class MyTask implements Runnable {
@Override
public void run() {
// 비동기로 실행할 작업
System.out.println("Hello from a thread!");
}
}
public class Main {
public static void main(String[] args) {
// Thread 객체 생성
Thread thread = new Thread(new MyTask());
// 스레드 실행
thread.start();
// Non-Blocking이므로 다른 작업 계속 가능
System.out.println("Main thread is running...");
// Sync를 위해 스레드의 작업 완료 여부 확인
while (thread.isAlive()) {
System.out.println("Waiting for the thread to finish...");
}
System.out.println("Thread finished!");
System.out.println("Run the next tasks");
}
}
분명 스레드를 이용하여 작업을 병렬적으로 처리하도록 지시했지만, 메인 코드의 while문을 수행함으로서 요청한 작업의 완료 여부를 계속 확인하고 결과적으로 결국 동기적으로 작업을 순서대로 수행됨을 볼 수 있다.
자바스크립트(Javascript) 경우에는 동기 + 논블로킹 코드를 구현하기에는 지원하는 메서드에 한계가 있어 완벽히 표현할 수는 없다. 다만 이와 비슷하게 표현할 수 있는 기법이 있는 데 바로 async/await 키워드 이다. 기본적으로 Promise.then 핸들러 방식은 비동기 논블로킹 방식이라 볼 수 있다. 이때 비동기 작업들 간의 순서가 중요하다면 await 키워드를 통해 동기적으로 처리해 줄 수 있다.
예를 들어 Node.js에서 여러개의 파일을 읽어서 내용을 비교하는 작업을 한다고 가정해보자. 이를 Sync Non-Blocking 방식으로 작성한다면, 세 파일을 동시에 읽기 시작하고 (Non Blocking), 두 파일의 읽기가 모두 완료되면 내용을 비교하는 후처리를 진행한다 (Sync).
const fs = require('fs');
const { promisify } = require('util'); // 유틸리티 모듈 불러오기
const readFileAsync = promisify(fs.readFile); // fs.readFile 함수를 Promise 객체를 반환하는 함수로 변환
async function readFiles() {
try {
// Promise.all() 메소드를 사용하여 여러 개의 비동기 작업을 병렬로 처리합니다. (비동기 논블로킹)
const [data1, data2, data3] = await Promise.all([
readFileAsync('file.txt', 'utf8'), // file.txt 파일을 읽습니다.
readFileAsync('file2.txt', 'utf8'), // file2.txt 파일을 읽습니다.
readFileAsync('file3.txt', 'utf8') // file3.txt 파일을 읽습니다.
]);
// 파일 읽기가 완료되면 data에 파일 내용이 들어옵니다.
console.log(data1); // file.txt 파일 내용을 출력합니다.
console.log(data2); // file2.txt 파일 내용을 출력합니다.
console.log(data3); // file3.txt 파일 내용을 출력합니다.
// 파일 비교 로직 실행...
} catch (err) {
throw err;
}
}
readFiles(); // async 함수를 호출
async/await은 비동기 논블로킹 방식을 동기 논블로킹 방식으로 바꿔주는 기법이라고 할 수 있지만, 그러나 async/await은 내부적으로는 여전히 비동기 논블로킹 방식으로 동작한다는 점은 유의하자. async 함수 자체가 메인 콜 스택(call stack)이 모두 실행되어 비워져야 수행하기 때문에 비동기 논블로킹이기 때문이다.
활용 예시 프로그램
동기 논블로킹은 흔하지 않는 케이스이지만 그래도 이를 표현할 수 있는 프로그램이 있다. 게임에서 맵을 이동할때를 생각해보자. 우선 맵 데이터를 모두 다운로드 해야 할 것이다. 그동안 화면에는 로딩 스크린이 뜬다. 이 로딩 스크린은 로딩바가 채워지는 프로그램이 수행하고 있는 것이다. 즉, 제어권은 여전히 나한테 있어 화면에 로드율이 표시되는 것이다. 그리고 끊임없이 맵 데이터가 어느정도 로드가 됬는지 끊임없이 조회한다. 자신의 작업을 계속하고 있지만 다른 작업과의 동기를 위해 계속해서 다른 작업이 끝났는지 조회하는 것이다.
Sync Blocking vs Sync Non-Blocking
그런데 가만히 보면 블로킹이든 논블로킹이든 메인 함수에서 결국은 코드를 순차적으로 수행하기 때문에 전체 작업의 최종 걸린 시간은 둘이 차이가 없어 보인다. 그럼 두 방식은 도긴개긴인 것인가?
성능 차이는 상황에 따라 다르겠지만, 일반적으로 동기 + 논블로킹이 동기 + 블로킹보다 효율적일 수 있다. 왜냐하면 동기 + 논블로킹은 호출하는 함수가 제어권을 가지고 있어서 다른 작업을 병렬적으로 수행할 수 있기 때문이다. 반면에 동기 + 블로킹은 호출하는 함수가 제어권을 잃어서 다른 작업을 수행할 수 없기 때문입니다
게임 로딩 화면과 더불어 동기 + 논블로킹의 또다른 예제로는 브라우저에서 파일을 다운로드하면 나타나는 다운로드 진행바를 들 수 가 있다.
웹브라우저에서 파일을 다운로드 할 경우 작업은 웹브라우저의 Web APIs 으로 작업을 백그라운드로 넘기고 '제어권'을 바로 반환받아 다른 작업을 수행할 수 있다. 이것이 Non Blocking 이다.
그런데 웹브라우저는 파일 다운로드 작업의 완료 여부에 관심이 있다. 즉, 다운로드 작업이 언제 끝나고 얼마나 진행되었는지 파일 다운로드 로드율을 브라우저 하단바에 표시해준다. 그리고 파일이 모두 다운로드 완료되면 사용자가 원하는 최종 작업인 파일 다운로드 작업을 수행하게 된다. 이것이 Synchronous 이다.
이때 우리는 파일을 다운로드 하면서 웹서핑을 하거나 유튜브 음악을 들을 수 있다. 제어권을 가지고 있기 때문에 브라우저가 멀티 태스킹을 하도록 적절히 코드를 구현하였기 때문에 가능한 것이다. 따라서 동기 + 논블로킹 방식은 일반적인 동기 + 블로킹 방식보다 구현에 따라 더 효율적으로 작업을 처리 할 수 있다고 볼 수 있다. (자바스크립트의 async/await 으로 상상하면 오히려 이해가 안될수가 있다. 자바의 스레드 객체로 상상하자)
Async Blocking 조합
Async Blocking 조합은 다른 작업이 진행되는 동안 자신의 작업을 멈추고 기다리는 (Blocking), 다른 작업의 결과를 바로 처리하지 않아 순서대로 작업을 수행하지 않는 (Async) 방식이다. Async-blocking 의 경우는 실무에서 잘 마주하기 쉽지 않아 다룰일이 거의 없다. 그래서 그냥 넘어가도 크게 문제는 없다.
Async Blocking vs Sync Blocking
위 사진만 보더라도 위의 Sync-blocking 수행 그림과 큰 차이가 없어 보인다. 실제로 독자분이 느낀 그대로 개념적으로 차이가 있을 뿐이지 정말로 성능적으로 차이가 없다.
보통 Async-blocking은 개발자가 비동기 논블록킹으로 처리 하려다가 실수하는 경우에 발생하거나, 자기도 모르게 블로킹 작업을 실행하는 의도치 않은 경우에 사용된다. 그래서 이 방식을 안티 패턴(anti-pattern)이라고 치부하기도 한다.
활용 예시 프로그램
다만 Async Blocking 이 실제로 적용된 실무 사례가 있긴 하다. Node.js + MySQL의 조합이 대표적인데, Node.js에서 비동기 방식으로 데이터베이스에 접근하기 때문에 Async 이지만, MySQL 데이터베이스에 접근하기 위한 MySQL 드라이버가 블로킹 방식으로 작동되기 때문이다.
- JavaScript는 비동기 방식으로 MySQL에 쿼리를 보낸다. (Async)
- MySQL은 쿼리를 처리하면서 JavaScript에게 제어권을 넘겨주지 않는다. (Blocking)
- 그러면 JavaScript는 다른 작업을 계속 수행할 수 있지만, MySQL의 결과값을 필요로 하기 때문에 MySQL이 쿼리를 완료할 때까지 기다려야 된다.
- 결국 Sync Blocking과 작업 수행에 차이가 없게 된다.
이렇게 JavaScript와 MySQL의 조합은 비동기적이면서도 블로킹되는 Async Blocking 조합이라고 할 수 있다. 이러한 오묘한 조합은 오히려 개발자에게 혼동만 일으키기 때문에 그래서 실무에서는 Node.js 서버 프로그래밍할때 아예 async/await로 동기 처리를 하는 편이다.
const mysql = require('mysql');
// connectDB 함수를 정의
async function connectDB () {
// DB 연결 정보를 담은 객체 생성
let connectionInfo = {
host: 'localhost', // DB 호스트 주소
user: 'root', // DB 유저 이름
password: '1234', // DB 비밀번호
database: 'spyncdb' // DB 이름
};
// connection 객체를 생성하고 반환
let connection = mysql.createConnection(connectionInfo);
return connection;
}
// mysql 라이브러리를 사용하는 경우
async function userHandler (username, displayName, profilePicture, email) {
// DB에 연결
connection = await connectDB()
// DB를 선택
await connection.query ('USE spyncdb;');
// 쿼리를 실행하고 결과를 받음
let result = await connection.query ('SELECT * FROM users WHERE username = ?', [username]);
}
// userHandler 함수를 호출하고 user_id 값을 얻으려고 함
let user_id = await userHandler (aUser.username, aUser.displayName, aUser.profilePicture, aUser.email); // userHandler 함수가 비동기 작업을 완료할 때까지 기다림
console.log (user_id); // user_id 출력
# 참고자료
https://medium.com/from-the-scratch/wtf-is-synchronous-and-asynchronous-1a75afd039df
https://notes.arkalim.org/notes/programming/asynchronous%20programming/
https://www.youtube.com/watch?v=oEIoqGd-Sns
https://joooing.tistory.com/entry/%EB%8F%99%EA%B8%B0%EB%B9%84%EB%8F%99%EA%B8%B0-%EB%B8%94%EB%A1%9C%ED%82%B9%EB%85%BC%EB%B8%94%EB%A1%9C%ED%82%B9
https://velog.io/@ellyheetov/Asynchoronous-VS-Synchoronous-1
https://velog.io/@nittre/%EB%B8%94%EB%A1%9C%ED%82%B9-Vs.-%EB%85%BC%EB%B8%94%EB%A1%9C%ED%82%B9-%EB%8F%99%EA%B8%B0-Vs.-%EB%B9%84%EB%8F%99%EA%B8%B0
https://ttl-blog.tistory.com/782
이 글이 좋으셨다면 구독 & 좋아요
여러분의 구독과 좋아요는
저자에게 큰 힘이 됩니다.