https://www.acmicpc.net/problem/14888


백준 14888번 문제 연산자 끼워넣기 풀이이다.


N값이 크지 않으므로 dfs를 통한 완전탐색으로 문제를 해결할 수 있다.



 
#include <bits/stdc++.h>
using namespace std;

typedef long long ll;
typedef pair<ll, ll> pll;
int arr[12];
int n;

//returns (min, max) value
pll dfs(ll lval, int index, int plus, int minus, int mult, int div) {
    if (index >= n) return make_pair(lval, lval);
    ll rmin = 9876543210000LL;
    ll rmax = -9876543210000LL;
    
    ll rval = (ll)arr[index];
    if (plus > 0) {
        pll ret = dfs(lval + rval, index+1, plus-1, minus, mult, div);
        rmin = min(rmin, ret.first);
        rmax = max(rmax, ret.second);
    }
    if (minus > 0) {
        pll ret = dfs(lval - rval, index+1, plus, minus-1, mult, div);
        rmin = min(rmin, ret.first);
        rmax = max(rmax, ret.second);
    }
    if (mult > 0) {
        pll ret = dfs(lval * rval, index+1, plus, minus, mult-1, div);
        rmin = min(rmin, ret.first);
        rmax = max(rmax, ret.second);
    }
    if (div > 0) {
        pll ret = dfs(lval / rval, index+1, plus, minus, mult, div-1);
        rmin = min(rmin, ret.first);
        rmax = max(rmax, ret.second);
    }
    
    return make_pair(rmin, rmax);
}
int main() {
    
    cin >> n;
    for (int i=0; i <n; i++) {
        cin >> arr[i];
    }
    int a,b,c,d;
    cin >> a >> b >> c >> d;
    pll ans = dfs(arr[0], 1, a, b, c, d);
    cout << ans.second << '\n' << ans.first << endl;
	return 0;
}



C++


Node.js

Node.js는 간단하게 이야기 한다면, 비동기 이벤트 기반 Javascript 런타임이다. 자바스크립트를 실행시켜 어떠한 프로그램을 작성할 수 있는데, 서버를 작성하는 것이 일단은 주 목적이다. 따라서 서버사이드 자바스크립트라는 대명사로 불리기도 한다. Node.js로 웹 서버를 프로그래밍 하게 되면 프론트앤드에서 사용하는 스크립트 역시 자바스크립트이기 때문에, 백엔드와 프론트앤드 둘 다 동일한 언어로 작성을 하게 되어서 언어 학습 오버헤드가 줄어들 것을 기대할 수 있고, 언어 자체가 매우 유연하기 때문에 간단한 서버는 빠르게 작성할 수 있다.

주요 특징들

노드의 의도된 용도(Intended Purpose)

노드의 주요 특징들이 있다. 공식 사이트에 설명된 대로, 이벤트 드리븐(Event-Driven), 논 블로킹 I/O(Non-Blocking I/O), 그리고 npm을 통한 패키지 에코시스템의 제공 등이 있다.

노드는 Scalable한 네트워크 어플리케이션을 제작하기 위해 만들어졌다. Scalable하다는 것은 크기를 늘였다 줄였다 자유롭게 할 수 있다라는 의미로 보면 된다. Lightweight한 규모부터 매우 Heavy traffic을 감당할 수 있는 규모까지 아우른다고 보면 된다.


이벤트 드리븐 구조

다수의 연결 요청은 concurrent하게 처리된다. Concurrent라는 것은 한번에 여러개가 동시에 처리된다는 뜻으로 보면 된다. Parallel과 다른 점은, Parallel은 실제 물리적으로 동시에 처리가 되는 것이고, Concurrent는 물리적으로는 동시가 아니지만, 겉보기에는 동시에 처리되는 것 처럼 보인다, 즉 논리적으로 동시에 처리가 된다는 차이점이 있다.

매 연결이 이루어질 때 마다 callback이 실행되지만 만약 처리할 일이 없으면 Node는 sleep상태로 쉬고 있게 된다.


Non-Blocking I/O

이러한 방식은 최근에 가장 많이 쓰는 OS 쓰레드를 사용한 concurrency 모델과는 다르다. 쓰레드 기반 네트워크는 비교적 비효율적이고 사용하기 매우 어렵다. 게다가 노드에서는 프로세스의 데드락과 같은 상황을 고려하지 않아도 된다. 왜냐면, lock을 사용하지 않기 때문에! Node에 있는 대부분의 함수는 I/O를 직접적으로 수행하지 않기 때문에, 노드 프로세스는 절때 block되지 않는다. Block이 없기 때문에 노드로 확장성 있는 시스템을 작성하면 좋다.


이벤트 루프 구조

노드의 설계는 루비의 이벤트 머신과 파이썬의 Twisted의 영향을 받았고, 따라서 비슷한 구조를 갖는다. 노드는 거기에 이벤트 모델이 적용되었고, 런타임을 만드는데에 라이브러리로서가 아닌 이벤트 루프가 존재하게 되었다. 다른 시스템의 경우 이벤트 루프를 시작하기 위해 항상 blocking 호출이 선행된다. 일반적으로 어떠한 동작을 할 것인지의 내용은 스크립트 시작 부분의 콜백을 통해서 시작되며, 그리고 스크립트 끝 부분에는 EventMachine::run()과 같은 blocking 호출을 통해서 서버가 시작됩니다. 노드에는 start-the-event-loop 와 같은 함수 호출이 없습니다. 노드는 단순히 서버 스크립트를 실행하고 나서 이벤트 루프로 들어갑니다. 더이상 처리해야할 콜백이 없을 경우 노드는 이벤트 루프를 탈출한다. 이러한 동작 방식은 브라우저에서 자바스크립트가 실행되는 방식과 비슷하다. 이벤트 루프가 유저로 부터 숨겨져 있는 방식이다.


HTTP는 스트리밍과 짧은 latency을 염두해 두고 설계된 Node의 첫번째 모듈이다. 따라서 Node는 웹 라이브러리 또는 프레임 워크의 기반으로 적합하다.


노드는 쓰레딩을 사용하지 않도록 설계되었다고 해서 당신의 시스템의 멀티코어를 활용하지 못하는 것은 아니다. child_process.fork() API를 이용하여 자식 프로세스를 만들어서 해당 프로세스와 쉽게 통신을 할 수 있다. 같은 인터페이스를 기반으로 구축된 cluster 모듈을 통해서 프로세스간 소캣을 공유하여 여러개의 코어에 로드 밸런싱을 할 수 있도록 해준다.


관련 개념들

Blocking vs Non-Blocking

Node.js에서 blocking 호출과 non-blocking 호출의 차이점에 대한 내용이다. 여기서는 이벤트 루프와 libuv에 대하여 언급하지만, 해당 주제에 대해 선행지식이 필요하지는 않다. 독자는 기본적인 자바스크립트 언어와 Node.js 콜백 패턴에 대한 지식이 있다고 가정한다.

I/O는 주로 libuv가 지원해주는 시스템의 디스크와 네트워크와의 상호작용하는 것을 뜻한다.

Blocking

Blocking은 Nodejs에서 추가적인 자바스크립트를 실행할 때, 프로세스가 non-자바스크립트 동작이 끝날 때 까지 기다려야 하는 것을 뜻한다. 이러한 현상이 왜 일어나냐 하면은, 이벤트 루프는 blocking 연산이 일어나고 있을 때에는 자바스크립트를 실행할 수 없기 때문이다. 노드에서는 I/O와 같은 자바스크립트가 아닌 동작을 기다리는 게 아니라, CPU intensive한 동작을 실행하기 위해서 자바스크립트의 성능이 저하된 경우에는 blocking이라고 부르지 않습니다. libuv를 사용하는 Nodejs 표준 라이브러리의 Synchronous 메소드는 가장 일반적인 blocking 작업입니다. 네이티브 모듈도 blocking 메소드를 가지고 있다.

Nodejs 표준 라이브러리에서 I/O 메소드는 모두 비동기 버전을 지원합니다. 요 녀석들은 non-blocking이고, 콜백 함수를 받습니다. 그리고 일부 메소드들은 blocking 버전도 있으며, 그녀석들은 이름이 Sync로 끝난다.

Blocking 함수들은 synchronous하게, 그리고 non-blocking 함수들은 asynchronous하게 실행된다.

Concurrency와 Throughput

