본문 바로가기

Backend/Javascript

[Javascript] - Asynchronous Programming

Asynchronous Programming (비동기 프로그래밍)

 

- 기존의 순차적 프로그래밍

 

처음에 있었던 순차적 프로그래밍은 한 번에 하나만 실행하는 방식은 계산이나 대규모 데이터 처리에 적합했습니다.

현대 프로그래밍 언어의 대부분은 순차적 프로그래밍에 그 뿌리를 두고 있습니다.

 

- 순차적 언어는 입출력을 블록 방식으로 처리합니다

 

프로그램이 파일을 읽거나 네트워크에서 데이터를 가져오려 하면, 데이터를 다 가져올 때까지 프로그램은 실행을 멈춥니다. 하지만 자바스크립트는 탄생 목적이 사용자와의 상호 작용이었기 때문에 더 나은 모델을 따르고 있어 다른 언어보다 순차적 모델에 영향을 덜 받습니다.

 

- 동시성

 

순차적 모델은 컴퓨터가 사용자와, 그리고 다른 컴퓨터와 상호 작용하면서 무너지기 시작했고, 동시에 여러 가지 일을 할 수 있는 동시성 프로그래밍이 필요했습니다.

동일한 유형의 동시성은 비슷한 많은 동작이 같은 시간에 처리될 수 있도록 해 줍니다.

서로 다른 유형의 동시성은 각각 서로 다른 책임을 지는 특별한 프로세스들의 협업을 가능하게 만듭니다.

 

- 스레드

 

스레드는 가장 오래된 동시성 기법 중 하나이며 지금도 널리 사용되고 있습니다. 메모리를 공유하며 동시에 실행되는 스레드는 순수 함수와 잘 맞습니다. (함수가 순수하지 않으면 스레드에서 멍청한 일을 벌일 수도 있습니다)

 

흔한 예시인 "읽기 - 수정 - 쓰기 경쟁" 을 보도록 하겠습니다.

 

Machine-1 Machine-2 variable
    0
variable += 1   1
  variable += 2 3

Machine-1 에선 variable 에 1을 더하고, Machine-2 에선 variable 에 2를 더하는 작업을 할 때, 해당 연산을 좀 더 저수준에서 보도록 하겠습니다.

 

Machine-1 Machine-2 variable
    0
load variable   0
add 1 load variable 0
store variable add 2 1
  store variable 2

Machine-1 과 Machin-2 둘 다  variable 이 0 일 때 그 값을 우선 읽어와 더하기를 수행하고 결과를 저장하면서 덮어쓰기가 됩니다. 대부분의 경우엔 Machine-1 과 Machine-2 가 같은 값을 읽어오지 않기 때문에 코드가 제대로 동작하는 것처럼 보일 뿐입니다.

 

이처럼 코드가 제대로 동작할 수도 있지만, 그렇지 않을 수도 있습니다.

명령어가 데이터를 처리하는 순서는 컴퓨터의 load 명령어 때문에 바뀔 수 있습니다.

여러가지 CPU 최적화 기법 등으로 인해 개발자가 작성한 코드 순서와는 다른 순서로 명령어가 실행될 수 있습니다.

 

스레드로 인해 발생하는, 잠재적인 에러는 복잡한 프로그램에서는 찾기 더 어려우며 그 대가는 더 비쌉니다.

 

- 스레드 간 경쟁으로 발생할 수 있는 위험성은 상호 배제 (mutual exclusion) 로 줄일 수 있습니다.

 

상호 배제는 메모리의 임계 구역을 잠그고, 스레드를 차단하고 , 서로 경쟁하는 코드 실행을 막는 것으로 이루어집니다.

임계 구역을 잠그는 작업은 비용이 아주 비싸며 실행이 차단된 스레드가 잠금을 해제하지 못하는 데드락 (deadlock) 이 발생하기도 합니다.

 

- 자바스크립트는 이런 식으로 스레드를 쓰지 않습니다.

 

위에서 본 문제점들은 주로 자바 언어에서 자주 볼 수 있으며, 적어도 저는 지금까지 자바스크립트 개발간에 이런 부분에 대한 고민을 해본 적은 없습니다.

자바스크립트에선 동시성을 더 나은 방법으로 구현할 수 있습니다.

 

- 비동기 프로그래밍

 

- 비동기 함수는 호출하면 즉각 반환합니다.

 

해야 하는 일을 다 끝내지 않아도 반환하고, 작업이 끝난 결과물은 콜백 함수나 메시지 전송을 통해 전달되지만 즉각 반환되는 값에는 진짜 결과 값이 없습니다.

 

