85 거침없는 자바스크립트 1회차

이누의 벨로그·2022년 3월 10일
0

이 글은 코드스피츠 유튜브 85 거침없는 자바스크립트 를 토대로 작성된 것입니다.이 강의에서는 앞으로 자바스크립트에 새롭게 추가된 async generator 의 개념을 학습하기 이전에 앞서 자바스크립트의 개발의 전반적인 모습과 학습의 근간이 되는 지식들을 다룹니다. 앞으로 보다 발전된 내용을 다루기 전에 개념을 학습하는 강의라고 보시면 될 것 같습니다.

85

코드스피츠 85 거침없는 자바스크립트 - 1회차

현대의 자바스크립트란 어떤 언어인가? Modern Javascript살아움직이는 언어이다. 우리가 자바스크립트를 어떻게 공부해야할지 , 어떻게 대해야할지 알기 힘들다. 게다가 현대의 개발은 대부분 프레임워크에 의존하기 때문에, 많은 개발자들이 학습의 우선순위를 프레임워크에 둬야 하는지, 자바스크립트에 둬야 하는지 고민한다. 하지만 결국 Framework을 깊이 있게 사용하려면 다시 자바스크립트로 돌아와야 한다.

이번 강의에서 우리는 자바스크립트 언어 전반에 걸쳐서 중요한 개념들에 대해서 학습해 볼 것이다.

우선 현대 자바스크립트 개발의 파이프라인을 살펴보자.

자바스크립트 개발을 얘기할 때, 자바스크립트 코드를 그대로 브라우저에 올리는 시대는 지났다. 당신이 프론트엔드 개발자라면, 코드를 작성한 뒤 Transpiler를 사용해 작성한 코드를 자바스크립트로 번역하게 된다. Transpiler는 자바스크립트의 서로 다른 버전을 변환해주거나, 혹은 코드 자체가 자바스크립트가 아닌 것을 자바스크립트로 변환해준다. Compiler 는 코드를 기계어로 변환해준다면, Transpiler 는 서로 다른 코드를 변환해준다고 할 수 있다. 이렇게 환원된 자바스크립트 코드는 다시 Packaging이라는 일종의 최적화 과정을 거친다 그 후 자동화 모듈인 CI로 이를 테스트하거나, 서버에서 여러 빌드나 스프링 통합과정을 거친 뒤, 서비스에 올라가게 된다.(Deploy). 현대 개발에서, 이렇게 많은 과정을 거쳐 서비스에 배포된 결과물과 내가 처음에 작성한 코드의 하늘과 땅만큼 큰 차이가 난다. 따라서 서비스에서 이미 배포된 코드를 디버깅한다는 것은 매우 어려운 일이다. 하지만 이렇게 많은 과정을 거치면서, 내가 작성한 코드를 네이티브 환경에 적합한 코드로 알아서 바꿔주기 때문에, 우리는 자바스크립트의 호환성에 신경쓰지 않고 우리가 원하는 버전으로 코드를 기술할 수 있다. 코드의 고도화에만 집중할 수 있게 된 것이다.

자바스크립트는 여러 언어와 버전으로 작성할 수 있다. ES3.1 부터 5, 6까지 여러 버전 뿐만 아니라, 슈퍼셋 언어인 타입스크립트, 혹은 Kotlin이나 Dart로도 자바스크립트 코드를 만들 수 있다. 안타깝지만 과거의 유산이 되어버린 Coffeescript 또한 가능하다. 이 언어들은 자바스크립트와 완전히 다른 언어들이지만, Transpiler 를 통해 자바스크립트로 번역이 된다. 언어별로 각각 컴파일러가 존재하는 것처럼 Transpiler 도 서로 다르다. (tsc, kotlinc 등) . 이렇게 Transpiler로 환원된 자바스크립트를 다른 버전으로 바꿔주거나 보다 개선해주는 유명한 Transpiler 가 있는데 바로 Babel 이다. Babel은 다른 Transpiler와는 달리, 다른 언어의 Transpiler로 번역된 자바스크립트를 다시 다른 버전 또는 다른 문법으로 변환시켜주는 역할을 한다. 이렇게 2번의 트랜스파일링을 하고 Packaging을 거치는데, 여러 다양한 기능을 가진 툴들 중에서도 표준으로는 Webpack이 사용되고 있다.

이러한 파이프라인을 소개한 이유는, 우리가 이러한 파이프라인의 인프라를 사용하여 언어에 구애받지 않고 원하는 언어를 사용할 수 있다는 점을 분명하게 하기 위해서이다. 우리는 이 강의에서는 이러한 환경에 대해 더 이상 다루지 않을 것이다.

강의에서 사용할 언어는 가장 최신(강의 촬영 날짜 기준) 버전인 ES2020이다. ES6는 2015년에 제안된 표준이며, ECMAScript는 매년 상반기에 차기 언어의 버전을 정하기 때문에 강의 기준(2019년 후반기)으로 이미 ES2020이 제정되어 있다. 물론 표준은 ES2019이지만, 이 강의의 목표는 최신 자바스크립트의 문법과 이론에 익숙해지는 것을 포함하기 때문에 가장 최신언어 버전을 사용할 것을 전제로 한다. ES6와 ES2020은 매우 다르다. 최신 버전의 언어를 사용한다는 것은 이전에 사용했던 코드의 구문들의 매우 많은 부분들이 무력화되거나 의미가 없어진다는 것을 뜻하기 때문에, 새로운 버전을 배운다는 것은 자바스크립트를 새롭게 다시 배운다고 생각해도 무방하다.