Nodejs에서 자바스크립트 실행은 싱글 쓰레드이다. 따라서 동시성(Concurrent)은 이벤트 루프가 다른 작업을 끝내고 자바스크립트 콜백 함수들을 실행시킬 수 있는 능력을 뜻한다. 동시성 방식으로 실행되게 하고 싶은 코드는 I/O와 같이 자바스크립트가 아닌 작업을 실행하게 해서 이벤트 루프가 다른 작업을 실행시키게 해 주어야 한다.

예를 들어서, 웹 서버로 온 요청을 처리하는데에 50ms의 시간이 걸리고, 45ms는 비동기로 처리 가능한 데이터베이스 I/O라고 하자. Non-blocking 비동기 작업을 하게 되면, 요청 마다 있는 45ms의 시간은 다른 요청을 처리하는데 사용될 수 있다. 이는 Blocking 메소드를 선택했을 때에 비해 Non-Blocking 메소드를 선택했을 때 갖는 서버 capacity의 엄청난 차이이다.

이벤트 루프는, 동시성을 위해서 추가적인 쓰레드를 만들 수 있는 다른 언어의 동시성 모델과는 다르다.

Blocking코드와 Non-Blocking 코드를 섞어 쓸 시 위험성

I/O를 처리할 때 안티패턴들이 좀 있다. 다음 예제를 보자
 
const fs = require('fs');
fs.readFile('/file.md', (err, data) => {
  if (err) throw err;
  console.log(data);
});
fs.unlinkSync('/file.md');

JavaScript

위 예제를 보면 fs.unlinkSync()는 fs.readFile()이 실행되기 전에 실행 될 것같다. 실제 file.md가 읽어지기 전에 말이다.

따라서 순서가 제대로 보장되게 하기 위해서 non-blocking으로만 작성해서 맞는 방법으로 코드를 다시 작성해 보았다.

 
const fs = require('fs');
fs.readFile('/file.md', (readFileErr, data) => {
  if (readFileErr) throw readFileErr;
  console.log(data);
  fs.unlink('/file.md', (unlinkErr) => {
    if (unlinkErr) throw unlinkErr;
  });
});

JavaScript


fs.unlink() 함수는 fs.readFile()함수의 콜백 내부에서 호출되어서, 맞는 호출 순서를 보장하게 된다.

참고

https://nodejs.org/en/about/
https://nodejs.org/en/docs/guides/blocking-vs-non-blocking/


누적합의 필요성

알고리즘 문제를 풀다보면 배열의 부분합을 빠르게 구해야 하는 경우가 있다. 

필요할 때 마다 구하게 되면 구할 때 마다 O(N)O(N)의 시간 복잡도를 갖는다.


종만북 1번 문제 페스티벌을 한번 보자.

https://algospot.com/judge/problem/read/FESTIVAL


가장 Naive한 알고리즘의 경우, 연속된 구간합을 구해서 그때그때마다 최소인지 모두 확인해 보는 방법인데, 구간합을 구하면 O(N2)O(N^2)의 시간 복잡도를 갖기 때문에 상당히 느리다.

하지만 누적합을 이용하면 연속된 구간합을 O(N)O(N)만에 구할 수 있다.


누적합의 개념

대충 아래와 같은 수열이 있다고 해 보자. 총 5개의 원소를 가지고 있다.

인덱스 번호

11

22

33

44

55

aia\scriptstyle i 

33

55

77

11

44

SiS\scriptstyle i 

33

88

1515

1616

2020


위 수열에서 연속된 구간의 합, 예를들어 a2a\scriptstyle 2부터 a4a\scriptstyle 4까지의 합을 구하려고 한다면, 3개의 원소 값을 참조해야 한다.

이러한 점을 보완하기 위해서 누적합 수열 SiS\scriptstyle i를 도입해보자. 

SiS\scriptstyle i는 a1a\scriptstyle 1부터 aia\scriptstyle i까지의 모든 원소의 합을 자신의 값으로 갖는다. 또한 S0=0S\scriptstyle 0\textstyle=0이다.

ii번째 원소부터 jj번째 원소의 구간합은 SjSi1S\scriptstyle j\textstyle - S\scriptstyle i-1으로 구할 수 있다.


누적합 수열의 성질

1. S0=0S\scriptstyle 0\textstyle=0
2. S1=a1S\scriptstyle 1\textstyle= a\scriptstyle1
3. Si=Si1+aiS\scriptstyle i\textstyle=S\scriptstyle i-1\textstyle + a\scriptstyle i

4. Sn=a1+a2+...+an1+anS\scriptstyle n\textstyle=a\scriptstyle 1\textstyle +a\scriptstyle 2\textstyle + ... + a\scriptstyle n-1\textstyle + a\scriptstyle n

5. ai+ai+1+...+aj1+aj=SjSi1a\scriptstyle i\textstyle +a\scriptstyle i+1\textstyle + ... + a\scriptstyle j-1\textstyle + a\scriptstyle j\textstyle= S\scriptstyle j\textstyle - S\scriptstyle i-1


누적합을 활용한 빠른 구간합 계산

위의 누적합 수열의 성질 중 5번째 성질을 이용해서 연속된 임의의 구간의 합을 O(1)O(1)의 시간복잡도에 구할 수 있다. 다만 유의해야 할 사항은, 코드로 옮길 때, 누적합의 값이 산술 오버플로(Arithmetic Overflow)가 나타날 수 있으므로 변수 크기를 잘 조절해야 한다.
누적합을 이용해서 페스티벌 문제를 풀어본 코드이다.

페스티벌 문제 풀이 코드


 
#include <stdio.h>
#include <stdlib.h>

void dump_arr(int* arr, int len) {
	for (int i=0; i < len; i++) {
		printf("%d ", arr[i]);
	}
	printf("\n");
}

int main() {
	int case_num, total_num, least_len, i, j, k, start, len, sum;
	int* arr, *summation;
	double min, current;
	
	scanf("%d", &case_num);
	
	for (i=0; i< case_num; i++) {
		scanf("%d %d", &total_num, &least_len);
		arr = (int*) calloc(sizeof(int), total_num);
		summation = (int*) calloc(sizeof(int), total_num + 1);
		summation[0] = 0;
		for (j=0; j< total_num; j++) {
			scanf("%d", &arr[j]);
			summation[j+1] = summation[j] + arr[j];
		}
		
		min = 9999999999;
		int start, end;
		for (start = 0; start <= total_num - least_len; start++) {
			for (end = start + least_len ; end <= total_num; end++) {
				current = (double)(summation[end] - summation[start]) / (end-start);
				if (min > current) {
					min = current;
				}
			}
		}
		
		printf("%.10f\n", min);
		
		free(arr);
		free(summation);
	}
}

C++


2차원 누적합

위에서 언급한 누적합의 개념을 2차원으로 확장시킬 수 있다. 2차원 누적합으로 해결할 수 있는 문제의 예시로는 다음 Star sky 문제가 있다.


누적합이 2차원으로 확장되면서 일차원 배열은 2차원 배열로 확장되고, 누적합 수열도 2차원 배열형태로 확장된다.

직사각형 형태의 배열에 포함되는 직사각형 구간의 모든 원소의 합을 빠르게 구할 수 있게 된다.


다음과 같이 2차원 배열 a(i,j)a(i, j)과 2차원 누적합 배열 S(i,j)S(i, j)가 있다고 가정해보자.


가장 왼쪽 위를 a(1,1)a(1, 1)라고 할 때 S(i,j)S(i,j)a(1,1)a(1,1)a(i,j)a(i,j)를 양 대각 끝 꼭지점으로 하는 직사각형 범위 면적 안의 모든 aa 원소의 합으로 정의된다.




a(2,2) a(3,3)a(2,2) ~ a(3,3)의 부분합을 구하기 위해서는 S(3,3)S(3,3)에서 시작한다. 

S(3,3)S(3,3)값에서 S(1,3)S(1,3)값을 빼고, S(3,1)S(3,1) 값을 빼면 초록색 부분의 합에서 주황색 부분의 합이 빠진 값이 된다

. 왜냐하면 S(1,3)S(1,3)값에 주황색 부분이 포함되어 있고, S(3,1)S(3,1)부분에도 주황색 부분의 값이 포함되어 있으니, 두번 빼진 셈 이다. 

따라서 다시 주황색 부분에 해당하는 S(1,1)S(1,1)를 더해주면 초록색 부분의 구간합이 된다.


(sx,sy)(sx,sy)부터 (dx,dy)(dx,dy) 까지의 합을 Range(a,b,c,d)Range(a,b,c,d)라고 할 때 이를 S(i,j)S(i,j)로 나타내면 다음과 같다.

