동기/비동기, 블로킹/논블로킹의 차이점?

늘보·2021년 12월 20일
0

OS

목록 보기
25/25

그 동안 미흡했던 동기/비동기의 개념에 대해 보충하기 위해서 검색을 하던 도중, 동기와 비동기, 블로킹과 논블로킹이 같이 작성되어 있던 글이 많았다. 작성자가 알기로는 사실 둘이 같은 개념인 줄 알았는데.. 찾아보니 명백히 다른 개념이었다.

동기와 비동기는 FE 개발을 하는 분이시라면 대부분 알 것이라고 생각한다. Promise, async/await이 등장하게 된 이유가 비동기적으로 작동하는 코드를, 즉 순서가 보장되어 있지 않은 코드를 동기적으로 실행 순서를 보장할 수 있기 때문 아닌가?

물론 콜백 헬, 가독성 등 더 깊이 들어가면 명확한 이유가 있지만, 이 글은 Promise와 async/await이 만들어진 이유에 대해 설명하는게 아니기 때문에 넘어가자!

그럼 동기와 비동기는 어느 정도라도 알고 있는데, 블로킹과 논 블로킹은 무엇인가? OS 공부할 때 분명 봤었던 것 같은데? 어디 기억 저편에 묻혀있어 뭔지 전혀 모르겠다. 지금부터 알아보도록 하자.

동기(sync) / 비동기(async)

동기와 비동기는 작업 완료 여부(작업 완료를 기다리는지 안 기다리는지!)에 따라 나뉘게 된다. 다음 그림은 동기적/비동기적 코드의 작동을 한 눈에 볼 수 있도록 나타낸 예시다.

동기(Synchronous)

  • 현재 작업의 응답이 끝남과 동시에 다음 작업이 요청된다.

  • 함수를 호출하는 곳에서 호출되는 함수가 결과를 반환할 때까지 기다린다.

  • 작업 완료 여부를 계속해서 확인한다.

// 동기적으로 작동하는 코드의 예시
function run(a, b) {
    return a + b
}

const result = run(1, 2);

console.log("시작");
console.log("결과:", result);
console.log("끝");

/**출력 결과
 * 시작
 * 결과: 3
 * 끝
 */

비동기(Asynchronous)

  • 현재 작업의 응답이 끝나지 않은 상태에서 다음 작업이 요청된다.

  • 함수를 호출하는 곳에서 결과를 기다리지 않고, 다른 함수(callback)에서 결과를 처리한다.

  • 작업 완료 여부를 확인하지 않는다.

function run(a, b) {
    return a + b
}

let result;
setTimeout(() => {
    result = run(1, 2);
}, 1000);

console.log("시작");
console.log("결과:", result);
console.log("끝");

/**출력 결과
 * 시작
 * 결과: undefined
 * 끝
 */

블로킹(Blocking) / 논블로킹(Non-Blocking)

블로킹/논블로킹은 주로 멀티 스레딩, I/O 등에서 사용되는 개념이며, 제어권에 따라 차이가 난다.

제어권: 제어권은 자신(함수)의 코드를 실행할 권리 같은 것이다. 제어권을 가진 함수는 자신의 코드를 끝까지 실행한 후, 자신을 호출한 함수에게 돌려준다.

블로킹(Blocking)

블로킹은 A함수가 B함수를 호출하면, 제어권을 A가 호출한 B 함수에 넘겨준다. (B 함수가 제어권을 가지게 된다는 말이다!)

호출된 함수 B에서 작업이 모두 끝나면 값은 리턴하고, 다시 호출한 함수 A로 제어권이 되돌아오게 된다.

// 블로킹 예시
function run() {
    // 오래 걸리는 작업
    console.log("작업 끝");
}

console.log("시작");
run();
console.log("다음 작업");

/** 출력 결과
 * 시작
 * 작업 끝
 * 다음 작업
 */

논블로킹(Non-Blocking)

논블로킹은 A함수가 B함수를 호출해도 제어권은 그대로 자신, 즉 A가 가지고 있게 된다.

값의 리턴이 함수의 실행과 동시에 이루어지는 특징이 있다.

// 논블로킹 예시
function run() {
    // 오래 걸리는 작업
    console.log("작업 끝");
}

console.log("시작");
setTimeout(run, 0);
console.log("다음 작업");

/** 출력 결과
 * 시작
 * 다음 작업
 * 작업 끝
 */

