pkg로 Node.js 애플리케이션의 하나의 바이너리로 만들기
pkg는 zeit에서 만든 Node.js 바이너리 컴파일러이다. 보통 Node.js로 애플리케이션을 만들면 실행 머신에 애플리케이션에 맞는 Node.js가 설치되어 있어야 하고 npm installl 로 관련 모듈을 설치하고 npm start 나 다른 실행방식으로 애플리케이션을 실행하거나 사용해야 한다. 사용자가 다 Node.js 개발자라면 큰 문제가 아니지만 타 언어 개발자나 일반 사용자를 생각하면 편한 환경은 아니라고 할 수 있다. pkg 가 Node.js 컴파일러라는 의미는 Node.js까지 내장해서 하나의 실행 파일로 다른 인덱스 바이너리 인덱스 바이너리 의존성 없이 실행할 수 있게 만들어 준다.
이런 방식이 편하다고 느끼기 시작한 건 Go 언어로 만들어진 프로그램을 사용하면서부터다. 나 같은 경우는 HashiCorp에서 만든 Vault, Consul, Terraform 등을 사용하면 이런 방식이 편하다고 생각하게 되었다. 내 맥북에 Go 언어 환경이 없어도 바이너리 파일 하나만 다운로드 받아서 실행하면 되었고 하나의 파일로 서버와 클라이언트를 모두 사용할 수 있어서 꽤 편했다. 보통 다른 언어의 프로그램을 다운받아서 사용하려면 한두 번씩은 의존성 문제나 버전이 일치하지 않는 문제로 고생했던 터라 더 편하게 느껴진 것 같다.
pkg를 위한 환경 구성
pkg를 사용하기 위한 간단한 예제를 만들어 보자. 애플리케이션의 종류는 여기서 크게 중요하지 않으므로 Express.js의 제너레이터를 사용해서 기본 Express 앱을 만들어 보자.
바이너리를 만들기 전에 의존성을 설치되어 있어야 하므로 npm install 로 의존성을 설치한다.
pkg 를 사용하기 위해 npm install --save-dev pkg 로 설치한다. pkg 는 빌드용 모듈이므로 devDependencies 로 추가했다.
pkg 명령어를 사용하려고 package.json 의 스크립트로 추가했다. 이렇게 하면 npm run build 로 pkg . 명령어를 실행할 수 있다.(요즘은 npm으로 전역 모듈은 거의 설치하지 않는 편이다) 그리고 pkg 가 바이너리를 만들기 위한 엔트리 포인트를 지정해야 하는데 bin 프로퍼티를 이용해서 지정한다. 여기서 express 애플리케이션의 실행하는 파일, 즉 시작 파일이 ./bin/www 이므로 이 파일을 지정했다. 이 시작 파일을 기준으로 pkg 가 require() 를 추적하고 필요한 파일을 찾아서 컴파일한다.
추가로 Express 예제 같은 경우 템플릿으로 Jade를 사용하고 있다. 이 뷰 엔진 같은 경우는 require() 로 불러오는 것이 아니라 아래와 같이 사용하게 된다.
그래서 pkg 가 이 views 디렉터리를 자동으로 바이너리 파일에 포함하지 않는다. 이런 경우 바이너리 파일에 넣어줄 파일(다른 수정 없이)을 직접 지정해 주어야 하는데 package.json 에 다음과 같이 지정할 수 있다.
pkg로 바이너리 생성하기
기본 타겟 플랫폼인 Linux와 macOS, Windows를 위한 바이너리가 생성되었다.
위처럼 생성된다. 기본 express 예제는 포함된 의존성이 많지 않음에도 Node.js가 바이너리 안에 포함했기 때문에 용량이 적지는 않다. 여기서 example 이라는 이름은 package.json 의 name 에서 온 것이다.
실제 이 파일이 Linux에서 다른 의존성 없이도 실행될 수 있는지 확인해 보자. Docker로 Ubuntu에서 실행해 보려고 Dockerfile 을 다음과 인덱스 바이너리 같이 만들었다.
이 도커 이미지를 만들어서 실행하면 다음과 같이 잘 실행되는 것을 알 수 있다.
당연히 여기서는 ubuntu:18.04 도커 이미지를 사용했으므로 Node.js나 다른 환경은 전혀 설정되어 있지 않으므로 pkg 로 컴파일한 바이너리 하나만으로 Express 애플리케이션을 실행할 수 있는 걸 알 수 있다.(사실 Windows에서는 직접 테스트해보지 않았다.)
pkg 옵션
위에서는 pkg . 로 컴파일할 위치만 지정했지만 몇 가지 옵션을 지정할 수 있다.
위처럼 --out-path 를 지정하면 dist 디렉토리 하에 바이너리가 생성된다.
타겟 플랫폼을 지정하고 싶다면 --targets 옵션을 사용하면 된다. 타겟 플랫폼의 지정 방식은 node8-linux-x64 처럼 node$ 로 Node.js 버전, freebsd , linux , macos , win 의 플랫폼 이름, x64 , x86 , armv6 , armv7 로 아키텍처를 지정해 주면 된다.
직접 서버에서 실행하는 경우에는 이렇게 컴파일하는 것에 큰 이점이 없지만 배포하는 프로그램의 경우에는 사용자가 쉽게 사용할 수 있어서 충분히 장점이 된다고 생각한다. Node.js가 포함되어 용량이 큰 것은 좀 문제이지만 상황에 따라서는 그 부분을 무시할 수 있는 이점을 준다고 본다.
인덱스 바이너리
이진 탐색을 재귀함수, 반복 함수로 c++로 구현해본다. Binary Search by Recursive function in C++
기본적으로 이진 탐색은 대상을 한 번 비교를 할 때마다 나머지 반을 무시한다.
① x를 가운데 원소와 비교한다.
② x가 가운데 원소와 같을 때, 가운데 index를 반환한다.
③ 만약 x가 가운데 원소보다 클 때, x는 가운데 원소 바로 다음의 오른쪽 subarray에 있을 수 있다.
④ 그렇지 않다면, x는 가운데 값보다 더 작은 것이고, 왼쪽 subarray에서 다시 찾는다.
Binary Search by Recursive function in C++
⊙ BinarySearch 함수
-line 6: 오른쪽에 원소가 하나라도 있을 때 반복한다.
-line 8: int형 변수 mid에는 가운데 원소의 인덱스를 저장한다.
-line 11: 만약 배열의 mid번째 인덱스의 값이 찾고자 하는 값인 x와 같다면 mid를 반환한다.
-line 15: 만약 배열의 가운데 값이 x보다 크다면 왼쪽 subarray에 대해서 함수를 다시 호출해서 반복한다.
-line 19: 만약 배열의 가운데 값이 x보다 작다면 오른쪽 subarray에 대해서 함수를 다시 호출해서 반복한다.
-line 23: 오른쪽에 원소가 하나도 없으면 찾고자 하는 값이 배열 안에 없는 것이고 -1을 반환한다.
-line 28: arr이라는 array에 2, 3, 4, 10, 40의 원소가 있다고 하자
-line 29: arr 안에 있는 전체 원소의 갯수를 arr의 전체 크기를 첫 번째 원소의 크기를 나눠서 구한다.
-line 30: 찾고자 하는 값인 x를 10으로 둔다.
-line 31: result라는 int형 변수에 위에서 선언한 BinarySearch 함수에서 x를 찾은 인덱스를 저장한다.
-line 32: x가 배열에서 몇 번째 index인지 출력한다.
-line 34: 만약 결과가 -1이라면 BinarySearch 함수에서 23번째 줄에서 언급했듯이 배열 안에 찾고자 하는 값이 없는 것이다.
-line 36: 만약 결과가 -1이 아니라면 result값을 출력해서 찾고자 하는 값이 배열의 몇 번째 index에 있었는지 출력한다.
Binary Search by Iterative function in C++
⊙ BinarySearch 함수
-line 9: 배열의 시작 주소가 배열의 끝 주소보다 같거나 작을 때 반복한다.
-line 11: int형 변수 mid에는 가운데 원소의 인덱스를 저장한다.
-line 14: 만약 배열의 mid번째 인덱스의 값이 찾고자 하는 값인 x와 같다면 mid를 반환한다.
-line 18: 만약 배열의 가운데 값이 x보다 작다면 배열의 시작 인덱스를 가운데 인덱스 바로 다음으로 바꾼다.
-line 22: 만약 배열의 가운데 값이 x보다 크다면 배열의 시작 인덱스를 가운데 인덱스 바로 전으로 바꾼다.
-line 27: 만약 배열의 시작 주소가 배열의 끝 주소보다 크다면 찾고자 하는 값이 배열 안에 없는 것이고, -1을 반환한다.
-line 32: arr이라는 array에 2, 3, 4, 10, 40의 원소가 있다고 하자
-line 33: arr 안에 있는 전체 원소의 갯수를 arr의 전체 크기를 첫 번째 원소의 크기를 나눠서 구한다.
-line 34: 찾고자 하는 값인 x를 인덱스 바이너리 10으로 둔다.
-line 35: result라는 int형 변수에 위에서 선언한 BinarySearch 함수에서 x를 찾은 인덱스를 저장한다
-line 36: x가 배열에서 몇 번째 index인지 출력한다.
-line 38: 만약 결과가 -1이라면 BinarySearch 함수에서 27번째 줄에서 언급했듯이 배열 안에 찾고자 하는 값이 없는 것이다.
-line 40: 만약 결과가 -1이 아니라면 result값을 출력해서 찾고자 하는 값이 배열의 몇 번째 index에 있었는지 출력한다.
인덱스 바이너리
데이터가 담긴 리스트를 탐색하는 방법은 크게 순차 탐색(Linear Search)과 이분 탐색(Binary Search)이 존재한다.
예를 들어 < 8, 3, 1, 5, 4, 7, 9, 2, 6, 10>, 10개의 데이터가 담긴 리스트가 있다고 하자. 이 인덱스 바이너리 리스트에서 9의 위치를 찾기 위해서는 어떻게 해야 할까?
위 리스트처럼 정렬되지 않은 리스트에서 데이터를 탐색하기 위해서는 어쩔 수 없이 처음부터 끝까지 순차적으로 탐색을 진행하여 9의 위치를 찾는 방법밖에 없을 것이다.
그렇다면 처럼 정렬된 리스트가 있다고 생각해 보자. 이 리스트에서도 9의 위치를 찾기 위해서는 어떻게 해야 할까?
단순하게는 앞서 언급한 방법처럼 처음부터 순차적으로 9의 인덱스를 찾을 수 있다. 만약 1을 찾는다면 운이 좋게 한 번 만에 찾을 수 있지만, 10을 찾는 경우 리스트 전체를 탐색해야 한다. 따라서 평균적으로 O(N)의 시간 복잡도를 가진다.
이보다 효율적으로 탐색할 수 있는 방법은 없을까? 바로 이분 탐색이다. 이분 탐색은 리스트가 정렬되어 있다는 가정하에 O(log N)의 시간 인덱스 바이너리 복잡도로 탐색이 가능하게 해주는 효율적인 알고리즘이다.
이분 탐색은 우리가 영어사전에서 영어 단어를 찾을 때와 비슷한 원리라고 생각하면 이해하기 쉬울 것이다. 영어 단어를 찾을 때 우리는 처음부터 한 장씩 넘겨가며 탐색하지 않고, 우리가 원하는 영어 단어의 알파벳 순서를 기준으로 대략 어느 정도의 위치를 펼친 후 탐색할 것이다. 이와 비슷한 개념으로 이분 탐색은 주어진 리스트를 절반씩 나눠가며 원하는 데이터를 탐색하는 방법이다.
의 리스트에서 7의 위치를 이분 탐색을 통해 찾는 과정을 살펴보자.
이분 탐색은 리스트의 중간부터 시작한다. 5, 6둘 중 아무 데서나 시작해도 되지만 나는 5부터 시작하겠다.
중간부터 시작하여 mid의 값이 key보다 작으면 좌측에는 탐색할 필요가 없기 때문에 left를 mid+1로 바꿔주고, mid의 값이 key보다 크다면 우측에는 탐색할 필요가 없기 때문에 right를 mid-1로 바꿔주면서 탐색을 계속 진행한다.
만약 mid의 값이 key과 같으면 원하는 key 값을 찾았기 때문에 탐색을 종료한다. 만약 left > right라면 리스트에는 원하는 데이터가 없는 것이다.
위의 그림을 보면 7의 인덱스를 순차 탐색으로 찾을 경우 7번의 과정이 필요하지만, 이분 탐색으로 찾을 경우 4번의 과정으로 찾을 수 있다.
N이 10일때는 크게 차이가 안 느껴질 수 있지만 N이 무수히 크다면 두 탐색 알고리즘의 성능 차이는 월등하게 차이 날것이다. 이분 탐색은 매번 절반씩 나눠서 탐색하기 때문에 N이 100만이라 하여도 100만은 약 2^20이기 때문에 20번의 탐색 이내로 원하는 데이터의 위치를 찾을 수 있다.
인덱스 바이너리
이진검색은 많은 곳에서 사용되는데 의외로 Lower Bound와 Upper Bound 문제가 나오면 정확한 코드를 만들지 못해서 쉬운 풀이임에도 틀리는 경우가 많고 오류가 많이 난다. 그래서 이번 기회에 Bound에 대해서 정리 하려고 한다.
Binary Search
이진 탐색은 가장 유명한 탐색 기법이다.
아주 쉬운 내용이기 때문에 간단히만 설명 하겠다.
Target이 3이고 길이 6의 array가 있다. 3을 찾기 위해서는 $ O(N) $으로 왼쪽에서 오른쪽으로 값을 찾는 방법도 있지만 Binary Search를 하면 $ O(logN) $ 으로 처리가 가능하다.
이를 처리하기 위한 핵심은 mid를 찾는데 있다.
일반적인 mid를 찾는 기법은
여기 나오는 mid와 Target이 같다면 결과값을 return 하면 인덱스 바이너리 된다.
결과 값이 나올수 있는 Target의 index 범위는
mid인 9는 13보다 작음으로 절대 답이 될 수 없다.
mid왼쪽으로는 답이 될 수 없는 영역임으로 left의 위치를 해당 영역밖으로 옮겨준다.
5번째 index와 3번째 index 중간이 4번째 index에서 Target 값을 찾게 되었다.
Target을 좀 낮게 바꾸어 보자.
3을 찾아야 하는데 mid값인 9는 3보다 작음으로 9오른쪽은 모두 답이 될 수 없다.
right를 mid 오른쪽 영역에서 벗어나게 하고 다시 mid를 찾아서 3이라는 value를 갖는 index를 찾았다.
상기 내용을 코드로 바꾸어 보면 아래와 같다.
Binary Search Lower Bound
내가 목표하는 값이 들어갈 수 있는 가장 왼쪽 범위 index를 찾는게 문제이다.
Insertion Sort를 구현할때 자주 사용한다.
만약에 2라는 값을 Array에서 위치를 찾는다고 하면, 2보다 작은 값 바로 뒤에오는 직전 값이 목표 index가 된다.
위 그림에서는 3개의 2가 있는데 이중에서도 가장 왼쪽에 위치하는 2가 나와야 한다.
아래 그림은 5가 목표한 값인데 5가 없음으로 가장 직전의 낮은 값인 4뒤에 5의 위치가 나오게 된다.
이 그림을 보고 포인트를 몇가지 잡자면 다음과 같다.
- 답이 나올수 있는 범위는 0 ~ array length + 1 이다
즉 lower bound의 답이 array의 범위를 벗어 날 수 있다는 것이다.
최소한 되지 않아야 할것은 명확하다
그 외의 경우는 모두 답이 될수 있는 자격이 생기게 된다.
답이 절대로 되어선 안되는 범위와 답이 가능한 범위를 분리 해서 생각해야 한다.
이 것을 코드로 만들면 아래와 같다.
Binary Search Upper Bound
Target보다 큰 첫번째 위치를 찾는 것을 목표로 한다.
즉, 위 예에서 보면 5가 target일때 해당 Array에 답이 없다면 마지막 위치인 array + 1의 위치가 답이 되어야 하기 때문이다.
1. 순차 탐색
순차 탐색은 리스트 안에 있는 특정 데이터를 찾기 위해서 앞에서부터 차례대로 확인하는 방법 이다. 앞에서부터 하나씩 확인해야 하기 때문에 시간 복잡도는 O(N)이 된다.
단순 탐색 시간 복잡도
1-1. 구현하기
순차 탐색 소스를 구현하면 아래와 같다. 사람 이름 리스트 중 dongbin의 위치를 출력한다.
2. 이진 탐색(Binary Search)
이진 탐색이란 정렬된 배열 에서 타겟을 찾는 검색 알고리즘으로 탐색 범위를 절반씩 좁혀가며 데이터를 탐색 한다. 이는 이진 탐색 트리와 유사한 점이 많다. 그러나 이진 탐색 트리는 정렬된 구조를 저장하고 탐색하는 자료구조라면, 이진 탐색은 정렬된 배열에서 값을 찾는 알고리즘을 지칭한다.
이는 절반씩 좁혀서 탐색하기 때문에 시간 복잡도가 O(logN) 이다.
이진탐색 시간 복잡도
2-1. 이진 탐색 과정
이진 탐색은 시작점, 끝점, 중간점 을 이용하여 탐색 범위를 설정한다.
아래는 10개의 데이터 중에 4인 원소를 찾는 과정이다. 이진 탐색을 이용하여 총 3번의 탐색으로 원소를 찾을 수 있다.
이진 탐색 과정
2-2. 구현하기
2-2-1. 재귀 함수 구현
2-2-2. 반복문 구현
2-2-3. 이진 탐색 모듈 사용
bisect 모듈은 이진 탐색 모듈이다. 자주 사용되는 메서드는 아래와 같다. bisect 메소드는 bisect_right와 동일하다.
만약 bisect_left(a, x)와 bisect_right(a, x)가 동일한 값이 나오면 target 값이 없는 것이다.
메소드 | 설명 |
bisect_left(a, x) | 정렬된 a에 x를 삽입할 위치를 리턴한다. x가 이미 있는 경우는 x의 위치를 반환한다. |
bisect_right(a, x) | 정렬된 a에 x를 삽입할 위치를 리턴한다. x가 이미 있는 경우는 오른쪽(뒤)의 인덱스를 리턴한다. |
3. 단순 탐색과 이진 탐색 비교
탐색 종류 | 단순 탐색 | 이진 탐색 | 인덱스 바이너리
의미 | 순서대로 추측한 것 | 중간을 탐색해서 비교해서 추측하는 것 |
예시 | "lena"를 전화번호에서 찾기 위해 처음부터 찾는 경우 | "lena"를 전화번호에서 찾기 위해 중간부터 찾는 경우 |
빅오표기법 | O(n) | O(logn) |
특징 | 정렬 여부와 무관하다. | 무조건 정렬이 되어 있어야 사용 가능하다. |
4. 이진 탐색 문제 접근법
이진 탐색은 파라메트릭 서치 문제로 출제되는 경우가 많다.
파라메트릭 서치는 최적화 문제를 결정하는 문제(결정 문제: 예 혹은 아니오로 답하는 문제) 로 바꾸어 해결하는 기법이다. 예를 인덱스 바이너리 들어 특정한 조건을 만족하는 가장 큰 값을 찾으라는 최적화 문제라면 이진 탐색을 이용해 해결할 수 있다.
5. 이진 탐색 트리
이진 탐색 트리란 이진 탐색이 동작할 수 있도록 고안된 효율적인 탐색이 가능한 자료구조이다. 이진 탐색 트리 자료구조를 구현하도록 요구하는 문제는 출제 빈도가 낮다. 따라서 간단히 데이터를 조회하는 과정만 살펴본다.
5-1. 트리 자료구조
이진트리구조를 살펴보기 전에 트리 자료구조를 살펴보자. 트리 자료구조는 아래와 같은 특징을 가지고 있다.
- 부모 노드와 자식 노드 관계로 표현된다.
- 최상단 노드를 루트 노드라고 한다.
- 최하단 노드를 단말 노드라고 한다.
- 트리에서 일부를 떼어내도 트리 구조이며 리를 서브 트리라 한다.
- 트리는 파일 시스템과 같이 계층적이고 정렬된 데이터를 다루기에 적합하다.
트리 자료구조
5-2. 이진 트리 특징
- 트리의 종류로 각 노드가 최대 2개의 자식 노드를 갖는 트리를 말한다.
- 왼쪽 자식 노드 < 부모 노드 < 오른쪽 자식 노드
이진 트리 특징
0 개 댓글