Range(a,b,c,d)=S(dx,dy)S(sx1,dy)S(dx,sy1)+S(sx1,sy1)Range(a,b,c,d)= S(dx,dy)- S(sx-1,dy)- S(dx,sy-1)+ S(sx-1,sy-1)


위 수식을 이용해서 직사각형 형태의 영역의 구간합을 O(1)O(1)의 시간복잡도에 구할 수 있다.

다만 구현 시 유의해야 할 점은, 정수 오버플로에 유의해야 한다. 

1차원 구간합 수열의 경우 S0=0S\scriptstyle 0\textstyle=0인데, 

2차원 구간합 수열의 경우 S(0,x)S(0,x)S(x,0)S(x,0)와 같은 형태로 나타나는 수열의 값은 모두 00이다.

따라서 해당 값에 0으로 값이 잘 들어가도록 하여 실수를 방지해야 한다.


2차원 구간합 수열을 구하는 방법은 Row 방향으로 한번 1차원 구간합을 모두 다 구한 뒤, Column 방향으로 구해진 구간합을 다시 구간합 하면 된다.

a(i,j)a(i,j)에서 Row(가로)방향으로 누적합을 구하여서 s(i,j)s(i,j) 수열이 되고, 해당 수열을 Column(세로)방향으로 누적합을 구해서 최종적인 S(i,j)S(i,j) 수열이 된다.




Star sky 문제 풀이 코드

#include <iostream> #include <vector> #include <cstdio> using namespace std; int map[101][101][11]; //x좌표 / y좌표 / 시작밝기 -> 누적합 버전 int main() { int n, q, c; //별의 개수, 별 바라보는 회수, 별의 최대밝기 cin >> n >> q >> c; for (int i=0; i < n; i++) { int a,b,c; cin >> a >> b >> c; map[a][b][c]++; } for (int s=0; s < 11; s++) { for (int x=0; x < 101; x++) { for (int y=0; y < 100; y++) { map[x][y+1][s] += map[x][y][s]; } } } for (int s=0; s < 11; s++) { for (int x=0; x < 101; x++) { for (int y=0; y < 100; y++) { map[y+1][x][s] += map[y][x][s]; } } } while(q--) { int t, x1, y1, x2, y2; cin >> t >> x1 >> y1 >> x2 >> y2; int sum=0; for (int src_bright = 0; src_bright < 11; src_bright++) { int add = (src_bright + t) % (c+1); int num = map[x2][y2][src_bright] - map[x1-1][y2][src_bright] - map[x2][y1-1][src_bright] + map[x1-1][y1-1][src_bright]; sum += add*num; } cout << sum << endl; } }
C++


복잡도 분석


누적합을 사용하는데에는 2단계의 Step이 있다.

1. 전처리(Preprocessing) : 누적합 수열 S(i)S(i) 혹은 S(i,j)S(i,j)를 구한다.

2. 계산 : 원하는 구간의 구간합을 계산한다.



전처리 단계의 경우 1차원 누적합일 경우 O(N)O(N)의 시간복잡도와 공간 복잡도를 갖는다.

2차원 누적합은 전처리 단계에서 O(N2)O(N^2)의 시간복잡도와 공간 복잡도를 갖는다.


계산 단계의 경우 차원과 관계없이 O(1)O(1)의 상수 시간복잡도를 갖는다.


누적합의 한계

누적합의 경우에는 합을 구하는 도중에 원본 수열값이 변하는 경우 누적합 수열을 다시 계산해야하는 단점이 있다. 따라서 원본 수열값이 변할 수 있는 경우에 구간 합을 빠르게 구해야 할 경우에는 세그먼트 트리를 사용해야 한다.


https://www.acmicpc.net/problem/2133

백준저지 2133번 문제 타일 채우기 문제 풀이입니다.


전형적인 dp문제로 부분문제로 나누어서 풀 수 있습니다.


저 같은 경우는 dp[len][state]로 나누었는데 len 이전까지의 타일의 모든 칸은 이미 채워졌다고 가정하고 len번째 column(열)의 타일은 state 형태로 채워져 있다고 부분문제를 정의했습니다.


새로 행은 3칸이 있으므로 총 2^3로 8가지의 상태가 있을 수 있고, 이를 비트마스크로 표현했습니다.


기존 모양에서 가장 왼쪽 칸을 모두 채우는 경우의 수를 따져보면 위와 같이 나타납니다.

위의 경우를 구현한 코드는 다음과 같습니다.



 
#include <bits/stdc++.h>
using namespace std;

int dp[31][8]; //dp[len][state]
int main() {
    int n;
    cin >> n;
    dp[0][0] = 1;
    for (int i=0; i < n; i++) {
        auto& c = dp[i];
        auto& n = dp[i+1];
        n[4] += c[0];
        n[1] += c[0];
        n[7] += c[0];
        
        n[0] += c[1];
        n[6] += c[1];
        
        n[5] += c[2];
        
        n[4] += c[3];
        
        n[0] += c[4];
        n[3] += c[4];
        
        n[2] += c[5];
        
        n[1] += c[6];
        
        n[0] += c[7];
    }
    
    cout << dp[n][0] << endl;
	return 0;
}



C++


이미 제대로 처리가 된 .ts파일들과 m3u8파일을 간단하게 뿌려주는 서버를 작성한 뒤, 엣지와 크롬 브라우저로 접속했을 때 크롬 브라우저는 재생이 되지 않고 엣지는 재생이 되었다.


여기까지 진행된 소스코드의 스냅샷 링크는 다음과 같다.


https://github.com/Einstrasse/hls-service/tree/20b3434fa3dcf62fa61e5ea31b685d871a3221ad


이때 소스코드를 다시 확인해보니, hls-server 노드 패키지의 정확한 사용법을 몰라서 직접 그 부분을 nodejs http server단에 구현을 했었다.



HLS 관련 토막글에서 대부분 브라우저에서 호환이 된다고 이야기를 들었던 것 같은데, 결국 그런 문서들은 공식 문서가 아니라서 정확도가 떨어질 수 밖에 없다고 생각하고 직접 확인해보도록 해보았다.


http://html5test.com/

위 링크에서 해당 브라우저의 html5를 얼마나 지원하는지에 대한 정보를 확인할 수 있다.

크롬 브라우저로 확인해보니, HTTP Live Streaming/HLS 지원 여부에 No라고 나왔다. 지원한다는 것은 새빨간 거짓말이었던 것이다. 역시 정확한 정보를 얻기 위해서는 공식 문서를 확인하거나 직접 확인하는 것이 빠르고 정확한 것 같다.

반면 엣지 브라우저로 접속을 하니, HLS를 지원하는 것으로 나타났다. 엣지 브라우저에서만 동작을 해서 내가 혹시 서버 설정을 잘못했는가에 대한 의문도 품어보았었지만, 일단은 아닌것으로 확인되었다.


크롬이 HLS 클라이언트를 브라우저 네이티브로 지원하지는 않지만, 아마 자바스크립트로 라이브러리가 있을 것으로 생각해서 hls client로 구글링을 해 보았다.


바로 뭔가 유명해보이는 깃허브 페이지가 나타났다.


https://github.com/video-dev/hls.js/




커밋 수도 많고, Star 달린 수도 많고 실제로 이 hls.js를 사용하는 서비스도 종류도 많고 유명한 곳도 많았다.

클라이언트 사이드에 이 hls.js를 적용하니 크롬에서도 음악 스트리밍 재생이 잘 되었다. 크롬 개발자 도구의 Network 탭을 보니, 이미 받은 ts파일을 다 재생할 때 쯤 새로운 요청을 보내는 식으로 아주 잘 동작하였다.


이 부분까지 진행한 git 커밋 스냅샷이다.


https://github.com/Einstrasse/hls-service/tree/c7861d0594e86273111f02541e0beccf7cf6d0e0


여기까지 진행된 바로는, 서버에 있는 이미 처리된 정적인 ts파일들과 m3u8파일로 스트리밍 재생이 가능하다는 것을 보였다.

이제 여기에 실시간으로 음성을 제공하는 방송자 입장에서 서버에 마이크로 녹음된 음성 정보들을 보내는 동작을 추가해야 한다.


웹 브라우저 마이크 API에 대해 구글을 해 보니 다음과 같은 글을 확인할 수 있었다.

https://www.html5rocks.com/ko/tutorials/getusermedia/intro/

https://developers.google.com/web/fundamentals/media/recording-audio/?hl=ko