- 비동기 프로그래밍은 콜백 함수와 프로세싱 루프, 이 두가지에 근간합니다.

 

- 콜백 함수는 기대하는 일이 향후에 일어날 때 호출되는 함수입니다.

 

기대하는 일이라는 것은 다음과 같습니다.

 

  • 메시지가 도착함
  • 어떤 작업이 완료됨
  • 사용자가 프로그램과 상호작용함
  • 센서가 특정 이벤트를 관측함
  • 시간이 흐름
  • 무언가가 잘못됨

콜백 함수는 어떤 활동을 표현하는 객체에 연결됩니다. 아래 예시)

 

// DOM 노드의 특정 속성에 콜백 함수를 할당
test_dom_node.onclick = callback function;

// 객체에 이벤트 등록 메서드를 호출해서 콜백 함수를 연결
test_dom_node.addEventListener('click', callback function, false);

 

- 프로세싱 루프

 

이벤트 루프 또는 메시지 루프라고도 하며 큐에서 가장 높은 우선순위를 가지는 이벤트 혹은 메시지를 가져와서 해당 이벤트나 메시지를 처리하도록 등록된 콜백 함수를 호출해줍니다. 그리고 콜백 함수가 작업을 완료하면 반환합니다.

 

그래서 콜백함수는 메모리 잠금이나 상호 배제가 필요 없습니다.

콜백 함수는 방해받지 않기 때문에 경쟁이 일어날 일도 없습니다.

콜백 함수가 끝나면 메시지를 꺼내와서 등록된 콜백 함수를 호출하고 이 과정을 반복합니다.

 

- 프로세싱 루프는 큐를 관리하며 들어오는 이벤트나 메시지를 저장합니다.

 

이런 이벤트나 메시지는 일반적으로 사용자 입력이나 네트워크, I/O, 프로세스 간 통신을 처리하는 보조 스레드를 통해서 전달됩니다. 자바스크립트 프로그램과 메인 스레드 간에 통신하는 방법이 바로 큐가 되는 것입니다.

 

- 이 모델을 통해 웹 브라우저에서 실행되는 자바스크립트 프로그램은 아주 탄력적입니다.

 

스레드를 사용하는 시스템의 경우, 한 스레드에 예외가 발생하면 그 스레드의 스택을 되감습니다. 이로 인해 연달아 다른 스레드에 문제를 일으킬 수 있습니다.

 

자바스크립트는 하나의 스레드만 사용합니다. 스레드 상태 값은 대부분 스택이 아닌 해당 함수의 클로저에 저장되어 있습니다. 그래서 동작이 계속 진행될 수 있습니다.

 

- 턴의 법칙

 

프로세싱 루프의 한 반복을 턴이라고 부르며 턴의 법칙은 비동기 모델의 규칙입니다.

 

기다리지 말라. 블록하지말라. 빨리 끝내라.

 

턴의 법칙은 프로세싱 루프에서 호출하는 콜백 함수, 콜백 함수가 직-간접적으로 호출하는 모든 함수에 적용됩니다.

함수는 절대 메인 스레드를 블록해서는 안됩니다. 예를 들어 웹 브라우저의 alert, Node.js 의 -Sync 를 붙인 함수입니다.

작업을 끝내는 데 오랜 시간이 걸리는 함수를 호출해서도 안 됩니다.

 

이를 위반하면 높은 성능을 자랑하는 비동기 시스템이 아주 낮은 성능을 보이게 될 것입니다.

턴의 법칙을 위반하면 단순히 현재 콜백을 지연하는 것 뿐 아니라 큐에 있는 모든 것을 지연시키고, 이런 지연들은 누적되어 큐에 이벤트나 메시지가 점점 더 쌓이게 됩니다. 이때는 시스템이 빠르지도 않게 됩니다.

 

- 턴의 법칙을 위반하는 함수는 수정되거나 별도의 프로세스로 격리되어야 합니다.

 

프로세스는 스레드와 비슷하지만 메모리를 공유하지 않습니다. 따라서 콜백 함수를 어떤 다른 프로세스로 격리해서 따로 작업을 하게 만들고, 작업이 끝나면 해당 프로세스가 메시지를 보내도록 비동기 동작을 만드는 것도 좋은 방법입니다.

 

- 자바스크립트의 유명한 세 가지 실수

 

서버에서의 작업시 메시지 루프는 흐름이 훨씬 복잡합니다.