여기까지 봤다면 동기와 비동기, 블로킹과 논블로킹을 같은 개념이라고 생각할 수도 있다. 그러나둘은 거의 유사하게 동작하지만, 별개의 개념이다. 이렇게 생각을 해보자.

  • 동기/비동기: 프로세스의 수행 순서 보장에 대한 매커니즘

  • 블로킹/논블로킹: 프로세스의 작업 완료 여부에 대한 개념

수행 순서와 작업 완료 여부는 엄연히 다른 말이다. 이것 또한 예시로 이해하는게 좋은데, 다음 그림을 보자.

분명 아직 이해가 안될 수도 있을 것이다. 작성자 또한 아직 이해가 안됐다(?) 다음 글을 보면 이해가 될 것 같기도 하다.

동기와 비동기, 블로킹과 논블로킹 비교

Evan Moon님의 글, 동기(Synchronous)는 정확히 무엇을 의미하는걸까? 에서 예시를 인용했고, 해당 글을 읽고 스스로 이해한 부분을 정리해서 이 블로그에 재작성합니다. 문제가 될 시 삭제하도록 하겠습니다.

동기 + 블로킹

개발자가 가장 흔하게 접하는 경우일 것이다.

  • 동기 방식이기 때문에 작업의 흐름도 순차적으로 진행되는 것이 보장되고,
  • 블로킹 방식이기 때문에 어떠한 작업이 진행 중일 때는 다른 작업을 동시에 진행할 수가 없다.
function employee () {
  for (let i = 1; i < 101; i++) {
    console.log(`직원: 인형 눈알 붙히기 ${i}번 수행`);
  }
}

function boss () {
  console.log('사장: 출근');
  employee();
  console.log('사장: 퇴근');
}

boss();
// 출력

사장: 출근
직원: 인형 눈알 붙히기 1번 수행
직원: 인형 눈알 붙히기 2번 수행
...
직원: 인형 눈알 붙히기 100번 수행
사장: 퇴근

외부 함수 boss는 내부 함수 employee에게 인형 눈알을 붙이는 작업을 요청했고, 이 인형 눈알 100개를 다 붙이기 전까지는 boss는 어떤 일도 하지 못하고, 퇴근도 하지 못한다. 블로킹의 특성이 드러났다고 볼 수 있다.

뿐만 아니라, employee 또한 boss가 출근하기 전에는 작업을 시작하지 않는다. 동기적인 흐름이라고 볼 수 있다.

그럼 boss도 놀고만 있을 수는 없으니 employee가 눈알을 붙이는 동안 다른 작업을 하는 방법은 없을까? 다음 예시를 보자.

동기 + 논블로킹

💡 뭐가 어찌됐건 동기라는 것은 작업들이 순차적인 흐름을 가지고 있다는 것을 의미하기 때문에 이 전제만 지켜진다면 나머지는 어떻게 지지고 볶든 간에 동기 방식이라는 것은 변하지 않기 때문이다. 그래서 동기 !== 블록킹이라고 말할 수 있는 것이다. - 출처: 동기(Synchronous)는 정확히 무엇을 의미하는걸까?

Javascript의 제너레이터(제너레이터 설명 참조 링크)를 이용하면 작업의 순서를 지키면서도(동기), 외부 함수가 다른 작업을 하도록 만들 수 있다. 다음 코드를 보자.

function* employee () {
  for (let i = 1; i < 101; i++) {
    console.log(`직원: 인형 눈알 붙히기 ${i}번 수행`);
    yield;
  }
  return;
}

function boss () {
  console.log('사장: 출근');

  const generator = employee();
  let result = {};
  let i = 1;

  while (!result.done) {
    result = generator.next();
    console.log(`사장: 유튜브 ${i}번 동영상 시청...`);
    i++
  }

  console.log('사장: 퇴근');
}

boss();
// 출력

사장: 출근
직원: 인형 깔알 붙히기 1번 수행
사장: 유튜브 1번 동영상 시청...
직원: 인형 눈알 붙히기 2번 수행
사장: 유튜브 2번 동영상 시청...
...
직원: 인형 눈알 붙히기 100번 수행
사장: 유튜브 100번 동영상 시청...
사장: 퇴근

❗ 여기서 done 프로퍼티와 next() 메서드가 무엇인지 궁금한 사람들이 있을 것이다. 제너레이터 문법이니 Generator 링크의 설명을 참조하자.