두번째 글에서 도움을 많이 얻었다. 하지만 두번째 글에 있는 코드를 그대로 복사해서 적용할 때에는, 글에 나온대로 실행이 되지 않았다.


따라서 MDN에 있는 MediaRecorder API를 찾아보았다.


https://developer.mozilla.org/ko/docs/Web/API/MediaRecorder


확인해보니 MediaRecorder.start() API에서 인자로 timeslice를 넘겨주면, timeslice마다 ondataavailable 이벤트를 호출하게 된다. timeslice를 넣어주지 않으면 명시적으로 MediaRecorder를 stop해주지 않는 이상 ondataavailable 이벤트가 발생하지 않기 때문에 원하는 대로 실행되지 않았던 것이다.

https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder/start



여기까지는 클라이언트에서 마이크 API 를 통해 녹음한 음성을 가지고 있는 부분까지 구현되었다. 아직 서버에 음성 데이터를 보내거나 받거나 하는 부분은 구현이 되지 않았다.

여기까지 구현된 모습이다.

https://github.com/Einstrasse/hls-service/tree/394c7f3d33d687c91367c58f464e6f799292e40d



HLS를 이용한 라이브 라디오 방송 웹 앱 개발기 (3)

본 포스팅은 개인적으로 진행한 토이프로젝트를 진행하면서 문제를 해결한 과정등을 정리한 포스팅입니다.


웹 브라우저에서 동작하는 네이버 뮤직은 어떻게 플러그인 하나 없이 스트리밍으로 노래를 들을 수 있을까? 라는 단순한 의문으로 간단하게 조사를 해 본적이 있는데, 네이버 뮤직 PC버전은 Http Live Streaming이라는 프로토콜을 이용해서 음원 스트리밍을 한다는 것을 알게 되었다.


HLS는 기존의 HTTP 웹서버로 쉽게 구성할 수 있도록 되어있어서 웹 서버 개발 경험이 있으면 쉽게 구현할 수 있을 것으로 보였습니다. 또한 스푼이라고 하는 라디오 방송 서비스에서도 HLS를 이용해서 라이브 음성 스트리밍으로 방송을 하도록 구현되어 있어서, 한번 비슷하게 만들어보기로 결정했다.


만들려고 하는 앱의 기능적 요구사항들은 다음과 같습니다.


1. 방송을 하는 사람은 웹 브라우저로 해당 방송을 하는 페이지로 접속합니다. 그러면 브라우저에 있는 마이크 API를 통해서 마이크에 녹음된 음성 정보를 일 정주기마다 서버로 보냅니다.

2. 서버는 받은 음성 정보를 처리해서 HLS로 전송가능한 형태로 바꿉니다.

3. 방송을 청취하는 사람은 웹 브라우저로 해당 방송을 청취하는 페이지로 접속합니다. 그러면 HLS를 통해서 방송하는 사람의 음성을 실시간으로 듣습니다.


HLS(Http Live Streaming) 프로토콜에 대한 내용은 네이버 d2에 잘 정리된 글이 있어서 해당 글을 보며 공부했다.

http://d2.naver.com/helloworld/7122


개략적인 HLS의 동작 방식을 확인한 후 비슷한 프로젝트가 있는지 한번 찾아보았더니 유사한 프로젝트가 있었다.


https://github.com/mjrusso/livestreaming-js


다만 조금 걱정이 되는 것은, 사용하는 nodejs 버전이 v0.2대 버전이고 최근 커밋이 2011년도이므로 현재 2018년인 상황에서 코드가 제대로 동작할지가 의문이었습니다. git clone을 받아서 실제로 필요한 파일들을 설치해서 구동을 시도했는데, 아니나 다를까 많은 에러 투성이었습니다.


nodejs 버전업이 많이 되면서 해당 프로젝트에서 사용한 API가 이미 deprecated되서 사라진 경우도 있었고, 결정적으로 ffmpeg의 SDK를 이용해서 미디어 데이터를 처리하는 C소스코드가 포함되어 있는데, 이 C API가 맞지 않아서 컴파일이 안되었다. 2011년도 버전 ffmpeg SDK를 사용하려고 했는데, 홈페이지에서도 구할 수 없을 만큼 오래된 SDK이라서 해당 프로젝트의 아키텍쳐와 문서를 참고해서 직접 새로 만들기로 했다.

또한 실제로 빌드가 된다고 하더라도, 해당 프로젝트는 파일 업로드를 직접 해서 스트리밍을 시키는 방식이고, 내가 원하는 프로젝트는 브라우저 마이크 API로 실시간으로 녹음되는 음성을 스트리밍 하는 것으로 조금 차이가 있다.


해당 서버를 단계별로 개발해보기로 했다.


일단 네이버 뮤직에서 m3u8파일과 ts파일들을 다운로드받아서 추출한 뒤, nodejs로 만든 간단한 서버로 뿌려주는 식으로만 작성해보았다.

Nodejs 패키지중에 hls-server라는 패키지가 있어서 확인해보았다.

https://www.npmjs.com/package/hls-server


문서가 많이 친절하지는 않지만, 대략적인 사용방법 예제코드들이 있다.

HLS 서버는 http 서버의 미들웨어 형태로 삽입되는 형식으로, http 요청 처리하는 로직 이전에 먼저 프로세싱을 하게 된다.


 
var server = http.createServer(function(req, res) {
	//HTTP 요청을 처리함.
});

var hls = new HLSServer(server, {
  provider: {
    exists: function (req, callback) { // hls 미들웨어가 삽입된 서버에서 요청이 올 때마다 호출되는 함수
      callback(null, true)                 // 파일이 존재하고 스트리밍을 할 경우 호출하는 콜백
      callback(new Error("Server Error!")) // 500 error시 호출하는 콜백
      callback(null, false)                // 404 error시 호출하는 콜백
    },
    getManifestStream: function (req, callback) { // 적절한 .m3u8 파일을 리턴한다. 
      // "req" is the http request
      // "callback" must be called with error-first arguments
      callback(null, myNodeStream)
      // or
      callback(new Error("Server error!"), null)
    },
    getSegmentStream: function (req, callback) { // 적절한 .ts 파일을 리턴한다.
      callback(null, myNodeStream)
    }
  }
})

server.listen(PORT);
JavaScript


npm 홈페이지에 있는 Using In-Memory Stream이란 탭에 있는 예제코드이다. 추가적으로 http 서버를 만드는 코드도 추가되었다.


HLSServer 함수의 첫번째 인자로 준 server에 해당 미들웨어가 삽입된다.

provider 객체 내의 exists함수는 해당 서버로 오는 모든 요청에 대해서 호출이 되며, Error을 던지게 되면 그 다음으로 진행하지 않고 에러를 응답하게 된다.

exists함수에서 callback(null, true)가 호출되게 되면, 요청 url의 path중 확장자를 추출해서 판단하게 되는데, 확장자가 .m3u8이면 getManifestStream 함수를 호출해서 요청을 처리하게 되고, 확장자가 .ts 이면 getSegmentStream 함수를 호출해서 요청을 처리하게 된다.

그리고 해당 확장자 (.m3u8이나 .ts)과 일치하지 않는 요청이면, httpServer 자체의 요청 처리 로직으로 넘어가서 처리되게 된다.


hls 패키지 미들웨어가 동작하는 방식이 잘 이해가 가지 않아서 소스코드를 확인해보았었다.


github에 공개된 hls-server node 패키지 소스코드 url이다.

https://github.com/RationalCoding/hls-server/blob/master/src/index.js


 
HLSServer.prototype._middleware = function (req, res, next) {
  var self = this

  var uri = url.parse(req.url).pathname
  var relativePath = path.relative(self.path, uri)
  var filePath = path.join(self.dir, relativePath)
  var extension = path.extname(filePath)

  req.filePath = filePath

  // Gzip support
  var ae = req.headers['accept-encoding'] || ''
  req.acceptsCompression = ae.match(/\bgzip\b/)

  if (uri === '/player.html' && self.debugPlayer) {
    self._writeDebugPlayer(res, next)
    return
  }

  self.provider.exists(req, function (err, exists) {
    if (err) {
      res.statusCode = 500
      res.end()
    } else if (!exists) {
      res.statusCode = 404
      res.end()
    } else {
      switch (extension) {
        case '.m3u8':
          self._writeManifest(req, res, next)
          break
        case '.ts':
          self._writeSegment(req, res, next)
          break
        default:
          next()
          break
      }
    }
  })
}

JavaScript