메시지 수신자나 핸들러가 어떠한 작업을 처리하기 위해 다른 시스템, 다른 서버와 통신해야 할 수도 있고 이러한 상호 작용에 대한 결과는 콜백을 통해 받게 됩니다.

이 책에서는 이러한 상황에서의 세 가지 실수에 대해 얘기합니다.

 

  • 콜백 지옥 (Callback Hell)

콜백 함수에 다음 작업을 요청하기 위한 코드가 들어가 있는 경우 다음 작업을 요청하는 코드 역시 콜백 함수를 제공하는데, 그 콜백 함수에도 역시 다음 작업을 요청하는 코드가 들어있는, 이런 연결 고리가 계속 이어지는 형태입니다.

이렇게 만들어진 프로그램은 읽기도 힘들고 유지보수 하기도 어려우며 깨지기 쉽습니다.

 

  • 프로미스 (Promise)

원래의 프로미스는 안전한 분산 프로그램 개발을 지원하기 위해 만들어진 아주 똑똑한 기능입니다. 지역적인 실행 흐름을 관리하려고 만든 기능이 아닌데, 당초 의도된 새로운 패러다임의 기능들이 모두 다 사라졌습니다.

물론 프로미스를 사용하면 콜백 지옥보다는 낫습니다.

 

  • Async Await

순차적으로 실행되는 코드에 붙이면 비동기 코드로 만들어 주는, 짝을 이루는 키워드입니다. ES6 제너레이터와 비슷한데, 신뢰도를 향상시키기 위해 프로미스의 실망스러운 점을 감추고 있습니다.

Async Await 의 좋은 점이라면 옛날 패러다임과 동일한 형태로 코드를 작성해도 비동기 프로그래밍을 구현할 수 있다는 점입니다. (사실 이게 제일 큰 문제)

 

- 더 큰 문제

 

문제는 위의 부분을 무시하고 개발자들이 async, await 데코레이터를 아무데나 집어넣고 있다는 점입니다.

위에서 설명한 세 가지 실수는 대개 로직 및 실행 흐름과 아주 강력한 결합을 보입니다.

너무 많은 이질적인 활동으로 인해 낮은 응집도를 보이게 되는데, 이런 것들은 따로 분리하는 것이 좋습니다.

 

저도 이러한 부분에 대한 생각은 전혀 못한 체 async await 를 사용하고 있었습니다.

오히려 이 데코레이터가 유명한 실수 중 하나에 들어가 있어서 의아해하고 있었습니다.

이 책에서는 모듈 하나를 예시로 들며 async await 가 없이 콜백 함수를 통한 코드를 보여주고 있습니다.

해당 부분은 따로 주제를 잡아 블로그에 올리도록 하겠습니다.

 

- 잘못된 단어로 인한 혼란

 

스레드로 인해 발생하는 문제를 처음 발견한 후 최초의 상호 배제 기법인 세마포어를 만들었습니다.

세마포어는 두 개의 함수 P (임계 구역을 잠그려 시도하고 이미 잠겨있다면 P 를 호출한 함수를 차단) 와 V (잠금을 해제하고 임계 구역에 진입하기를 기다리는 다른 스레드가 진입할 수 있도록 해줌) 로 만들어졌습니다.

 

자바에는 이와 비슷한 동기화 (synchronized) 라는 것이 있습니다. synchronized 라는 키워드를 사용해 세마포어가 필요한 코드를 데코레이터할 수 있습니다.

하지만 동기화 라는 것은 동시에 무언가를 하는 것을 뜻합니다. 키워드 선택이 잘못된 것 같습니다.

 

하지만 더 잘못된 단어가 있습니다. 마이크로소프트가 C# 에서 아주 잘못된 방향으로 비동기 프로그래밍을 지원하고자 했을 때, 자바에서 잘못된 단어를 따온 다음 반대 뜻을 만들기 위해 a- 접두사를 붙여 만든 async 라는 키워드입니다.

실제로 제가 처음 async 라는 키워드를 보며 느낀 이상함은 이 때문이었던 것 같습니다.

 

출처 : 자바스크립트는 왜 그 모양일까?

 

'Backend > Javascript' 카테고리의 다른 글

[Javascript] - JSON  (0) 2021.01.23
[Javascript] - Date  (0) 2021.01.16
[Javascript] - Purity  (0) 2021.01.02
[Javascript] - this  (0) 2020.12.19
[Javascript] - Exception  (0) 2020.12.12