자바스크립트는 C#이후 정식버전으로 가장 빨리 그리고 많이 업데이트가 일어나는 언어이다. 자바스크립트의 버전은 연도로 포기하기도 하며, ES11과 ES2020는 같은 버전이다. ECMAScript 위원회는 ES6 이후 급격한 변화를 지양하고 점진적인 버전업을 지향한다는 청사진을 제시했으며, 이를 위해 새롭게 반영될 내용에 대해서 Stage0으로 제안을 하고, 위원회가 이를 Stage4로 정식반영할 때까지 단계별로 승격하는 형식을 취하고 있다.현재 제안 중인 내용의 상태는 이 레포에서 확인할 수 있다. https://github.com/tc39/proposals. Stage 3단계에 있는 제안들도 극히 일부만이 정식반영이 되는 편이다.

대의명분을 제외하고 그 이면에 있는 진실을 바라볼 시간이다. EMAScript 마이크로소프트가 IE로 자바스크립트를 좌지우지할 것을 걱정한 나머지 단체들이 서로 뭉쳐 표준위원회에 상정하여 MS의 독재를 막기 위해 상정된 것으로 부터 출발했다. 넷스케이프의 몰락 이후, 위원회에는 MS에 대항하는 회사들이 다수 가입하였고, 많은 IT회사들이 여기 포함되어 있다. 커뮤니티에는 누구나 참여할 수 있지만, 로열티 멤버가 되려면 기부금을 내야 한다. 결국 자본의 논리가 통용되는 곳이다. 이를 알 수 있는 것은 Stage0의 제안 조건이다. Stage0 또는 1을 tc39에 제안하려면 반드시 tc39 champion을 동반하여 제안해야 하는데, 이는 오픈소스의 일반적인 방식이다. 하지만 tc39의 챔피언들은 대부분 위원회 소속 회사의 직원들로 이루어져 있다. 보통은 IT회사의 브라우저 개발자들이 챔피언을 맡는다. 의도는 이러한 챔피언들을 통해서 발제가 이루어지도록 하는 것이었게지만 실제로는 대부분의 발제자가 챔피언들이다. 따라서 위원회의 로열티 멤버들인 각 회사들의 의도에 따라 발제가 이루어지게 되고, 챔피언들의 대부분이 (70% 이상) 직간접적으로 구글과 연결된 직원들이기 때문에 구글에 의도에 따라서 발제가 이루어지는 것이 현실이다. 이와 똑같은 일들이 이미 WC3에서도 일어나고 있는데, 구글은 웹표준을 제안만하고 표준이 반영되기도 전에 크롬에 먼저 제안들을 반영시키고 있다. 마찬가지로 tc39 위원회에서 구글 개발자들이 발제한 내용들은 정식반영이 되기도 전에 이미 크롬에 반영되어버린다. 결국 크롬에 구글 개발자들의 제안부터 반영하고 다른 회사의 제안들을 보다 천천히 반영시킴으로써 크롬의 시장점유율을 무기처럼 사용하고 있다. 대표적으로, ES6부터 표준스펙이었던 꼬리물기 재귀 최적화는 아직도 크롬에 반영되고 있지 않다. 꼬리물기 최적화를 발제한 챔피언은 애플 개발자로, 애플의 사파리 브라우저만이 현재 유일하게 꼬리물기 재귀 최적화를 반영하고 있다. 결국 각 회사들은 자사의 브라우저에서 해당 제안을 구현함으로써 자사의 서비스를 개선하거나 유리한 결과를 가져올 수 있는 제안들을 선별적으로 채택하고 있는 것이다. 참고로 구글은 houdini이라는 CSS와 자바스크립트와의 연동 스펙도 W3C에서 표준으로 채택되지 않음에도 크롬에 반영하고 있다. 따라서 자바스크립트 개발자라면 최신 언어 스펙에 대해 공부할 때 크롬 업데이트 사이트를 배제할 수 없다. 우리는 IE를 포함하여 모든 브라우저를 지원하던지 / 아니면 크롬만 지원하던지의 두가지로 양분화된 시대적 환경에서 개발하고 있기 때문에, 최신 언어를 사용하기 위해서는 크롬의 업데이트를 확인할 수 밖에 없는 것이 슬픈 현실이다.

그럼 이제 ES6부터 언어의 변화들을 살펴보자. 수업에 중요한 부분들만 간략하게 짚고 넘어갈 것이다.

  1. Class와 Object Literal의 도입
  2. Arrow Function 도입 ( 순수함수를 지향하는 가벼운 객체 생성)
  3. Iterator와 Iterator를 생성하는 Generator 및 코루틴 시스템, 외부에서 사용하게 해주는 for of의 도입
  4. var를 배제하는 순차적인 컨텍스트를 만들어주는 const, let
  5. 문법상의 편의를 위한 destructuring, rest, spread
  6. undefined의 재정의
  7. template string 도입
  8. 그동안 약점으로 지적받았던 빈약한 라이브러리 셋을 보완하는 여러 내장객체의 도입 (Symbol, Promise, Map, Set, WeakMap, WeakSet, Proxy, Reflect)

이 중 강의에서는 Class와 Iterator/ Generator에 대해 집중적으로 다뤄볼 것이다.

Class는 설탕문법(Syntatic Sugar)라는 오해가 공공연하게 퍼져있다. 수많은 문서와 글들에서 이러한 표현을 보았을 것이다. 하지만 ES6의 Class는 절대로 ES5를 흉내낼 수 없다. 단적으로 ES5에서 다음과 같이 함수나 배열을 상속받은 클래스를 만드는 것은 불가능하다.