self.provider.exists 부분에 보면, 파일 확장자(extension)을 기준으로 알맞는 함수를 호출하고, 해당 확장자에 해당하지 않으면 해당 서버의 원래 처리 로직으로 가도록 되어있다. 그리고 두 확장자에 둘다 해당되지 않으면, next()를 호출해서 미들웨어에서 제어권을 넘겨준다. 이러한 함수들을 이용해서 이미 세그먼팅 된 .ts파일들과 .m3u8 매니페스트 파일로 브라우저단에서 잘 재생이 되는지 확인해보았다.


주로 쓰는 크롬 브라우저와 윈도우10에 디폴트로 설치되어있는 엣지 브라우저를 이용해서 테스트를 해 보았는데, 엣지 브라우저에서는 음악이 잘 재생되었는데, 크롬 브라우저에서는 재생이 되지 않았다. 어떻게 된 것인지 확인해보기 위해 HLS와 관련된 문서들을 찾아보았다.

본 포스팅은 구글 프로젝트 제로의 멤버인 Ivan Fratric이 'So you want to work in security?'라는 제목으로 정보보안 직종에 종사하고 싶은 후배들에게 한 블로그 포스팅을 번역한 포스팅입니다.


번역 상에 오역이 있을 수 있으므로, 원문도 같이 첨부합니다.


원문 링크 : http://ifsec.blogspot.kr/2018/02/so-you-want-to-work-in-security-and-for.html?m=1


트위터를 써라



갑자기 트위터라는 SNS하라는 것이 이상한 소리처럼 들리겠지만, 많은 보안 커뮤니티가 정보를 공유하기 위해 트위터를 쓴다. 그리고 최근 연구, 취약점, PoC, 컨퍼런스 발표자료 소스같은것도 올라온다. 나는 이런것들이 어떻게 올라오는지 모르지만, 불필요하게 긴 토론이 있는 것 보다는 이런식으로 공부 자료들에 대한 링크를 공유하는 것이 더 높아 보인다. 그러므로 트위터에서 관심있는 보안 관련 자료들을 게시하는 사람들을 찾아라.



트위터 말고도, 너가 관심있을 만한 자료들을 찾을 수 있는 곳이 또 있다. r/netset고 해커뉴스이다.(비록 요녀석들은 보안과는 관계없는것들도 공유하긴하지만) 컨퍼런스 발표자료와 녹음본들도 확인해라(그런 자료들이 많다, 하지만 모두 다 좋은 자료는 아니긴 하다. 기술적인 부분에만 집중해서 봐라)


CTF를 하는 것은 배우기 좋은 방법 중 하나이다.



나는 해본적이 없지만 남에게는 추천하는 이상한 충고가 또 있지만, 내가 학습 곡선을 그린 것을 기억하는가? CTF는 다양한 어려움에 직면하기 때문에 학습을 점근적으로 하도록 해줄 수 있다.(각 문제의 점수로 난이도를 알 수 있다.) 따라서 쉬운 것 부터 시작해서 공부하게 할 수 있다. 예를 들어 미티게이션이 해제된 체 있는 익스플로잇 문제가 있는 경우가 있다. 그런 것을 익스플로잇 할 수 있는 버그와 방법이 있다는 것을 알게 되는식으로 공부할 수 있다.



CTF는 거의 매주 있으며 대부분 온라인에서 참여할 수 있다. 여기서 일정을 찾을 수 있다.(ctftime.org) 문제를 푸는 것을 실패한 경우, 문제 푼 사람들의 write-up을 확인하는 것을 잊지 말라.



CTF는 만족스러운 경험일 수 있지만, 그 이후에 실제 목표를 달성하기 위해 리얼월드 취약점을 찾으러 나가고 시도하는 것을 두려워 하지 말라. 너는 스스로 놀랄것이다.


그리고 리얼월드 취약점을 찾으러 나갔을때


실패를 두려워하지 말라, 많이하더라도


특히 최근에 취약점 분석 연구는 너를 매우 시무룩하게 할 수 도 있다. 너가 시도하는 대부분의 타겟들은 너가 원하는 대로 동작하지 않을 것이고, 너는 그걸 받아들여야 한다. 그렇다고 시도를 포기하지 마라. 이런 일은 너에게만 일어나는 것이 아니라, 나한테도 일어나고 다른 경험많은 연구원들도 똑같이 겪는 일이다. 하지만 다른사람들은 결국 성공하기 때문에 이런 일은 너에게만 일어난다고 생각하기 쉽다. 중요한 점은, 너의 아이디어가 실패했을 경우 왜 실패했는지를 파악하는 것이다.


너는 너 생각보다 똑똑하다(반대로, 다른 사람들은 너가 생각하는 것 보다 똑똑하지 않다)



다른 사람들은 "너는 개발자 보다 똑똑하지 않다"라는 조언을 주었기 때문에, 이 조언은 논란의 여지가 있을 수 있습니다. 이러한 조언은 맞는 말이고, 실제로 이미 산업에 있는 많은 사람들에게 좋은 조언입니다. 하지만, 이제 막 입문하였거나 입문하려는 사람들에게는 잘못된 조언일 수 있습니다. 해당 분야에서 해낸 것이 아무것도 없는 상태에서 똑독한 사람들이 직접 하는 일들을 보면, 자신의 능력을 의심하기 쉽다는 것입니다. 내 개인적인 예를 하나 들어보겠습니다.



지금은 이상하게 들릴 수 있지만, 내가 보안을 취미로 처음 시작했을 때 Windows에서 버그를 발견할 만큼 충분히 "l33t"가 될 수 없을 것이라 생각했습니다. 그리고 내가 우연히 윈도우 버그를 발견하기 전 까지 시도조차 하지 않았습니다. 나는 오래된 이미지 라이브러리를 퍼징하고 있었고 잠시후 크래시를 일으키는 샘플이 있었습니다. 그리고 내가 우연히 그 크래시 샘플을 윈도우에서 눌렀을때, 윈도우 익스플로러가 크래시가 났고 그것은 CVE-2008-3013이었습니다.



다른 케이스도 있다. 소프트웨어를 리뷰하는 도중에, 이러한 생각이 들 수가 있다. "와, 바보같이 개발자들은 분명 그렇게 생각했다." 이러한 일은 종종 일어날 수 있다. 이거는 그들이 멍청해서그런게 아니라 그 당시 다른 문제를 생각했기 때문입니다. 하지만 "나는 그들보다 더 똑똑하다"는 사고 방식을 통해 자신이 스스로 설정한 한계를 뛰어넘을 수는 있지만, 다른데서 겸손해지게 되는 문제를 일으킬 수 있습니다.


당신이 다른사람들, 특히 개발자들과 이야기할 때 그런 생각을 놓을 때입니다. 당신이 그들을 적으로 만나지 않고 같이 일할 사람으로 만난다면 대화하면서 즐거운 시간을 보낼 수 있을 것입니다. 이것은 당신이 듣는 모든것을 믿으라는 뜻은 아닙니다. 명심하세요, 그들은 그들이 작성한 코드의 전문가이지만 당신은 보안에서 전문가입니다.


내가 가지고 있는 기술을 세상에 보여줄 준비가 되었을때 무엇을 해야하나요?



처음에는 돈을 벌면서 무언가를 할 수 있습니다. 많은 회사들은 자신들의 제품에 있는 버그를 찾기 위해 크고 작은 버그바운티를 합니다. 구글과 페이스북, 마이크로소프트가 그렇지요.



버그바운티 상금이 없는 제품을 보더라도, 많은 사람들이 사용하는 것이므로 버그를 발견하면 당신의 기술을 뽐낼 수 있는 좋은 방법이 될 수 있습니다. 그러면 다른 사람들도 당신을 알아채기 시작할 것입니다.



불균형적으로 많은 관심을 얻겠지만, 취약점을 리포트하는 것은 커뮤니티에 기여하는 유일한 방법은 아닙니다. 유용한 도구를 만들고 방어 리서치를 하는 것도 멋진 방법입니다!


내가 더 알아야 하는게 있나요?


보안 연구원의 삶은 당신이 상상하는 것 처럼 영광스럽지 않을 수 있습니다. 컴퓨터 앞에 오랫동안 앉을 것입니다. 그래서 뭔가 괜찮은 아이디어를 얻었는데 그것이 커리어에 좋은 경로가 아닐 수 있다. 또한 꽤 지적으로 도전적이고 일상적이지 않은 일입니다. 이것은 꽤나 보람이 있을 수 있지만 정신적으로 힘들 수 있다는 것을 의미합니다.