사장님(boss)은 출근 이후, 직원(employee)을 불러서 인형 눈알 붙이기 작업을 100번 시키고 그 동안 자신은 유튜브 동영상을 1번부터 100번까지 시청하는 것을 확인할 수 있다. boss 함수 또한 employee 함수가 눈알 붙이기 작업을 100번 모두 끝내기 전까지는 퇴근하지 않는다.(사장님?) - 동기적인 코드라는 의미!

이 작업은 순서대로 진행되고 있으니 동기적인 코드의 흐름이지만, boss 함수는 '유튜브 동영상을 본다'라는 다른 작업을 수행 중이므로 논블로킹 방식을 사용하고 있는 것이다.

그럼 다음으로 비동기+논블로킹 방식을 살펴보도록 하자.

비동기 + 논블로킹

비동기 + 논블로킹 조합은 위에서 보았던 동기+블로킹 조합만큼 개발자에겐 익숙한 조합이다.

  • 비동기 방식이기 때문에 boss는 작업 지시(함수 호출)만 하지, 작업이 완료되는지 여부는 신경쓰지 않는다.(return을 신경쓰지 않는다)

  • 또한 논블로킹 방식이기 때문에 bossemployee가 작업을 하는 동안 자신 또한 다른 작업을 수행할 수 있다. (이제 좋은 boss가 됐다!)

function employee (maxDollCount = 1, callback) {
  let dollCount = 0;
  const interval = setInterval(() => {
    if (dollCount > maxDollCount) {
      callback();
      clearInterval(interval);
    }
    dollCount++;
    console.log(`직원: 인형 눈알 붙히기 ${dollCount}번 수행`);
  }, 10);
}

function boss () {
  console.log('사장: 출근');
  employee(100, () => console.log('직원: 눈알 결산 보고'));
  console.log('사장: 퇴근');
}

boss();
// 출력

사장: 출근
사장: 퇴근
직원: 인형 눈알 붙히기 1번 수행
직원: 인형 눈알 붙히기 2번 수행
...
직원: 인형 눈알 붙히기 100번 수행
직원: 눈알 결산 보고

위 코드를 보면, boss 함수는 출근 이후 employee에게 눈알 100개를 붙이라는 작업 지시만 한 뒤, 바로 퇴근해버렸다.

boss 함수는 employee 함수의 작업이 언제 끝나는지는 상관 없고, 작업이 끝났는지 여부는 눈알 결산 보고 대신 처리하고 있다.

비동기+논블로킹 방식은 여러 작업을 동시에 처리할 수 있다는 부분에서 효율적이지만, 개발자가 코드의 흐름을 예측하기 어렵다는 단점이 생긴다. Javascript의 Promise나 async/await 또한 이런 흐름을 예측 가능하도록 만들기 위한 노력이다.

비동기 + 블로킹

가장 일반적이지 않은 방식이라고 한다. 순서가 보장되지 않지만, 다른 작업을 동시에 진행하지는 못한다.

얼핏 들으면 굉장히 비효율적이라고 볼 수 있다. 비동기 방식의 장점은 여러 작업을 병렬적으로 처리가 가능하다는 점인데, 프로세스가 블로킹 되어버려서 다른 작업을 처리할 수 없는 상태에 빠지게 되기 때문이다.

이러한 단점에도 불구하고 비동기+블로킹 방식을 사용하는 이유는 제가 아직 이해를 하지 못했기 때문에 작성하지 못했습니다. 추후 이해하게 된다면 재작성하겠습니다 :)

결론

이해하고, 글을 작성하는데 굉장히 오래 걸렸다.(사실상 카피가 아닌가..?) 원래 개념을 이해하는건 오래 걸리는 일이지만, 코드의 실행 순서와 다른 작업의 가능 유무를 생각하며 글을 작성하다보니 더더 오래걸린 것 같다.

다행히 이 두 개념에 대해 명확하게 설명해 준 글이 여럿 있어서, 힘들었지만 이해를 하게 됐다! 블로킹/논블로킹에 대한 개념까지 FE 개발자가 언제 내부적으로 활용할 수 있을지는 모르겠지만, 동기와 비동기가 작동하는 과정에 대한 이해는 이전보다 명확하게 머릿속에 그려지는 것 같다. 👍

참조 링크

0개의 댓글