월요일 화요일 양일간 정말 한계를 본 것 같다. 매번 '오늘이 정말 역대급이야'라고 생각하는데, 오늘은 정말 충격적인 날이었다. 하루를 정말 열심히 살았기 때문에 더 크게 느껴지는 것 같기도 하다. 체크리스트를 전부 No로 체크하고, 새벽 3시까지 붙잡고 있었지만, 두 문제 중 한 문제도 해결하지 못했다. 그래서 오늘 하루 뭐라도 얻어가야겠다는 마음으로 왜 하필 오늘 이렇게 힘들었는지 곰곰히 생각해 보았는데, 약점을 발견 할 수 있었다.
하나는, 익숙한 영역에서 익숙하지 않은 것을 배우는 것을 매우 어려워한다. Django 기반 백엔드 엔지니어로 1년 반을 일하면서, 객체지향, 절차적, 동기적 프로그래밍에는 익숙해졌지만, 함수형, 비동기 프로그래밍은 처음 공부하는 것이라 기존 지식에 반하는 거부감을 느끼고 있다. (이전의 함수형 과제에서도 엄청난 시련을 겪었....) 이것이 극복해야 하는 약점 1번.
다른 하나는, 독해력과 고집이다. 부캠에 들어와서 계속 느끼는건데, 복잡한 요구사항을 읽고 문맥을 파악하는게 쉽지 않다. 쉽게 읽히는 과제는 당연히 이해하는 데 어려움이 없지만, 그렇지 못한 과제를 읽을 때는 여기서도 일종의 방어기제를 가지고 독해를 해나가는 것 같습다. "나는 이렇게 이해했는데 그 다음 줄은 말이 안되는데?", "뭐야 이게 뭔 말이야?" 하면서 문맥을 정확히 파악하기 보다 회피하려는 습관이 있다. 이것이 극복해야 하는 약점 2번.
더 나은 개발자가 되기 위해 위에 서술한 약점은 반드시 극복해야 하는 것을 알고있다. 다행이 오늘은 비동기 프로그래밍에 대해 이해를 쌓았고, 과제도 어느정도 해결해냈다. 다행이다. 내일을 더 힘든 과제가 기다릴 것 같아 심히 걱정된다. 이제 열흘정도 남았다. 버텨보자.
JS 에서의 비동기
javascript는 Single-Threaded, non-blocking, asynchronous, concurrent language이다. (싱글 스레드, 논 블로킹 비동기적 동적 언어) 그리고 call stack, event loop, callback queue, web api(libuv) 등의 개념이 있다.
js는 싱글스레드 언어이다. js는 다른 코드를 실행시키는 동안 Ajax 요청을 할 수 없다. setTimeout도 물론 마찬가지다. 근데 우리는 브라우저를 사용하는동안, js로 만들어진 서버를 사용하는 동안 이런 동시성(concurrency)를 경험해 왔다. 그렇다면 js는 어떻게 동시성을 경험할 수 있었을까? 이를 위해 js는 비동기성(asynchronous)을 사용한다. js가 동기적 코드를 실행할 동안, 비동기적 코드는 다른 스레드(web api 등)에서 실행된다. 그리고 비동기적 코드가 완료되면, 콜백함수가 호출되어 동기적 코드가 실행된다. 이러한 과정을 관리해주는 주체가 Event Loop이다. Event Loop의 역할은 Call Stack과 Task Queue(Callback Queue)를 관리하는 것이다.
브라우저는 그렇다고 치고 Node는 어떨까? Node.js는 C++로 작성된 런타임이고 그 내부에 V8 Engine를 가지고 있다. 그 덕분에 크롬과 같은 브라우저에서 실행하던 자바스크립트를 로컬에서 실행할 수 있다. 그런데 그 내부에는 V8 Engine 말고도 libuv 라는 라이브러리가 존재한다. (최근에 Node.js 백엔드 개발자 되기 라는 책에서 배운 내용이다.)
libUV란 C++로 작성된, Node.js가 사용하는 비동기 I/O 라이브러리다. 이는 사실 운영체제의 커널을 추상화한 Wrapping 라이브러리로 커널이 어떤 비동기 API를 지원하는지 알고있다.
다시 말해 우리가 libuv 에게 파일 읽기와 같은 비동기 작업을 요청하면 libuv는 이 작업을 커널이 지원하는지 확인한다. 만약 지원한다면 libuv가 대신 커널에게 비동기적으로 요청했다가 응답이 오면 그 응답을 우리에게 전달해준다. 만약 요청한 작업을 커널이 지원하지 않는다면 어떻게 할까? 바로 자신만의 워커 스레드가 담긴 스레드 풀을 사용한다.
위에서 서술한 바와 같이 network bound나 cpu bound등 시간이 많이 걸리는 로직을 처리할때는, js engine의 main thread에서 처리하지 않고, 별도의 thread에서 처리한다. 이때, 별도의 thread를 관리하는 것이 thread pool이다. thread pool은 thread를 미리 만들어 놓고, 필요할 때마다 thread를 가져다 쓰고, 사용이 끝나면 다시 thread pool에 반납한다. 이렇게 thread pool을 사용하면 thread를 생성하고 삭제하는 비용이 줄어들어 성능이 향상된다.
thread pool은 libuv 라이브러리가 node.js에 제공하는 기능으로 네개의 스레드를 추가적으로 제공한다. Event Loop는 무거운 작업들을 thread pool이 처리하게끔 할당한다. libuv 에게 파일 읽기와 같은 비동기 작업을 요청하면 libuv는 이 작업을 커널이 지원하는지 확인한다. 만약 지원한다면 libuv가 대신 커널에게 비동기적으로 요청했다가 응답이 오면 그 응답을 우리에게 전달해준다. 만약 요청한 작업을 커널이 지원하지 않는다면 어떻게 할까? 바로 자신만의 워커 스레드가 담긴 스레드 풀을 사용한다. (실제로 node를 실행하면 그 아래의 여러 개의 node 스레드가 존재하는 것을 확인할 수 있다.) libuv는 기본적으로 4개의 스레드를 가지는 스레드 풀을 생성한다. 물론 uv_threadpool라는 환경 변수를 설정해 최대 128개까지 스레드 개수를 늘릴 수도 있다. 만약 우리가 요청한 작업을 커널이 지원하지 않는다면 libuv는 커널을 호출하는 대신 이 스레드 풀에게 작업을 맡겨버린다.
- libuv는 운영체제의 커널을 추상화해서 비동기 API를 지원한다.
- libuv는 커널이 어떤 비동기 API를 지원하고 있는지 알고 있다.
- 만약 커널이 지원하는 비동기 작업을 libuv에게 요청하면 libuv는 대신 커널에게 이 작업을 비동기적으로 요청해준다.
- 만약 커널이 지원하지 않는 비동기 작업을 libuv에게 요청하면 livuv는 내부에 가지고있는 스레드 풀에게 이 작업을 요청해준다.