본 포스팅은 구글 프로젝트 제로의 멤버인 Ivan Fratric이 'So you want to work in security?'라는 제목으로 정보보안 직종에 종사하고 싶은 후배들에게 조언한 블로그 포스팅을 번역한 포스팅입니다.


번역 상에 오역이 있을 수 있으므로, 원문도 같이 첨부합니다.


원문 링크 : http://ifsec.blogspot.kr/2018/02/so-you-want-to-work-in-security-and-for.html?m=1


그래서 보안쪽 일을 하고 싶다고?


많은 사람들(내 구글 직장동료인 Parisa와 Michal을 포함해서)이 이미 이 주제에 대해 좋은 포스팅들을 많이 썻고 나도 그 포스팅들을 읽어보는 것을 추천한다. 내생각에는 내가 이제부터 할 말들은 걔내들이 이미 말한것들과 많이 겹치겠지만, 사람들이 이러한 주제로 나한태 물어볼때마다 채팅으로 치고, 앞서 말한 사람들의 포스팅들 링크를 던져주는 것 보다는 내가 직접 내 의견을 담은 내 포스팅을 쓰는게 낫다고 생각해서 쓰게 되었다.


일단, 나는 응용 보안을 하고, 취약점 연구부터 보안 리뷰, 버그헌팅, 해킹 등 너가 perspective라고 부르는 것들을 한다. 그리고 이쪽 보안바닥에 다른 커리어도 있는데, 보안개발이나 악성코드 분석, 인프라 보안과 기타 내가 잘 모르는 분야들, 내가 잘 몰라서 조언해주기 어려운 분야들도 있다.


그래서 내가 누구이고, 너가 내가 하는말을 왜 믿어야하냐? 음, 일단 나는 너가 내 말을 무조건 믿어야한다고 생각하지 않는다. 왜냐면 각각의 사람들은 각각의 경험이 있고 각각의 사람들이 걸어온 길은 다 다르니깐. 하지만 아마 너가 이쪽에 관심이 있을수 있으니 설명하자면, 나는 지금 구글 프로젝트 제로의 멤버이고, 구글 보안팀에서 일했었다. 그리고 너가 아마 이 블로그의 옛날 글들을 잘 뒤적거렸을 경우 발견할 수 있는 몇개의 보안툴들을 직접 만들었다.(근데 좀 업데이트가 안되서, 확인해보려면 GPZ 블로그의 가장 최신 포스팅을 봐라.) 그러면 너는 내가 10년넘개 보안관련 일들을 했다는것을 알 수 있을거다.


근데  나는 원래 이쪽바닥이랑은 좀 다른 전공지식을 갖고있는데, 내가 아는 보안하는 인간들은 정말 다양한 전공 출신이다. 예를 들어 나는 다양하게 학술적 기반을 갖고 있다.(전산학 박사, 그리고 대학에서 오랜기간 일했다) 하지만 내가 전산학 박사를 땃다는 것은 내가 같이 일하는 동료들 사이에서는 좀 특이한 케이스이고, 보안을 하기 위해서 이러한 조건이 필요한 것도 아니다. 이건 물론 학위를 따지 말라는 소리는 아니며, 내 전산학 지식이 나중에 내 기술 기반을 쌓아올리는데 많은 도움이 됬다고도 생각한다. 하지만 너가 이미 선택했거나 가지고 있는 학벌과는 관계없이, 이 보안 바닥에 종사하는 사람들이 공통적으로 가지고 있는 특징이 있는데, 이 특징에서 내가 주는 첫번째 팁이 있다.


너만의 일을 해라

내가 이쪽 바닥에서 알고있는 대부분의 사람들은, 보안은 일이기 이전에 취미로 시작했다. 당연히 너가 보안에 갓 입문하려고 할때, 너한태 어떻게 입문하는지도 알려주지 않고 "너 만의 일을 해라"라고 말하는건 별로 도움이 안되겠지. 계속 읽어봐라, 왜냐면 나중에 답이 나오니까. 하지만 명심해라(너를 기죽일려고하는게 아니라, 나중에 대처법도 알려줄거다)



지금을 보지마라, 하지만 10년전에 입문하는건 지금 입문하는 것 보다 훨신 어려웠다.


모든 사람들이 인정하진 않겠지만, 보안은 시간이 지날수록 매우 크게 발달되었다. 물론, 니가 적당히 잘 파보면 10년전의 기술이 여전히 동작하는 하드웨어와 소프트웨어들을 찾을 수 있을것이다. 하지만 예를들어 웹브라우저를 봐라. 내가 맨 처음 윈도우 익스플로잇을(힙 오버플로) 했을때, 마이크로소프트가 Safe Unlinking 기술을 도입해서 잘 알려진 힙 익스플로잇 테크닉들을 막아버렸을때 기죽었었다. 10년 뒤에 브라우저에서 익스를 하는 사람은 Safe Unlinking과 스택 쿠키 뿐만 아니라, SafeSEH/SEHOP, DEP, ASLR, CFG, ACG와 샌드박스 등에 대해 잘 알고 그걸 우회해야 한다. 그리고 이런것은 웹 브라우저에만 적용되는 것이 아니다. 만약에 너가 10년전의 웹 어플리케이션 프레임워크와 지금의 것과 비교를 한다면 보안 부분에서 매우 큰 차이가 있다는 것을 알 것이다.


겁내지마라, 윗 단락에서 나온 단어들은 너에게 아무런 의미가 없다(아직까진)


그래서 어떻게 저렇게 빨리 발전하는 방어기법들과 싸울것인가?


학습자료들을 이용해라


일반적으로 진입장벽이 계속 높아지고 있지만, 팩트는 공부할 자료들도 이전보다 매우 많아졌다는 것이다.


그렇다고 안심할 순 없는게, 너가 나가서 스스로 공부할 수 있는 능력이 있어야 한다. 그 누구도 너의 손을 잡고 하나하나 알려주는 멘토가 되주지 않을 것이다(Sith에게 견습생으로 있을 수 있지만, 해커들이 그런식으로 일하는 경우는 거의 없다). 만약에 너가 잘 짜여진 커리큘럼을 따라가는 것을 좋아한다면(대부분의 공부를 그런식으로 한다면) 너는 보안을 하기는 힘들것이다.


너가 괜찮은 공부 자료를 구하기 전에, 너는 괜찮은 질문들을 던져야 한다. "해킹하는법"과 같은 단어로 구글링을 하면 지금까지 봐왔던 거지같은 결과만 나올 것이다. 대신 더 괜찮은 질문들을 해야 한다. 다음과 같은,


내가 관심있는 하드웨어/소프트웨어가 어떻게 동작하는가? 어떤 기술이 기반이 되는가? 내가 볼 수 있는 소스코드가 있는가? 튜토리얼? 서적?



내가 대상으로 하는 하드웨어/소프트웨어를 누군가가 공격시도한 적이 있는가? 그들이 write-up을 올려놓았는가? 익스플로잇을 올렸는가? 컨퍼런스 발표자료가 있는가? 내가 정말로 그사람들이 한 것들을 이해하는가?



따라서 누군가 실제로 만든 소프트웨어나 하드웨어가 어떻게 작동하는지 이해하려면 기술적으로 정통해야 합니다. 코드를 작성하는 것과 읽는 것은 정확히 같은 능력이 아닙니다. 만약 당신이 코딩을 쉽게쉽게 할 수 없다면 보안 공부를 깊게 하기 전에 이것부터 먼저 해야 합니다.



두번째 포인트를 잊지 마시오. 나는 예전에도 기술적으로 아는 것이 꽤나 많았지만, 내가 실제로 사람들이 공개한 취약점 연구와 익스플로잇들을 보기 시작하기 전에는 보안에 대한 내 이해도가 낮았다.



그리고 조언을 하나 더 하자면, 너가 모르는 무언가를 맞닥드렸을때 절때 포기하지 마라. 특히 너가 공부를 시작하고 다양한 자료들을 읽을때, 너가 모르는 무언가를 만날 경우가 매우 많을 것이다. 그 내용을 이해하지 않고 넘어가는 것은 쉬운 방법이지만, 한편으론느 잘못된 방법이기도 하다. 너가 모르는 무언가를 맞닥드렸을때, 모른다고 어렵다고 하기보다는 새로운 것을 배울 수 있는 단서가 된다고 생각해라.



내가 아무도 너의 손을 잡고 이끌어주지 않는다고 썻긴 했지만, 이 말이 너가 질문을 하지 말아야한다는 것은 아니다. 너는 편하게 생각해야 한다. 사람들은 너의 일을 대신해주지 않겠지만, 너가 멘붕에 빠졌을때 적절한 방향으로 가도록 도와줄 것이다.