new (class extends Function{
})()
new (class extends Array{
})()

하지만 ES6는 가능한데, 그 이유는 객체를 생성하는 방식히 완전히 달라졌기 때문이다. ES6 이전에 함수를 이용한 프로토타입에서 객체를 만드는 방법은 가장 마지막에 Object 객체를 생성한 후에 _proto 체인으로 상위객체를 연결하는 것이었다. 이 방법으로는 Object의 프로토타입이 Array나 Function이 될 수 없기 때문에 상속받는 것이 불가능하다.

하지만 class 구문을 사용하면 체이닝 되어있는 생성자를 끝까지 탐색하여 가장 상위의 생성자를 생성하여 지금 생성한 객체에 붙여준다. 만드는 방향이 아예 다르다. Array를 상속받을 수 있는 이유는 우리의 생성자로부터 객체를 만드는 것이 아니라 체이닝된 생성자를 따라서 가장 상위에 있는 생성자인 배열 객체를 생성한 뒤 그것을 꾸미기 때문이다. 따라서 우리는 함수든, 배열이든, 정규식이든 관계없이 상속받은 객체를 만들 수 있다. 게다가 class 구문은 정의하는 타이밍과 사용하는 타이밍이 구분되어 지기 때문에 정의하는 순간에 확정되는 스펙이 존재한다. super의 바인딩 같은 경우가 클래스의 정의와 함께 바인딩이 확정되는 경우이다. 따라서 차후에 프로토타입을 변경한다 하더라도 정의하는 구문에 확정된 super의 바인딩 등은 변하지 않는다. 이처럼 class구문은 엄연히 다른 문법이며, 계속해서 확장되고 있다.

ES6 이후의 표준들을 살펴보자.

  • ES7 중첩된 rest 해체