리눅스에서 부팅 시 특정 작업을 하거나 특정 프로그램이 돌도록 하고 싶을 때가 있다. 그럴 경우 어떻게 해야하는지 차근차근 알아보자. 

본 포스팅은 우분투 16.04 기준으로 작성되었다. 하지만 다른 리눅스 배포판도 비슷한 구조를 가지고 있을 것이다.


리눅스 부팅 과정

일단 리눅스 부팅 과정을 간략하게 알아보자.


BIOS

메인보드에 있는 BIOS라고 불리는 펌웨어가 컴퓨터 부팅 시 필요한 처음 동작들을 처리한다. 이 BIOS는 POST(Power On Self Test)라고 불리는 동작을 통해서 부팅에 필요한 하드웨어들이 잘 연결되어 있는지, 하드웨어들이 잘 동작하는지 등을 확인한다. 그리고 부트 가능한 장치들을 사전에 설정된 부팅 우선순위(Periperial Booting Priority)에 따라 차례대로 각각의 장치의 MBR(Master Boot Record) 혹은 부트섹터라고 불리는 첫번째 섹터의 마지막 2바이트를 확인해서 부트섹터의 시그니쳐가 맞는지 확인한 후 시그니쳐(0x55AA)가 맞을 경우 해당 장치로 부팅을 시도한다. 그리고 MBR의 첫번째 바이트로 점프해서 기계어 Instruction을 실행한다.
최근에는 구식의 BIOS를 대체한 UEFI라는 녀석이 여기에 위치하는 경우도 있다.

MBR

부팅하도록 설정된 장치의 첫번째 섹터를 MBR(Master Boot Record)라고 하며, 여기에 나머지 부팅 절차들을 밟기 위한 명령어들이 있다. 하드웨어에서 한 섹터는 512바이트이며, 컴퓨터들은 하위 호환성을 유지하면서 발전했기 때문에, 저장장치의 용량이 커져도 이는 동일하다. 이 MBR에 있는 코드에는 리눅스 부트로더인 GRUB(혹은 레가시 시스템에서는 LILO)이 들어있다.

GRUB

GRUB은 Grand Unified Bootloader의 약자이며, MBR에 GRUB의 기계어 Instruction들이 있다. 하나의 저장장치에 여러개의 OS가 설치되어 있을 수 있으므로, GRUB은 어떤 OS로 부팅을 할 것인지 사용자가 설정할 수 있도록 UI를 제공한다. 사용자가 원하는 OS를 선택하면, 이 부트로더는 저장장치의 다른 섹터에 있는 커널 코드들을 메모리에 로드한 뒤, 커널 코드로 점프하여 커널의 기계어 Instruction들을 실행하도록 한다.

커널

드디어 리눅스 운영체제의 핵심부인 커널이 메모리에 올라오게 되었고, 이제 커널은 부팅을 마무리 짓기 위한 마무리를 지어야 한다.  grub 설정파일에 정의된 루트 경로에 부분에 루트 파일시스템을 마운트하고, /sbin/init 프로그램을 실행한다. init 프로세스는 해당 OS에서 실행된 첫번째 프로세스로 pid가 1이다. 우분투 최신 버전의 경우 init 프로세스 대신 systemd이라는 시스템 데몬 프로세스가 그 자리를 대신하기도 한다. 하지만 전체적인 맥락은 비슷하다.


Init

Init 프로세스는 해당 시스템에 저장된 런 레벨에 맞게 동작한다. 런 레벨은 보통 0부터 6까지의 숫자로 이루어져 있으며 시스템을 어떤 방식으로 부팅하는지를 결정한다. systemd로 제어되는 시스템의 경우 더 많은 종류의 런 레벨을 갖고 있으며, 리눅스 메뉴얼 페이지에서 man systemd.special명령어로 확인이 가능하다.


0 - 시스템 종료


1 - 싱글 유저 모드.

 계정이 하나만 존재한다. 시스템 복원모드라고도 불리며, 기본적으로 관리자 권한의 쉘을 얻게 된다. 파일시스템을 점검하거나 관리자 암호 변경 시 사용한다. S 모드와의 차이점은, 이 모드는 멀티 유저 모드에서 싱글 유저 모드로 스위치 된 경우 이 모드이다. 점검을 위해 싱글 유저 모드로 들어간 경우라고 인식된다.

S - 싱글 유저 모드

1번 모드와 동일하나, 부팅 시 부터 싱글 유저 모드 인 경우이다.

2 - 멀티유저모드, NFS비활성화. 

여러개의 계정을 사용할 수 있으며, NFS(Network File System)사용이 불가능하다. 3번 모드와 같지만 네트워크 사용이 불가능하다.

3 - 풀 멀티유저 모드.

CLI를 가지며 멀티 유저를 지원한다.

4 - 사용안함. 

기본적으료 사용하지 않는 모드이며, 사용자 정의하여 원하는 모드로 사용 가능하다.

5 - X11

3번 모드와 같이 멀티 유저를 지원하는 일반모드이지만 GUI를 지원한다는게 큰 차이점이다.

6 - 재시작

시스템을 재시작한다.


런레벨 프로그램

Init 프로세스가 런 레벨을 결정하면, 각각의 런 레벨에 맞는 서비스들을 실행해야 한다. 이때 각각 런 레벨 별 실행해야하는 서비스들의 디렉토리 경로는 다음과 같다.

레벨 S - /etc/rcS.d
레벨 0 - /etc/rc0.d
레벨 1 - /etc/rc1.d
레벨 2 - /etc/rc2.d
레벨 3 - /etc/rc3.d
레벨 4 - /etc/rc4.d
레벨 5 - /etc/rc5.d
레벨 6 - /etc/rc6.d

여기서 rc는 Run Command의 약자라고 한다.

/etc/rc0.d 디렉토리를 보면 다음과 같이 보인다.
 
root@ubuntu:/etc# tree rc1.d/
rc1.d/
├── K01alsa-utils -> ../init.d/alsa-utils
├── K01bluetooth -> ../init.d/bluetooth
├── K01cups-browsed -> ../init.d/cups-browsed
├── K01docker -> ../init.d/docker
├── K01irqbalance -> ../init.d/irqbalance
├── K01kerneloops -> ../init.d/kerneloops
├── K01lightdm -> ../init.d/lightdm
├── K01mysql -> ../init.d/mysql
├── K01saned -> ../init.d/saned
├── K01speech-dispatcher -> ../init.d/speech-dispatcher
├── K01thermald -> ../init.d/thermald
├── K01ubuntu-fan -> ../init.d/ubuntu-fan
├── K01ufw -> ../init.d/ufw
├── K01uuidd -> ../init.d/uuidd
├── K01vmware-tools-thinprint -> ../init.d/vmware-tools-thinprint
├── K01whoopsie -> ../init.d/whoopsie
├── K01xinetd -> ../init.d/xinetd
├── K02avahi-daemon -> ../init.d/avahi-daemon
├── K02cgroupfs-mount -> ../init.d/cgroupfs-mount
├── K02cups -> ../init.d/cups
├── K04rsyslog -> ../init.d/rsyslog
├── README
├── S01killprocs -> ../init.d/killprocs
└── S02single -> ../init.d/single

Bash

K로 시작하는 녀석들은 해당 서비스들을 종료하기 위한 스크립트로, 심볼릭 링크 원본의 스크립트를 stop인자와 함께 실행한다.
S로 시작하는 녀석들은 해당 서비스들을 구동하기 위한 스크립트로, 심볼릭 링크 원본의 스크립트를 start 인자와 함께 실행한다.

뒤에 있는 숫자는 순서를 맞추기 위함이다.


이런식으로 rc?.d 디렉토리들을 확인해보면, 런레벨 2~5와 S레벨의 경우 마지막에 /etc/init.d/rc.local 이라는 스크립트를 실행시킨다.

/etc/init.d/rc.local 스크립트를 살펴보면 내용은, /etc/rc.local 이라는 파일이 있을 경우 실행시킨다고 되어있다.


즉 런레벨 2~5, S의 경우 /etc/rc.local 이라는 스크립트를 부팅 과정 마지막에 실행시킨다는 것이다.


/etc/rc.local 스크립트 실행

Ubuntu16.04의 경우 런 레벨 2~5, S의 경우 /etc/rc.local를 최종적으로 실행시킨다. 해당 파일이 없을 경우 실행하지 않는다. 런레벨 0은 시스템 종료, 6는 재부팅이므로 관계 없고, 런레벨 1의 경우 시스템 복구 모두이기 때문에 복구를 위해서 해당 스크립트를 실행하지 않는것으로 보인다.



리눅스 부팅 스크립트 등록

리눅스 부팅 과정을 알아보면서 알 수 있듯이, 부팅때마다 실행되어야 할 스크립트는 /etc/rc.local에 입력하면 된다.


단, 주의할 사항은 PATH 같은 환경변수가 일반 쉘과 같은 환경으로 설정되지 않을수도 있기 때문에, 환경변수를 설정하고 명령어를 실행하거나, 절대경로로 실행하는 것을 추천한다.


또한 스크립트 실행 도중 에러가 발생했을 때, 표준 출력과 표준 에러를 직접 터미널로 확인하기 어려우므로 파일로 리디렉션(Redirection)하되, 표준 에러도 같이 리디렉션(Redirection)하도록 하는 것을 추천한다.


-------------------------------------

참조

https://www.thegeekstuff.com/2011/02/linux-boot-process/

http://kateee.tistory.com/51

http://nuitstory.tistory.com/500

https://wiki.debian.org/BootProcess

http://cherub.sungkyul.edu/~web/jinboard/files/63_boot.pdf

https://ko.wikipedia.org/wiki/%EC%8B%9C%EB%8F%99_%EC%9E%90%EC%B2%B4_%EC%8B%9C%ED%97%98

http://forensic-proof.com/archives/435

자바스크립트 동시성 모델


자바스크립트는 이벤트 루프에 기반한 동시성(Concurrency) 모델이라는 것을 가지고 있습니다. 이 모델은 다른 C나 Java와 같은 언어가 실행되는 방식과 다릅니다.


본디 자바스크립트는 웹 브라우저 상에서 동작하는 스크립트 언어로 만들어졌습니다. 그리고 자바스크립트로 DOM API에 접근해서 브라우저의 UI를 제어하기 위해 UI스레드를 공유할 수 밖에 없는 환경입니다.


만약 자바스크립트가 일반적인 C나 Java언어 처럼 동작하고, 동시성을 제공하지 않는다면 다음과 같은 상황이 벌어질 수 있습니다.


서버에서 비동기적으로 데이터를 받아오기 위해 XHR을 통해 Ajax요청을 보내고, 해당 데이터를 받아오기 까지 시간이 소요됩니다. 그러면 해당 데이터를 받아오기 전 까지 UI를 제어하는 자바스크립트는 멈추게 되므로 사용자는 UI를 제어할 수 없습니다.


이러한 경우가 Ajax가 잦은 페이지에서는 클라이언트 리소스문제가 아니라도 브라우저가 매우 자주 버벅일 것이고 이는 사용자에게 매우 나쁜 UX를 제공합니다.


이러한 상황을 피하기 위해서 자바스크립트는 이벤트 기반 동시성 모델을 제공합니다. Ajax요청을 보내는 것과 응답을 받는 것, 각각을 다 이벤트로 처리하며 Ajax와 같이 I/O 요청 이후 결과를 계속 기다리는 것이 아니라 곧바로 다른 처리할 수 있는 작업을 처리하게 되며 UI 쓰레드가 멈추는 것을 막을 수 있습니다.

동시성과 병렬성

동시성(Concurrent)는 병렬성(Parallel)과는 다릅니다. 둘 다 여러개의 실행 흐름이 동시에 일어나는 것이라는 공통점이 있습니다.


동시성

동시성(Concurrent)는 실제 물리적으로 동시에 일어나는 것이 아니라, 흐름을 실행시키는 것은 하나(e.g. CPU 코어 혹은 쓰레드)지만 작은 타임 슬라이스(Time slice, Time quantum)단위로 다른 흐름을 돌아가면서 실행시켜서 동시에 일어나는 것 처럼 보이게 하는 방식입니다. 논리적인 의미에 동시 실행으로 볼 수 있습니다.


병렬성

병렬성(Parallel)은 실제로 흐름을 실행시키는 것(e.g. CPU 코어 혹은 쓰레드)이 복수개여서, 각각 실행 흐름을 할당받아 동시에 실행시킵니다. 물리적인 의미에 동시 실행입니다.




자바스크립트는 언어 스펙이 싱글쓰레드이므로, 병렬성(Parallel)을 지원할 수 없습니다. 대신 동시성(Concurrent)는 지원할 수 있습니다.


자바스크립트 런타임 모델

자바스크립트는 브라우저에서 더 좋은 UX를 제공하기 위해 동시성 모델을 제공한다. 하지만 이 동시성 모델은 자바스크립트 언어 자체의 스펙이 아니라, 자바스크립트를 구동하는 환경인 웹 브라우저의 자바스크립트 엔진에서 구현한 모델이다. 또한 범용 언어로서 자바스크립트 런타임인 Node.js도 비슷한 방식의 동시성 모델이 구현되어 있다.

다음 그림은 런타임의 구성요소들을 시각화 한 것이다.



image/svg+xml Stack Heap Queue


스택, 힙, 큐로 이루어져 있다.


스택

스택은 C와 같은 다른 프로그래밍 언어에서 사용하는 함수 호출시 할당, 리턴 시 해제되는 그 콜 스택이다.

함수가 호출될 때 마다 해당 함수에 대한 스택 프레임이 생성되고, 해당 스택 프레임은 해당 함수의 인자와 지역변수들을 저장한다.

함수가 리턴될 때 해당 함수의 스택 프레임은 해제된다.


일반적으로 힙은 동적으로 할당된 변수나 메모리 등을 포함한다. 자바스크립트는 인터프리터형 스크립트 언어이므로 선언된 것들을 저장할 수 있지만, 지역변수나 함수 인자들은 스택에 저장되므로, 전역 객체와 같은 것들이 힙에 저장되게 된다.

자바스크립트 런타임에서 큐는 메시지 큐이다. 이 큐를 이용해서 자바스크립트는 동시성 모델을 지원하며, 큐에는 처리해야 할 메시지들이 저장되어 있다.
메시지는 처리해야할 함수들의 셋이라고 볼 수 있으며, 큐는 메시지 단위로 처리한다. 큐가 메시지를 처리하는 로직은 다음과 같다.

1. 콜 스택이 비었으면 큐에서 메시지를 꺼내서 처리한다.
2. 콜 스택이 비지 않았으면 콜 스택이 빌 때 까지 처리하던 함수를 계속 처리한다.

메시지 처리는 콜 스택에 처음 비어서 해당 메시지를 큐에서 꺼내어서 해당 메시지에 대한 함수를 호출하면서 메시지 처리가 시작이 된다.
콜 스택에 다시 비워질 경우 해당 메시지 처리는 종료된다.

메시지 큐 동작 방식을 확인할 수 있는 간단한 예제를 보자.

 
function foo(a) {

	while(true) {
		a += bar(5);
	}
}

function bar() {
	var a = 1;
	return a;
}

function baz() {
	console.log('Another task');
}

foo();
baz();

JavaScript

브라우저에서 위 코드를 실행하면 Another task라는 메시지는 보이지 않는다. foo함수가 맨 처음 메시지 큐에 들어가서 처리되게 되는데, foo 함수는 bar함수를 호출, 리턴을 무한 반복하게 되며 콜 스택에 비워지지 않기 때문이다. 그러므로 다음 메시지 큐에 baz함수가 있지만 계속해서 실행이 되지 않게 된다.
 
function foo(a) {
	setTimeout(0, bar);
}

function bar(x) {
	setTimeout(0, foo);
}

function baz() {
	console.log('Another task');
}

foo();
baz();

JavaScript

브라우저에서 위 코드를 실행하게 되면 Another task라는 메시지는 잘 보인다. setTimeout(time, function); 함수를 호출하면, time시간 이후에 function 함수를 메시지 큐에 추가하게 된다.

메시지 큐에는 다음과 같은 방식으로 메시지가 들어가고, 처리되게 된다.

foo 

 baz

 bar

 foo

 bar



이러한 이벤트 루프의 특성을 잘 파악해야지, 여러개의 테스크를 동시성있게 실행되게 자바스크립트 코드를 작성할 수 있다. 이러한 이벤트 루프 모델에 반하는 방식으로 코드를 작성하면 나쁜 UX를 사용자에게 제공하게 되며, 이는 거의 버그나 마찬가지이다.



------------------------------------------------------

참고

https://developer.mozilla.org/ko/docs/Web/JavaScript/EventLoop

+ Recent posts