const [a, ...[b, ...c]] = [1,2,3,4] (a=1, b=2, c = [3,4]

  • ES8
async/await, `shared memory`, `atomics`

자바스크립트는 웹워커를 사용함으로써 멀티스레드 환경에 적응할 수 있다. 워커 쓰레드 패턴에서는 바깥쪽 쓰레드가 별도의 작업을 한 후 기존 객체와 싱크로 문제를 일으키지 않는 새 객체를 메인쓰레드에 전달해야 하는데, 마찬가지로 메인쓰레드도 워커에 새로운 객체를 전달함으로써 쓰레드 간의 동기화 문제를 피할 수 있다. 하나의 값을 두고 여러 쓰레드가 경합하는 쓰레드 경합을 일으키지 않는 패턴이다. 따라서 웹 워커는 여러 쓰레드를 지원하면서도 싱글 쓰레드의 컨텍스트를 방해하지 않도록 워커 쓰레드 패턴을 준수하여 설계되었다. 하지만 이 방법의 치명적인 단점은 객체를 복사해야하기 때문에 객체의 용량이 커지면 부하가 매우 심하게 걸린다는 것이다. 이미지 파일같은 경우 가볍게 mb 단위가 되기 때문에 이미지 작업 같은 무거운 작업의 경우 웹 워커를 사용하기 힘들었다. 따라서 이를 해결하기 위해 메인쓰레드에서 읽은 값을 복사하는 것이 아니라 쓰레드 간에 share하는 방식이 고안되었는데 이것이 shared memory 이다. Shared Memory는 멀티 쓰레드 간에 공유가 가능한 자료구조를 만들어 공유하게 되며 대부분 ArrayBuffer를 사용한다. 다른 추상화된 스레드 통제시스템을 갖고 있는 언어에서는 오브젝트 수준에서 락을 걸 수 있는 기능 등을 내장하고 있지만, 자바스크립트에는 이러한 고수준의 스펙이 없으므로, atomics라는 뮤텍스 락 기능을 지원함으로써 shared memory를 가능케 한다. atomics는 상당히 저수준의 lock이고 추상화가 되어 있지 않기 때문에, 프로그래머가 직접 저수준의 동시성 프로그래밍을 설계해야 하는 단점이 있다.

이러한 동시성 프로그래밍 스펙이 우리에게 아직도 익숙치 않은 이유는 2017년에 해당 스펙이 표준으로 채택되었을 때 마침 터진 스펙터/ 멜트다운 취약점에 shared memory 스펙이 그대로 노출되었기 때문이다. 따라서 당시 크롬에서 shutdown한 뒤 스펙터 패치를 거친 1년여간의 공백기간 때문에 개발자들에게 익숙하지 않기도 하다. 최근에는 각광받고 있는 웹 어셈블리가 이 스펙을 적극적으로 사용하기 때문에 이를 학습해야하는 필요성이 증가하고 있다.

  • ES9 Object 해체, async iterator

async Iterator는generator와 async await의 장점을 합쳐 비동기적인 코루틴 을 만들 수 있는 구조를 제공하며, 이 강의의 테마이다.. 비동기적인 코루틴 이라는 용어에 대해서는 앞으로 차차 알게될 것이다 현대의 크롬 브라우저는 async와 async iterator에 대한 최적화를 거쳐서 거의 일반 함수처럼 실행할 수 있는 성능을 낸다. 따라서 성능에 관한 고려없이도 범용적으로 사용할 수 있게 되었다.

  • ES10 optioncal catch

기존에 catch(e) 중괄호 구문이 e가 인식되는 렉시컬 환경을 생성해줬다면 ES10부터는 e를 사용하지 않는 경우 이러한 렉시컬 환경을 생성하지 않도록 생략할 수 있는 문법을 지원.

그 외 Stage(3) 단계에 있는 제안

BigInt - 큰 정수를 다룰 수 있음

globalThis- 명확하게 시스템상으로 this의 전역 바인딩을 제공

top level await - async 밖에서도 await를 쓸 수 있게 됨.

class field- 자바처럼 field 구문을 사용 가능

private method/field - 필드와 메서드에 #으로 private 권한 제어자 설정 가능. private은 정의시점에 확정되며 외부에 노출되거나 키로 접근이 불가능한 내부 바인딩 속성

optional chaining - 체이닝된 키중 null이 존재하면 null을 반환해줌

nullish coalescing - undefined/ null 일 경우 뒤의 값으로 평가해주는 지연 연산자

WeakRefernce - WeakSet/ WeakMap의 Weak한 참조를 자료구조수준이 아니라 개별 객체 수준에서도 가능한 내장객체.

굵은 글씨로 표시한 건 물론 구글 챔피언이 제안하여 크롬에 벌써 반영된 스펙들이다.

자바스크립트는 ES6 이후 모던 프로그래밍 언어에서 좋은 평가를 받은 스펙들을 가져와서 매년 표준 스펙으로 채택하고 있다. 여러가지 언어를 다루는 사람들은 이러한 스펙들이 C#이나 자바 등에 이미 있던 것이라는 것을 알 수 있지만, 이러한 개념들을 미리 타 언어에서 접해보지 않았던 사람들에게는 개념조차 생소한 스펙들이다. 자바스크립트를 정확하게 사용하기란 매우 어려운 일이며 점차 보다 광범위한 언어적 지식을 요구하고 있다. 대체불가능한 코어로직을 자바스크립트로 개발하는 개발자가 되느냐, 프레임워크에 종속된 단순 컴포넌트 개발자가 되느냐는 자바스크립트를 정확하게 이해하느냐에 달렸다. 거기에 매년 새롭게 새로운 개념들을 추가하고 있으니, 자바스크립트에 대한 정확한 이해는 매우 경쟁력 있는 스펙임에 틀림없다.

ECMAScript는 매년 약속대로 새로운 표준을 발표하고 있고, 브라우저 개발사들은 눈에 불을 켜고 이를 채택하고 있지만, 사실상 크롬의 독점이 성립한 이 시대에서 크롬에 반영된 제안이 가지는 효력이 다른 State3 단계의 제안들보다 훨씬 세다는 것은 부정하기 힘든, 그러나 씁쓸한 현실이다.

CS기초

  • 프로그램은 무엇인가?

우리는 보통 Language Code부터 짠다. 그 다음 시스템이 이해할 수 있는 기계어로 이를 번역한다. 우리에게 시스템이란 곧 브라우저 이기 때문에, 브라우저가 이해할 수 있는 언어로 번역해준다는 관점에서 보다 넓게 본다면 Transpiler도 이 역할을 한다고 볼 수 있다. 요점은, 기계어(Machine)의 Machine 은 기계나 CPU가 아니라 가상머신일 가능성이 높다는 것이다. 혹은 JVM이나 브라우저 등의, Machine 위에 존재하는 또다른 Machine일 수도 있다. 결국 웹 개발자에게는 브라우저가 최종적으로 번역된 언어가 작동하는 Machine이 되는 것이다.

그 다음 번역된 Machine Language를 파일로 변환하고, 이를 실행하고 싶을 때만 메모리에 적재(Load) 하고, 컴퓨터가 메모리에 적재된 프로그래밍을 실행(Run)하고 실행이 끝나면 종료(Terminate)하는 것 까지가, 프로그램의 하나의 사이클이다. 만약 우리가 코드를 수정하지 않는다면 한 번 컴파일하여 파일로 변환된 프로그램은 Load-Run-Terminate 을 반복한다. 우리는 이 사이클을 최대한 안정적으로 유지함으로써 프로그램의 안정성을 보장하려 하지만, 코드에 변경사항이 발생한다면 이를 컴파일 하여 새로운 파일을 생성해야하고, 그에 따라 앞서 수행한 사이클이 변화하게 된다. 따라서 우리의 최대 관심사는 어떻게 하면 코드를 최소한으로만 수정하여 도메인의 요구사항이나 기능적 요구사항에 대응할 수 있을지에 있다. 그러나 현실적으로, 코드의 수정 요인은 각기 다른 파일의 수만큼 존재하고 이를 인위적으로 통제할 수는 없기 때문에, 우리는 단지 하나의 변화요인에 여러 파일을 한꺼번에 수정하지 않도록 코드의 변화 요인을 하나로 규정지을 수 있을 뿐이다. 좋은 설계란 바로 이러한 코드의 변화요인이 하나로 격리되어 수정여파가 다른 코드에 영향을 미치지 않는 설계를 말한다. 이는 객체지향의 SRP(단일 책임 원칙)과 일맥상통한다고 볼 수 있다. 하나의 코드의 수정사항이 다른 여러 코드들에게까지 영향을 미친다면 그 설계는 이미 어딘가 잘못되어 있는 것이다.

프로그램을 작성할 때 도움을 주는 여러 툴들이 존재한다. 최신 IDE들은 런타임 파싱으로 마치 컴파일 된 것처럼 많은 힌트를 제공해주기도 하며, Lint 또한 사전에 에러를 감지하는데 도움을 준다. IDE의 발전속도는 특히 더 빠른 편이다. 이런 툴들을 사용함으로써 프로그램 작성시간이 매우 단축되는데, 이는 프로그램 작성 단계 이후에 발견되는 에러를 해결하는 데에 소요되는 시간이 진행단계의 지수로 증가하기 때문이다. 따라서 작성단계에서 빠르게 에러를 탐지하는 것이 중요하다.

기계어로 번역해주는 컴파일러는 속도를 개선하기 위한 많은 기법들이 존재하고, 따라서 컴파일러 설정은 프로그램 수행 속도에 큰 영향을 끼친다. 컴파일러나 트랜스파일러 자체가 우리의 코드에 대한 정보를 줄 수도 있다. 장문의 코드를 다룰 때 그 복잡성을 제어하는 것은 인간에게는 어려운 일이지만 , 트랜스파일러/컴파일러는 복잡성에 한계가 없다. 수많은 복잡성을 가지는 대량의 코드의 컨텍스트를 전부 기억하고 이에 대한 에러를 파악하는 것은 트랜스파일러/컴파일러의 도움 없이는 힘든 일이다.

런타임은 우리가 가장 궁금해하는 일이 일어나는 과정이다. 우리가 CS에서 배운 일반적인 지식을 자바스크립트에 대입해보면, 우리는 ES2020/타입스크립트 등의 언어로 코드를 작성하고, 이를 Transpiler가 번역하여 파일로 만들어진 후 이를 서버에 배포하여 브라우저에서 볼 수 있게 된다. 따라서 만들어진 파일을 실행하려면 브라우저는 이를 서버에서 로딩 한 뒤, 로딩한 자바스크립트 파일을 내부에서 파싱해야 한다. 그 뒤 마침내 파일이 실행되고, 프로그램이 자동으로 종료되는 것이 아니라 브라우저를 직접 종료해야 비로소 종료된다. 이는 다른 언어와는 다른 점으로, 자바스크립트는 프로그램이 자동으로 종료되지 않고 계속해서 실행중인 상태를 가졌다가, 브라우저가 종료되면 같이 종료된다. 이는 자바스크립트 개발자들이 프로그램 종료를 위해 메모리를 수거하는 등의 추가 작업을 하지 않게끔 하는 요인이다.

따라서 자바스크립트 개발은 다른 플랫폼 개발과는 다른 특징을 가지는데, 바로 브라우저가 파일을 로딩한 후 로딩한 파일을 파싱하는 과정을 거친다는 것이다. 따라서 이 과정을 최적화 하기 위해서 쓰는 프로그램이 바로 Webpack등의 Packaging Manager 이다. 게다가, 이 일은 처음 한번만 일어나는 것이 아니라 코드를 실행할 때 추가적인 모듈을 로딩하게 되고, 그 때마다 브라우저가 이를 로드하고 파싱하고 실행하는 작업을 반복한다. 네이티브 머신 상에서 개발하는 개발자들은 추가적인 모듈을 로딩하는 속도에 관해서 신경쓸 필요가 없을 정도로 최적화가 잘 되어있지만, 브라우저 개발자에게 모듈의 로딩은 중요한 고려사항이다. 실제로 리액트에서 라우터별로 모듈을 분리하여 Lazy하게 불러오려는 이유도 모듈의 로딩에 따른 부하를 최적화하기 위한 것이다.

앞서 컴파일된 기계어는 파일로 변환된다고 했었다. 이 파일을 실행하면 메모리에 적재되며, 이 때 항상 명령과 값으로 분리되어 메모리에 적재된다. 이렇게 로딩이 되자마자 컴퓨터는 즉시 실행단계로 넘어가며, 이것을 운영체제의 스케줄링이라고 한다. 따라서 메모리에 프로그램이 로딩에 되고 나면 프로그래머는 더 이상 이에 개입할 수 없다.

그 다음 실행 단계에서는 앞서 분리한 명령 셋(Instruction Set)을 fetch한다. 명령(Instruction)이란 CPU가 해석할 수 있는 명령을 뜻한다. 이를 fetching하고 decoding하는 것이 런타임 실행의 첫 단계이다.

명령어 fetching이란 명령어를 외부버스를 거쳐서 가져오는 것을 말하며, 외부버스를 거침으로서 여러가지 최적화 기법을 적용하는데, 예를 들 CPU 캐시 등이다. fetching 과정을거치고 나면 제어유닛이 디코더를 로딩하는데 디코더는 메모리에 적재하기 적합한 형식과 CPU가 이해하기 적합한 형식을 변환해주는 역할을 한다. 그 뒤 디코더가 명령어 변환 과정을 수행하고, 이를 연산유닛에 보낸다. 연산유닛이 수행하는 연산에는 단순 산술연산 뿐만 아니라 메모리 연산도 포함되며, 이 때 메모리 연산은 메모리를 가져오거나 적재하는 등의 메모리 조작을 말한다. 메모리 연산이 일어나게 되면 데이터 유닛에 있는 레지스터로 메모리에 있는 값을 로드한 뒤에 이를 사용해서 연산유닛에서 연산을 한다. 연산 결과를 다시 레지스터로 보내서 메모리로 이동시켜주게 되면 메모리에서 값을 읽고 다른 메모리 주소에 이를 저장하는 하나의 명령어 셋이 전부 실행된 것이다.

명령어 셋이 하나 실행되고 나면 바로 다음 명령어가 실행된다. 적재되어 있는 명령어의 실행에 프로그래머가 개입할 수 있는 방법은 없다. 이미 적재된 명령이 있다면 순차적으로 이를 모두 해소할 때까지 CPU는 동작을 멈추지 않는다. 명령어가 전부 해소 되었다면 그때서야 프로그램은 종료된다. 결국 CPU는 명령어를 가져와서 메모리를 조작하는 것이 하는 일의 전부이며, 이러한 구조를 폰 노이만 머신이라고 한다.

동기 명령(Synchronus flow)이란 이러한 명령어의 순차적 실행을 기다리는 것을 말하며 프로그램을 사용하는 기본적인 방법이다. 그것이 바로 CPU가 명령어를 해소하는 방법이기 때문이다.


런타임은 로딩 이후 프로그램 종료까지를 말한다고 했다. 런타임의 세부적인 과정을 살펴보자.

일반적으로 컴파일 언어들의 런타임은 다음과 같다. 우선 프로그램을 실행하기 위해서 언어의 핵심적 정의사항들을 로딩해서 우선적으로 실행한다. 자바스크립트의 예를 들면 Map, Set 등의 내장 라이브러리 등에 해당되며 이를essential definitions loading 이라고 한다.

그 다음 vtable mapping 과정이 실행된다. 이를 알아보기 앞서 스스로에게 던져봐야할 질문이 있다. 앞서 프로그램은 컴파일 되고 파일로 변환된 후 메모리 상에 로드 되고 나서야 실제로 컴퓨터 상의 메모리를 할당 받는다고 했었다. 그러면 대체, 우리가 만든 프로그램은 존재하지 않는 메모리에 대한 참조를 가지고 있는데 어떻게 컴파일 되는 것일까? 현대의 컴파일러들은 코드로 정의한 메모리 공간에 가상 메모리 공간으로 맵핑하여 컴파일한다. 가상 메모리 공간에서 코드의 메모리 동작을 컴파일한 뒤, 이를 진짜 메모리와 맵핑하기 위한 과정이 vtable mapping이다.(variables table mapping)

그 다음으로 사용자가 추가적으로 정의한 파일들을 로딩한다. 매번 사용자 정의가 추가될 때마다 새롭게 실행하는 사이클을 수행한다.

반면 자바스크립트의 런타임은 컴파일 언어들과는 다르다. 브라우저가 배포된 자바스크립트 코드를 로드하고 파싱하는 과정이 전부 런타임에 포함된다. 브라우저는 스크립트를 미리 파싱해놓는 것이 아니라 실행할 때 같이 파싱하게 되므로, 자바스크립트는 런타임과 컴파일 타임의 구분이 없다고 할 수 있다. 런타임에 브라우저는 다른 컴파일 언어들과 같이 핵심 정의사항을 먼저 로딩하고 , 사용자 확장 정의를 추가적으로 로딩한 후 프로그램을 실행한다. 또한 사용자 정의가 확장될 때마다 마찬가지로 실행 사이클을 반복한다.

따라서 자바스크립트의 런타임을 보다 상대적인 관점에서 보자면, 이미 로딩한 사용자 정의는 스크립트의 런타임에 수정이 더 이상 되지 않으므로 상대적으로 사용자 정의 타임과 프로그램 런타임으로 나눠볼 수 있다.


State Control

우리가 통제해야 하는 대상은 메모리에 있는 값과 명령 2가지이다. 메모리의 값은 엄밀히 말하면 값이 아닌 변수이며, 값이 계속 변하기 때문에 상태 state 라고 부른다. 매우 자주 변하는 상태를 추적하여 원하는 타이밍에 원하는 값을 얻어 연산한다는 것은 어려운 일이다.

우리가 대학에서 처음 프로그래밍을 접할 때 마주하는 벽이 있다. 바로 C언어의 포인터이다. 포인터의 문제는 컴퓨터 전반에 걸쳐서 일어나는 문제로 참조의 문제라고도 한다. 참조라는 건 직접적으로 해당 메모리를 이용하는 것이 아니라 메모리가 간접적으로 가리키는 주소에 있는 값을 이용하는 것을 말한다.

변수는 곧 메모리 주소의 별명이다. a=”TEST” 라고 변수를 선언하면, a는 00이라는 메모리 주소를 갖고 크기가 4인 값을 가지는 메모리 주소이다.

참조를 할당한다는 것은 결국 참조하는 변수의 메모리를 가져오는 것이 아니라, 그 변수 안에 들어있던 주소값을 가져오게 된다. 따라서 연쇄된 참조는 결국 가장 처음 선언한 메모리 주소에 있는 값을 가져오게 되는 것이다. 위의 a 변수를 참조하는 b변수를 만들고, b변수를 참조하는 c변수를 만들면 결국 c변수는 a변수를 가리키게 되는 것이다. 그런데 이 때 b가 다른 d라는 변수를 참조하게 만든다면 어떻게 될까? c가 d를 참조할지, a를 참조할지 헷갈리신다면 이미 참조 전파의 함정에 빠진 것이다. 참조 전파란 참조를 참조함으로써 계속해서 일어나는 일을 말한다. 만약 어떤 참조변수가 외부에 공개되는 순간, 이 참조변수를 참조하는 또다른 참조가 생성되고 이 여파를 프로그래머가 컨트롤 하는 것은 불가능하다. 따라서 우리는 참조변수를 외부에 공개되지 않도록 철저히 감추거나, 아니면 참조변수가 외부에 공개되어 전파되기 전에 참조하던 그 상태를 절대 변경하지 말아야 한다. 따라서 참조변수가 외부에 공개될수록 프로그래밍적으로 제약이 많이 걸리게 된다.

위와 같은 직접참조의 참조전파 문제를 피하기 위해서는 간접참조를 사용해야 한다. 직접적으로 메모리를 참조하는 대신, 객체를 통해서 간접적으로 참조를 한번 쿠셔닝한다고 볼 수 있다. 예를 들어 const b = {target:&a}

라는 코드가 있다고 해보자. 자바스크립트에 포인터문법은 없으니, 앞서 선언한 const a = “TEST” 라는 변수를 가리킨다고 가정한다고 편의상 생각하자. b라는 변수는 객체 리터럴로 생성된 객체의 메모리 주소를 참조하고 있는데, 그 참조값 내부에는 target이라는 키로 a라는 변수의 참조값을 가지고 있다. 이 때 c라는 변수가 b 주소값을 참조하게 한다면, b.target을 아무리 변경해도 c.target도 같이 참조가 변경된다. 앞서서 직접참조의 경우에는 c와 b의 싱크가 깨졌지만, 간접참조를 통해서는 c와 b의 싱크를 유지할 수 있다는 것이다.

(이 부분은 어찌보면 참조를 이해하는 개발자에게는 당연한 얘기일 수 있다. 참조 전파 의 함정을 보여주기 위해 고안된 예시이니 직접참조와 간접참조가 어떻게 다른지만 이해하면 된다.)

따라서 객체를 통한 간접참조에서 [b.target](http://b.target) 같은 . 은 바로 쿠셔닝이 일어날 때마다 찍히는 것이다. 간접참조를 한단계 더 거치는 이 쿠셔닝은 런타임에 계산되며 따라서 비용이 발생한다. 메모리를 간접적으로 한번 더 연산함으로써 참조의 공개 안정성을 확보했다고 할 수 있다. 동일한 객체를 가리키는 공개된 참조가 서로 메모리가 싱크된 것이다. 결국 자료구조는 직접참조 혹은 간접참조로 귀결되며, 링크드 리스트나 객체지향의 패턴 및 추상 클래스 또한 간접참조의 원리를 활용하는 것이다. 링크드 리스트 등의 간접참조는 상태 관리를 위한 가장 강력한 무기라고 할 수 있다.

Sync Flow

앞서 Sync Flow가 적재된 명령어가 모두 해소되어 프로그램이 종료될 때까지 순차적으로 실행된다고 했었다. 그러면 if 나 goto 같이 순차적으로 실행하다가 조건에 따라 명령의 위치를 이동하여 다른 명령어를 실행하거나 다시 원래의 flow로 복귀하는 분기는 어떻게 작동할 수 있는걸까? 분기문을 사용한다고 프로그램이 자동으로 순차적으로 실행되는 것을 막을 수 있는 것은 아니다. 다만 우리는 그 시점의 메모리의 값 혹은 상태에 따라 명령어가 의도한 대로 flow를 변경하도록 미리 덫을 놓을 수 있을 뿐이다. 즉 프로그램이 실행되는 시점에 이미 메모리로 명령어가 전부 로드된 후이기 때문에 프로그램의 흐름을 제어하기 위한 분기의 조건으로 우리가 사용할 수 있는 것은 분기할 때의 메모리의 값, 혹은 상태 뿐이다.

즉 우리는 프로그램 실행의 flow를 컨트롤 하기 위해 분기문을 사용하지만, 실제로 우리가 컨트롤 할 수 있는 건 분기 당시의 메모리의 상태, 즉 condition 뿐이다. 따라서 우리가 흐름 제어에 성공하냐 성공할 수 없느냐는 메모리의 상태를 잘 관리할 수 있느냐에 달렸다. 메모리의 상태를 잘 파악할 수 있는 사고력은 노이만 머신과 비슷하게 사고할 수 있는 훈련이 필요한 부분이다.

반복문은 분기 시점 혹은 그 전으로 이동하여 다시 분기문을 반복하는 분기문의 일종이며, 분기문은 전부 필요에 의해 발생한다. 어떠한 조건이 생성된다는 건 근거가 있다는 뜻이기 때문이다(물론 장난으로 프로그램에 조건을 생성할 수도 있겠지만 일반적인 경우에는 도메인의 요구사항에 근거하여 프로그램의 조건이 생긴다). 이렇게 만들어진 분기문을 필요한 곳에 적재적소에 배치하느냐에서 바로 설계의 문제가 발생하는 것이며, 설계는 궁극적으로는 코드의 배치를 통해 DIP(의존성 역전) 과 IOC( 제어 역전)을 달성 하여 복잡한 제어문을 하나의 제어문으로 제어하는 것이 목적이다.좀 더 구체적으로 얘기하자면, 구상 객체가 추상 객체에 의존하는 의존성 역전을 달성하고, 이를 통해 하나의 제어로 추상적인 객체에 의존하는 여러 구상 객체들의 복잡한 제어를 제어할 수 있는 제어 역전을 달성하게 된다.

Sub Flow 라는 개념은 아예 main flow에서 분리된 별도의 명령어 세트로 구성된 flow를 함수 등위 단위로 구성하여 반복적으로 수행할 수 있게한 것을 말한다. 또는 Sub Routine 이라고도 한다.

따라서 명령어가 순차적으로 시행되는 Sync Flow에서 이러한 순차적 시행이 우선 시행되고 다른 명령이나 프로그램은 중간에 개입할 수 없는 것을 Blocking 이라고 한다. 이는 노이만 머신의 특징이다. 우리가 노이만 머신에서 프로그램을 작성하고 메모리에 적재하여 이를 실행하는 한, 우리는 전부 Blocking 코드만 작성하게 된다. 실제로 non-blocking 코드라는 건 존재하지 않는다.

Blocking을 줄이기 위해서 우리가 할 수 있는 방법은 뭐가 있을까? 첫째로는 당연히 Sync Flow에 적재된 명령어를 줄이는 것이 있겠다. 물론 앞서 말했듯이 필요에 의해 작성된 코드를 줄일 방법은 없기 때문에 실제로 사용할 수 있는 방법이 아니다. 두번째는 바로 다른 쓰레드에 Sync Flow를 떠넘기는 것이다. 결국 처리해야할 총 Sync Flow는 똑같으니 조삼모사 같다고 생각할 수 있겠지만 현대의 모든 운영체제는 가장 우선적으로 Blocking으로 부터 풀려나서 대기상태에 있어야할 Main Thread를 지정하고 있으므로, 메인 쓰레드가 최대한 다른 쓰레드에 Sync Flow를 넘기는 것이 유리하고 이는 개발자의 책임이기도 하다. 운영체제는 이를 통해 중앙에 최대한 제어권을 가지려 하고, 실제로 Main Thread에 blocking이 적을 수록 프로그램 실행이 더 용이하며, 메인 쓰레드 점유율이 일정 수준 이상 높아지면 강제로 종료하기도 한다. 웹에서 브라우저의 자바스크립트 타임아웃은 30초이지만 모바일에서는 타임아웃이 10초이다. OS가 이 이상 메인 스레드를 점유하는 것을 허용하지 않는 것이다.

다른 쓰레드로 Sync Flow를 넘기게 되면, 우리는 그 Flow가 언제 동작할지 아무도 모른다. 다른 스레드는 서로 다른 스케줄러를 사용하여 작동하기 때문이다. 그저 그 Flow가 순차적으로 작동할 것이라는 것밖에는 모른다. 이를 병행적 프로그래밍이라고 한다. 병행적 프로그래밍에도 안전한 프로그래밍을 하려면 그에 맞는 사고를 통해 메모리 상태를 제어해야 한다.

이렇게 다른 쓰레드에 sync flow를 넘겼다면, 해당 쓰레드의 flow가 전부 종료되면 프로그램은 어떻게 되는 것일까? 혹은, 메인 쓰레드가 다른 쓰레드보다 먼저 sync flow가 종료된다면? 자바스크립트에선 메인쓰레드의 명령어가 모두 해소되어 비어있더라도 브라우저가 종료되기 전까지 프로그램은 종료되지 않는다. 백그라운드에서 계속하여 이벤트 루프가 대기하면서 다른 쓰레드로부터 sync flow가 종료되었다는 리턴값을 받는다. 이벤트루프가 동기화명령 사이사이에 다른 쓰레드의 작업을 기다리게 되는 것이다.

Non-blocking

논 블로킹이란 Sync Flow가 납득할만한 시간 안에 종료되는 것을 말한다. 즉 이는 상대적인 개념이다. 하나의 blocking flow가 다른 쓰레드로부터 결과를 리턴받아 진행되는 2개의 flow로 끊어졌다고 햇을 때 각각의 flow의 blocking 시간이 납득할만한 시간 안이라면 non-blocking 이라고 할 수 있다. 반면, 각각의 blocking시간이 납득 못할 정도로 길다면 다른 쓰레드로 flow를 위임했다가 다시 이어갔더라도 blocking이 될 수도 있다. 즉 논 블로킹에는 절대적인 기준은 없고 단지 프로그래머가 보다 더 non-blocking 하도록 시간을 단축하려 노력할 수 있을 뿐이다.

Sync & Async

Sync는 앞서 알아본 Sync Flow와 다른 개념이다. 앞서 Sync Flow는 순차적으로 동작하는 하나의 Flow를 가리켰지만, 여기서 말하는 Sync는 앞서 알아본 main flow로 부터 분리되어 실행되는 별도의 명령어 셋인 sub flow가 , 즉시 값을 반환하는 것을 말한다. 우리가 함수를 작성하였는데 리턴값을 사용한다면 sync함수라고 할 수 있다. 반면 async 라는 값을 즉시 반환하는지 타이밍과는 상관없이, sub flow가 값을 반환할 때 다른 수단으로 값을 반환하는 것을 말한다. 어떤 함수가 즉시 Promise를 반환했다면 그 함수는 Promise 내부에 값을 래핑하며 반환하였으므로 Async함수가 되는 것이다. Promise 뿐만 아니라 우리가 원하는 값이 리턴되지 않는 모든 경우가 다 async 가 된다. 여기에는 Promise, callback 함수, 비동기 iterator 등이 포함된다.

우리가 async 형태에 약한 이유는 프로그래밍을 배울 때부터 순차적으로 sync flow로 실행되는 프로그래밍에만 익숙해져왔기 때문이다. async에서 어려운 점은 호출 결과가 즉시 값을 반환하지 않으므로 main flow가 종료되게 된다는데 있다. sub flow 를 호출할 때의 Lexical Context 내의 상태가 결과시점에는 이미 존재하지 않기 때문이다. 따라서 sub flow에 main flow의 상태를 같이 전달하여 이를 종료시점에 받아와야 한다. 따라서 우리가 yield 나 await을 사용할 때마다 컴파일러는 코드를 분리하여 각각의 sub flow로 만들어 쓰레드로 보낼 분만 아니라, 호출시점의 변수나 상태들을 sub flow에 공급하는 기능도 가지고 있다. 이렇게 상태를 유지하여 공급하는 기능을 continuation 이라고 한다. 이를 활용하는 프로그래밍 스타일을 Continuation Passing Style이라고 한다.

profile
inudevlog.com으로 이전해용

0개의 댓글