2016-08-18

[Web] SpeechSynthesis - TTS API

 SpeechSynthesis는 Web Speech API의 하나로 주어진 텍스트를 소리로 바꿔주는 TTS API이다. SpeechSynthesis 이전에도 TTS 서비스가 있었지만, 이들은 유료이거나 웹에서 사용하기 불편한 경우가 대부분이었는데 SpeechSynthesis의 경우 브라우저에 내장되는 API이므로 무료로 쉽게 사용할 수 있다는 장점이 있다.
 물론 Web Speech API 자체가 아직 draft에 해당하기 때문에 브라우저가 지원해야만 사용 가능하다는 문제는 있지만, 2016년 8월 15일 현재 데스크탑에서는 크롬과 사파리, 안드로이드 기본 브라우저인 크롬, 아이폰의 기본 브라우저인 사파리에 지원하고, 파이어폭스는 9월에 지원할 예정이므로 대부분의 모던 브라우저에서는 사용할 수 있다.

 speechSynthesis는 5개의 함수를 가지고 있다. 그중 4개는 speak, cancel, pause, resume이다. 이를 이용해서 재생할 음성을 추가하거나 취소하거나 일시 정지할 수 있다.  이 중 speak 함수는 SpeechSynthesisUtterance를 인자로 받는다. speak 함수를 호출했을 때, 이미 재생 중인 utterance가 없고 speechSynthesis가 pause 되어 있지 않으면, 요청된 utterance는 즉시 재생된다. 하지만 이미 재생 중인 utterance가 있거나 speechSynthesispause 되어 있다면, utterance는 바로 재생되지 않고 queue에 저장되었다가 후에 재생된다.
 따라서 실제로 언제 재생되는지는 utterance의 콜백을 통해서만 알 수 있다. 이벤트의 종류에 따라서 onstart, onend, onerror, onpause, onresume 등의 콜백을 등록할 수 있다.

 또한, SpeechSynthesisUtterance는 6개의 속성을 가지고 있다.
 첫 번째 속성은 text로 읽을 텍스트를 지정한다. 객체를 생성할 때 생성자에 넘겨줄 수도 있고, 생성한 뒤 text attribute에 값을 할당할 수도 있다.
 두 번째 속성은 lang으로 어떤 언어로 읽을지를 결정해준다. 만약 아무런 값도 지정하지 않으면 기본적으로는 html 태그의 lang 값을 이용한다.
 세 번째 속성은 voice다. voiceSpeechSynthesisVoice 객체를 값으로 설정할 수 있는데, 아무런 값도 할당 안 하면 default voice를 사용하게 된다.
 네 번째 속성은 volume이다. volume은 최소 0에서 최대 1의 값을 가질 수 있다. 아무런 값을 주지 않으면 기본값은 1이 된다.
 다섯 번째 속성인 rate는 소리의 속도를 결정한다. 기본값은 1이고, 값이 커지면 빠른 속도로, 값이 작으면 느린 속도로 발음한다.
 마지막 속성인 pitch는 소리의 전체적인 높이를 결정한다. 기본값은 1이고, 0에서 2 사이의 값을 가질 수 있는데 최저와 최고일때 어떤 음을 가질지는 브라우저의 구현에 따라서 다르다.

 speechSynthesis에 있는 마지막 함수는 getVoices다. 이 함수는 SpeechSynthesis가 사용할 수 있는 SpeechSynthesisVoice 목록을 반환한다. 하지만 처음에는 아무런 값도 없을 수도 있다. 사용할 수 있는 voice 목록이 초기화되면 voiceschanged 이벤트가 호출되는데, 이 이벤트가 호출되어야 getVoices 이벤트가 정상적으로 사용할 수 있는 voice 목록을 리턴한다.  이렇게 가지고 온 SpeechSynthesisVoicename, lang, voiceURI, localService, default 5개의 속성을 가진다. 이때 lang은 해당 목소리가 어느 나라의 언어를 표현하는지를 나타내고, name은 목소리의 이름을 나타낸다. 하지만 이 둘 다 유일할 것이 보장되지 않는다. 어떤 나라의 언어를 표현하는 목소리가 여러 개 있을 수 있고, 같은 이름의 서로 다른 목소리가 존재할 수도 있다. 하지만 현재 안드로이드 크롬에는 버그가 있어서 voice를 설정해도 목소리가 변하지 않고, lang을 설정해야 목소리가 변한다. 따라서 한 언어에 여러 개의 목소리가 있다고 해도 한 가지 목소리밖에 쓰지 못한다.

 SpeechSynthesis는 데스크탑과 모바일 등 다양한 플랫폼에서 사용할 수 있고, 사용하기도 편하다. 하지만 현재 크롬 구현체에는 약간 문제가 있다. 현재는 크롬은 60개 이상의 문자를 말하고 나면 그다음 utterance에 대해서 소리가 끝나도 end 이벤트도 error 이벤트도 발생하지 않는다. 라이브러리의 버그로 보이는데 작년 6월부터 이슈가 나왔던 문제인데 올해 6월 드디어 원인이 나왔다. 가비지 컬렉팅과 관련된 문제로 보이는데, utterance를 전역변수로 보관하고 있으면 문제가 발생하지 않는다. 원인이 나왔으니 곧 고쳐질 것으로 보이지만, 그동안은 utterance를 전역변수에 저장하는 식으로 우회해야 한다.

2016-07-31

Go의 장단점

 지난 4개월 동안 Go로 2개의 프로젝트를 진행하게 되었다.
 한 프로젝트는 비동기적인 웹 서버를 작성하는데, 실행파일이 네이티브 바이너리로 나올 필요가 있었다. C나 C++은 웹서버를 만드는 것도, 비동기적인 작업을 하는 것도 간단하게 되지 않기에 Rust와 Go 중에서 고민하다가 Go를 사용하기로 했다.
 다른 한 프로젝트는 큰 파일을 다운받아서 라인 파싱하고, rest API로 데이터를 요청하고, 그래프를 만들어 자료를 분석하여야 했다. 워낙 큰 파일을 파싱해야 해서 JavaScript는 사용하기 싫었고, 며칠을 밤낮으로 돌아가야 하는 코드인데 별도의 검증을 할 수 없는 Python으로는 실행 중 버그가 발생했을 때 손실되는 시간이 크기 때문에 사용하기는 싫어서 Java, Scala, Go 중에서 고민하다가 Go를 쓰기로 했다.
 이번 글에서는 Go를 사용하면서 느꼈던 장단점에 대해서 간단히 써보도록 하겠다.

장점

학습 속도가 빠르다.

 Go는 공식 튜토리얼만 읽으면 다른 사람이 쓴 코드를 읽고 쓰는데 아무런 문제가 없다. 물론 Go의 튜토리얼 문서가 잘 돼 있기는 하지만, 다른 언어에 비해 크게 뛰어난 것은 아니다. 그냥 Go에는 프로그램을 작성하는데 필수적인 최소 기능들만 들어가 있기 때문이다.

Channel 기반 언어

 Go는 대표적인 채널 기반 언어이다. 스레드라는 것을 명시적으로 주지 않고 goroutine을 생성하면 알아서 스레드를 생성해주고 적절한 스레드에 goroutine을 할당한다. goroutine 사이의 커뮤니케이션을 전부 채널을 통해서 한다면, 귀찮은 동기화 문제를 신경 쓰지 않아도 된다.

네이티브 바이너리가 나온다.

 결과를 배포하는 입장에서 네이티브 바이너리가 나온다는 것은 매우 큰 장점이다. 요즘은 대부분 서버에 파이썬이나 JVM이 설치되어 있지만, 역시 배포는 네이티브 바이너리로 하는 것이 가장 편하다.

컨벤션이 통일되어 있다.

 Go는 컨벤션에 대한 논쟁이 전혀 없다. go에는 컨벤션에 맞춰서 코드를 수정해주는 go fmt라는 기능이 내장되어 있다. 개발자는 그저 go fmt을 돌리기만 하면 되기 때문에, 별도로 스타일 논쟁이 발생할 일이 없다. 코드를 올리기 전에 go fmt을 돌리는 것조차 귀찮은 사람을 위해서 vim-go 같은 플러그인이 있어서 개발자는 스타일을 신경 쓰지 않고 코드를 작성하기만 해도 컨벤션에 맞출 수 있다.

인터페이스 기반의 폴리모피즘

 Go에서 폴리모픽한 코드를 작성하는 유일한 방법은 인터페이스를 인자로 넘기는 것이다. 이 덕분에 인터페이스 단위의 추상화가 자연스럽게 이루어진다.

단점

verbose 한 코드

 Go는 템플릿도 매크로도 없다. 이는 Go의 코드를 단순하게 만들어주는 장점도 있다. 하지만 템플릿도 매크로도 없어서 단순한 코드가 반복해서 등장한다.

type introspection

 모든 폴리모피즘이 인터페이스를 기반으로 돌아가기 때문에 구체적인 구조체 타입별로 다른 동작을 하기 위해서는 type introspection을 통해서 타입을 체크해야한다. 그래서 Go는 정적 타입 언어임에도 불구하고 아래처럼 동적 언어에서나 볼법한 코드가 나온다.

interface{}

 게다가 표준 라이브러리는 interface{}의 사용을 적극적으로 권장한다. 예를 들어 list 같은 자료 구조의 경우 템플릿이 존재하지 않기 때문에 특정 타입의 값을 담는 자료구조를 만들 수 없어서 interface{} 타입의 값을 추가하고 interface{} 타입의 값을 리턴하는 자료구조를 만들 수밖에 없다. 이러한 자료구조 때문에 type introspection을 자주 사용할 수밖에 없고, 동적 타입 언어에서나 볼법한 코드가 나오게 된다.

어설픈 최적화

 Go는 함수를 inline 시키는 최적화를 하지 않는다. 다른 언어라면 별문제 없을 수도 있다. 하지만 Go는 함수를 사용할 일이 많다.
 예를 들면 자원 관리나, recover 등을 위해서 defer를 사용할 일이 많은데 defer는 함수 단위로 호출된다. 따라서 특정 시점에서 자원을 해제시키고 싶다면 새로운 함수를 만들어 새 안에서 defer 함수를 호출해야 한다. 이 모든 함수가 인라이닝 되지 않기 때문에 실제로 스택 프레임을 변경시켜가며 함수를 호출했다 해제하는 일을 한다.

라이브러리 버전 관리

 Go는 언제나 하나의 실행파일을 컴파일한다. 정적 라이브러리나 동적 라이브러리를 배포할 수 없고, 소스를 가져와서 컴파일하는 방식을 사용한다. 이는 딱히 나쁜 방식이 아니다. 현대의 패키지 매니저 중 많은 것들이 이런 방식을 취한다.
 문제는 Go에는 라이브러리의 버전을 명시할 방법이 없다는 것이다. 만약 사용하고 있던 라이브러리가 버전업을 해서 인터페이스가 변경되거나 버그가 발생하거나 해서 옛 버전을 사용해야 할 경우 문제가 되는 라이브러리뿐 아니라 모든 라이브러리의 버전업을 할 수 없다.

변화에 민감한 스타일과 문법

 Go는 컨벤션이 정해져 있는 것이 장점이라고 위에서 말했다. 컨벤션이 정해져 있다는 사실은 꽤 편하다. 하지만 몇몇 부분에서 잘못된 스타일을 사용한다.
  예를 들면 Go는 필드의 타입 부분을 인덴트를 맞춘다. 따라서 아래와 같이 짧은 이름의 필드만 있던 구조체에 긴 이름의 필드가 추가되면 긴 이름에 맞춰서인덴트를 바꾼다. 이런 컨벤션은 보기 좋아 보일 수도 있다. 하지만 이런 스타일은 이력을 더럽히기 때문에 좋지 않다.  위와 같은 변경이 있으면 실제로는 필드가 한 개 추가된 것이지만, 버전 관리 도구에서는 한 개의 필드를 지우고 2개의 필드를 추가한 것처럼 나오게 된다. 지난번 포스트에서도 말했듯이 코드의 스타일은 사용하는 도구를 따라가야 하고, 개발에서 버전 관리 도구는 때놓을 수 없으므로 버전 관리 도구에 친화적인 스타일을 사용하는 것은 중요하다.

결론

 Go는 장점이 분명하다. 러닝 커브가 완만하므로 팀원에 누가 들어오더라도 쉽게 적응할 수 있다. 하지만 이 점을 제외하면 단점이 장점보다 많다. 다만, 지금까지 썼던 네이티브 바이너리가 나오는 언어 중에서 가장 학습하는데 걸리는 시간이 짧았다는 점은 중요하다고 생각한다. 네이티브 바이너리가 나오는 것이 가장 중요한 요구 사항이라면 Go를 고려해볼 만 하겠지만, 그 외의 상황에서 Go를 사용하지는 않을 것 같다.

2016-06-11

Normalized Google Distance - 구글을 통해서 유사도 비교하기

 지난번 글에서는 압축 알고리즘을 통해서 두 object의 유사도를 비교하는 방법을 소개하였었다. 이번에 소개할 방법은 지난번 방식보다 더 재밌는 방법이다. 이번에 소개할 방법은 Normalized Google Distance(a.k.a. NGD)라는 방법으로 이름 그대로 구글을 이용한다. 구글을 이용하기 때문에 문자적으로 의미를 가지는 키워드에 대해서만 유사도를 비교할 수 있다.

 만약 두 키워드가 비슷한 의미라면, 이 두 키워드 중 하나가 나오는 문서에 다른 키워드도 등장할 확률이 높다. 이를 구글을 이용해서 측정하는 것이다. 구글에 의해 검색되는 문서 전체의 수를 N이라고 하고, 구글에 검색해 검색결과의 수를 리턴하는 함수를 f라고 할 때, NGD의 수식은 다음과 같다.
NGD(x, y) = max{ log f(x), log f(y) } - log f(x, y) / log N - min{ log f(x), log f(y) }
 비교하고 싶은 각각의 키워드를 따로따로 검색했을 때의 결과와 함께 검색했을 때의 차이를 전체 문서의 수 N으로 정규화하는 것이다. 이때 정확한 전체 문서 수를 아는 것은 사실 불가능하다. 그래서 NGD를 이용할 때는 IDF가 매우 낮은 단어의 문서 수를 전체 문서 수로 가정한다. 보통 thea 같은 단어를 사용한다.

 NGD는 0에서 ∞사이의 값을 가진다. 0에 가까울수록 둘은 유사한 키워드이다. 유사하지 않은 키워드는 비교하면 큰 값이 나온다. 보통 1보다 크면 둘은 유사하지 않다고 말한다.

 NGD는 일반적으로 사용할 수 없고, 문자적으로 의미를 가지는 키워드를 비교할 때만 사용할 수 있다는 점과 특정 토픽 내에서의 유사도를 비교한다거나 하는 식으로 확장할 수 없다는 단점이 있다. 하지만 구글링할 수 있는 키워드를 일반적으로 비교할 때 쉽게 구현할 수 있다는 장점이 있다.

2016-06-10

Normalized Compression Distance - 압축 알고리즘을 통해서 유사도 비교하기

 이번에 소개할 Normalized Compression Distance (a.k.a. NCD)는 압축 알고리즘을 이용해서 두 object의 유사도를 측정하는 참신하고 재밌는 방법을 소개할 것이다. 이 방법은 간단하면서도 의외로 효과적이면서 기발한 방법이다.

 compression 알고리즘은 보통 효율적인 압축을 위해서 자주 등장하는 sequence일수록 더 짧은 길이로 압축하도록 노력한다. 따라서 같은 길이의 sequence가 더 짧은 길이로 압축됐다면, 이 sequence에는 반복되는 sub-sequence가 많이 있다는 말이 된다.

 NCD는 compression의 이러한 성질을 이용한다. 입력받은 sequence를 compression 한 길이를 리턴하는 함수를 Z라고 할때 NCD의 수식은 다음과 같다.
NCD(x, y) = [Z(x + y) - min{Z(x), Z(y)}] / max{Z(x), Z(y)}
 즉, 비교할 두 sequence를 합친 sequence를 압축한 길이가 sequence를 따로따로 압축한 길이에 비해서 얼마나 더 짧아졌는가를 보여주는 것이다. 압축 알고리즘은 일반적으로 Z(x + y)Z(x) + Z(y)보다 작아서 NCD는 0에서 1 사이의 정규화된 값을 가진다. NCD가 1에 가까울수록 둘 사이에는 공통점이 없는 것이고, 0에 가까우면 둘은 매우 비슷한 패턴을 가지고 있는 것이다.

 NDC는 두 object를 binary sequence로 변환할 수 있는 경우에만 사용 가능하다는 문제가 있지만, 컴퓨터로 계산하는 대부분의 경우 binary sequence로 변환할 수 있으므로 큰 문제는 되지 않는다.

 NCD의 재밌는 점은 어떤 압축 알고리즘이든지 사용할 수 있다는 것이다. 덕분에 gzip, bzip, 7z 등 이미 존재하는 다양한 압축 알고리즘을 이용할 수 있어서 구현이 쉽지만, 비교할 object의 패턴에 따라서 어떤 압축 알고리즘을 사용하는 것이 정확한 비교가 되는지 결정하기 어렵다는 단점이 있다.

2016-04-08

Balanced graph와 unbalanced graph

 사람과 사람 사이의 관계를 그래프로 나타내보자. 그렇다면 사람은 node로 표현될 것이고, 두 사람이 서로를 알고 있다면 두 node 사이의 edge를 그릴 수 있다. 실제로는 내가 모르는 사람이 나를 알고 있을 수도 있고, 내가 아는 사람이 나를 모를 수도 있지만, 그런 복잡한 경우는 생각하지 않고 서로 간에 알고 있는 경우만을 고려하도록 하자.1)
 이때 두 사람은 친구이거나 적일 것이다. 현실적으로는 그냥 아는 사람이고 적인지 친구인지 별생각 없을 수도 있거나, 내가 친구라고 생각하는 사람이 나를 적으로 생각할 수 있지만, 이런 경우 문제가 복잡해지니 생각하지 않도록 하자.
 그러면 친구인 사람 사이의 edge에는 +사인을, 적인 사람 사이의 관계에는 -사인을 추가하여 signed undirected graph2)로 표현할 수 있다.

positive signed edgenegative signed edge

 사람 사이의 관계를 signed graph로 표현한다면, 이는 아래의 4가지 모습 중 하나일 것이다.

 만약 친구일 확률과 적일 확률이 똑같이 1/2로 랜덤하게 그래프를 생성하였다면, 모두가 친구로 생각하거나 모두가 적으로 생각할 확률은 각각 1/8이고, 한 명은 다른 두 명을 적이라고 생각하는데 두 사람은 서로 친구라고 생각하거나, 친구로 생각하는 두 사람이 서로 적일 확률은 각각 3/8으로 나타날 것이다.
 하지만 실제 인간관계를 그래프로 그렸을 때는 첫 번째나 세 번째 삼각형이 나올 확률은 기대했던 것보다 높게 나오고, 두 번째나 네 번째 삼각형이 나올 확률은 기대했던 것보다 낮게 나온다. 이 두 삼각형은 다른 삼각형으로 쉽게 변하기 때문이다.

 내가 친구라고 생각하는 두 사람이 서로를 적으로 생각하는 경우를 보자. 이 경우 나에게 압박이 가해져서 한쪽 관계를 끊거나, 적으로 돌아서기 쉽다. 3명이 서로를 모두 적으로 생각하는 경우도 쉽게 변한다. 이런 경우 공동의 적을 상대하기 위해 적이었던 두 사람이 손을 잡기 쉽다.

 이렇게 변하기 쉬운 삼각형을 unbalanced triangle이라고 부르고, 이런 삼각형이 있는 그래프를 unbalanced graph라고 부른다. unbalanced graph의 경우 외부에서 압력을 주지 않아도 쉽게 상태가 변한다.

 반대로 3명이 모두 친구이거나 공통의 적을 둔 친구 사이를 가지는 경우는 외부 압력이 없는 한 상태가 변하지 않는다.

이런 삼각형을 balanced triangle이라고 부르고, 이런 삼각형으로만 이루어진 그래프를 balanced graph라고 부른다.


1) 그래프 안의 모든 사람이 서로 알고 있는 경우. 즉, 모든 node와 연결된 edge를 가지는 경우를 complete graph라고 부른다. 실제 커뮤니티를 조사하면 대부분 서로 모르는 관계이기 때문에 complete graph가 되는 경우는 드물다. 하지만 문제를 단순화하기 위해 앞으로 나오는 그래프는 전부 complete graph라고 가정하도록 하겠다. incomplete graph의 balance에 대해서는 다른 글에서 설명하도록 하겠다.
2) undirected가 directed보다 많이 쓰이기 때문에, undirected는 간단하게 signed graph라고 하고, signed directed graph를 signed digraph라는 이름으로 부르기도 한다.

2016-03-10

OSI 모델은 무엇인가

 OSI 모델은 추상화를 위해 프로토콜을 여러 계층의 layer로 나누고, 각 layer에서 자신이 책임진 일만 하도록 설계한 네트워크 모델이다. 각 layer는 완전히 독립되어 있으며, 각 layer에서 수행해야 하는 일만 제대로 된다면 그 구현은 다른 구현으로 대체 될 수 있도록 설계되어야 한다.

OSI 7 layer

 OSI layer는 보통 7계층으로 나누어지기 때문에 OSI 7 layer라고 불리기도 한다. 각 layer의 이름은 가장 아래부터 physical layer, data link layer, network layer, transport layer, session layer, presentation layer, application layer다. 이를 다시 크게 둘로 나누어서 network layer까지의 아래 3개 layer는 media layer라고 하고, transport layer부터 위의 4개 layer를 host layer라고 한다. media layer는 네트워크상에서 원하는 머신을 찾아 데이터를 보내는 역할을 하고, host layer는 전송된 데이터를 안전하게 사용하는 방법에 대한 역할을 한다.

 그러면 이제부터 각 layer에 대해 자세한 설명을 할 것인데 명심해야 할 것은 어떤 네트워크 프로토콜을 만들 때 OSI 모델을 지키는 것이 필수적인 것은 아니라는 것이다. 실제로 많이 사용되는 네트워크 프로토콜 중에서도 완벽하게 OSI의 원칙에 따라 설계된 프로토콜도 거의 없고, 7개 계층을 전부 구현하여 사용하는 것도 본 적이 없다. 그냥 네트워크 프로토콜은 이러한 역할이 필요하고, 그것은 이러한 순서로 지켜지는 게 안전하다는 가이드 정도로 생각하는 것이 좋다.

physical layer

 우선 가장 아래 계층은 physical layer다. physical layer에서는 전달 매체를 통해 전달된 신호를 어떻게 0과 1로 바꾸는지를 담당한다. 데이터의 전달 매체에 따라서 0과 1이 반드시 bit일 필요도 없고, 사실 전기 신호일 필요도 없다. 정말 다양한 방법으로 구현될 수 있지만 보통은 프로그래머가 신경 쓸 필요 없는 수준의 이야기다.

data link layer

 두 번째 계층은 data link layer다. data link layer에서 전송되는 data의 단위는 frame이라고 부르는데, 이 계층에서는 오류 없는 온전한 frame이 전달되는 것을 보장하거나 데이터를 전달할 매체에 접근하는 것을 컨트롤 해주는 계층이다. 다만 최종목적지까지 전달되는 것은 아니고, 로컬 네트워크 내에서만 전달해주거나, 로컬 네트워크 사이를 연결해주는 역할만 한다. 오류가 없는 것을 보장하기 위해 에러를 감지하거나 고칠 수 있는 데이터를 프레임에 추가하기도 한다.

network layer

 세 번째 계층은 network layer다. network layer에서 다루는 data의 단위는 packet이다. 일반적으로 frame은 packet을 감싼 1 : 1 관계지만, 꼭 그렇다고 할 수는 없다. 다양한 이유로 data link layer에서 packet들을 모아서 하나의 frame으로 만들거나, 하나의 packet을 쪼개서 여러 개의 frame으로 만들거나 한다. 이 계층에서는 data가 목적지까지 도착하도록 routing 해준다. 하지만 목적지까지 제대로 도착한다거나, 순서대로 도착하는 것을 보장해주지는 않는다.

transport layer

 네 번째 계층은 transport layer다. transport layer는 최종 노드까지의 안전한 통신을 보장해서 이다음 layer부터는 data의 무결성을 신경 쓰지 않을 수 있도록 해준다. 에러 검출이나 order의 보장 등을 할 지 말지에 따라서 TP0부터 TP4까지 4개의 class로 분류된다.

session layer

 다섯 번째 계층은 session layer다. 두 node 사이의 인증을 해주거나, 연결이 끊어졌을 때 다시 연결을 해주거나 하는 것을 담당한다. SSH protocol처럼 지속적인 연결이 보장되어야 하는 프로토콜에서 사용된다.

presentation layer

 여섯 번째 계층은 presentation layer다. 교환되는 정보의 형태를 결정한다. endian을 결정하기도 하고, 전송할 data field의 크기를 결정하는 등 전송될 데이터의 모양을 만든다. 하지만 현실적으로 별도의 프로토콜 레이어로 구현되지 않고, 서버와 클라이언트 간의 암묵적인 약속으로 보장해준다.

application layer

 일곱 번째 계층은 application layer다. 이 계층의 프로토콜은 사용자가 네트워크를 이용해 실제로 하고자 하던 것. 즉, file을 주고받는다거나, 명령을 보낸다거나 하는 내용을 명시한다.

2016-03-07

[Android] AsyncTask - UI 스레드에서는 시간이 오래 걸리는 일을 하면 안 된다

 안드로이드는 메인 스레드에서 UI와 관련된 일을 처리한다. 그래서 메인 스레드를 UI 스레드라고 부르기도 한다. 그런데 UI 스레드에서 오랫동안 CPU를 점유하는 일을 하게 되면, "Application Not Responding"이라는 메시지가 나온다. 이러면 앱에 대한 컨트롤을 잃게 되며, 응답할 때까지 기다리거나 강제종료할 수밖에 없다.
 이런 일을 방지하기 위해서는 새로운 스레드를 생성하여 다른 스레드에 시켜야 한다. 하지만 로우레벨의 스레드를 바로 사용하면 동기화하는 과정에서 쉽게 실수할 수 있으므로 실수 없이 쉽게 스레드를 사용할 수 있게 하려고 안드로이드는 AsyncTask라는 클래스를 지원한다.

 AsyncTask는 UI 스레드에서 잠시 동안 백그라운드로 일을 호출하고 싶을 때 사용된다. 잠시 동안이라는 것을 문서상으로는 should ideally be used for short operations (a few seconds at the most.)1)라고 표현하고 있다. 만약 길게 실행되는 일을 백그라운드에서 실행하고 싶으면 스레드를 직접 사용하거나 다른 방법을 이용해야 한다. 그 이유는 몇 가지 있다.
 대표적인 문제는, AsyncTaskActivity에 종속되지 않기 때문에, AsyncTask를 호출했던 Activity가 먼저 죽었을 경우 처리가 복잡해진다.
 또 다른 문제는 백그라운드 스레드를 점유해버린다는 것이다. AsyncTaskAsyncThread가 새로 만들어질 때마다 스레드를 생성하지 않는다. 고정된 크기의 스레드 풀이 있고 이 스레드 풀을 모든 AsyncTask가 공유한다. API 버전에 따라서 1개의 태스크만 실행될 수도 있고, 여러 개의 태스크가 병렬적으로 동시에 수행될 수도 있는데, 최신 버전은 1개만 실행되며 모든 태스크는 순차적으로 실행하게 되어 있다. 따라서 어떤 태스크가 데 시간이 오래 걸린다면 다른 태스크가 실행되지 못한다.

 AsyncTask를 사용하는 대략적인 과정은 다음과 같다. AsyncTask를 상속받는 클래스를 만들어서 필요한 함수를 오버라이드한다. 그 뒤 UI 스레드에서 AsyncTask를 상속한 클래스의 객체를 생성하여 execute 메소드를 실행시킨다.
 AsyncTask는 3개의 타입 파라미터를 받는다. 각각은 순서대로 Params, Progress, Result라고 부른다. Params는 태스크를 실행시킬 때 넘기는 파라미터의 타입이고, Progress는 UI 스레드에 태스크의 진행 상황을 넘기기 위해서 사용되는 파라미터의 타입이고, Result는 태스크의 결과를 UI 스레드에 넘길 때 사용되는 타입이다.

 AsyncTask를 상속받는 클래스는 반드시 doInBackground 함수를 구현해야 한다. 이 함수는 태스크를 백그라운드에서 실행시킬 일을 의미한다. 이 함수는 자동으로 백그라운드 스레드에서 실행되며, 수동으로 실행시켜서는 안 된다. 또한 이는 백그라운드 스레드에서 실행되기 때문에 UI를 수정하면 안 된다. 중간에 UI를 수정하고 싶으면 publishProgress 함수를 호출하여 UI 스레드에서 콜백이 실행되도록 해야 한다.  그 외에 필요에 따라서 onPreExecute, onPostExecute, onProgressUpdate, onCancelled 함수를 구현할 수 있다. 각각 태스크가 수행되기 전, 수행된 후, publishProgress 함수가 불렸을 때, 태스크가 취소됐을 때 UI 스레드에서 호출된다. 위의 상황에서 UI를 변경시키고자 할때는 위의 함수들을 구현해서 사용해야 한다.


1) http://developer.android.com/intl/ko/reference/android/os/AsyncTask.html

2016-03-04

멀티 쓰레드 환경에서 fork는 조심해야 한다.

 리눅스나 유닉스 같은 POSIX 시스템에서는 fork를 이용해서 자신과 똑같은 프로세스를 만들 수 있다. 이때 fork를 호출한 프로세스를 부모 프로세스라고 하고, 새로 생성된 프로세스를 자식 프로세스라고 부르는데, 자식 프로세스는 부모 프로세스의 모든 메모리를 복사한다.

 fork는 그 뒤 exec을 해서 다른 바이너리를 실행시키는 fork-exec이 일반적인 사용법이다. 하지만 자식 프로세스부모 프로세스와 완전히 같은 메모리를 가지기 때문에, 스레드가 존재하지 않던 시절에는 exec을 하지 않고 병렬 처리를 하기 위해서도 자주 사용되었다. 지금은 스레드를 사용하는 것이 더 사용하기 쉽고 가벼운 방식이기에 스레드를 병렬처리를 위해 스레드를 주로 사용하지만, 스레드보다 서로 간에 독립적이기 때문에 일을 분리하기 위해서 사용하기도 한다.

 분명히 fork는 없어서는 안 될 기능이지만 fork에는 태생적 한계가 있다. 애초에 fork는 스레드라는 개념이 존재하지 않던 시절에 만들어졌기 때문에 thread-safe 하지 않다. 따라서 멀티스레드 환경에서 사용하려면 조심해서 사용해야 한다.

 fork가 멀티스레드 환경에서 문제를 일으키는 이유는 fork부모 프로세스의 메모리를 전부 복사하지만, fork를 호출한 스레드를 제외한 나머지 스레드들은 죽어버리기 때문이다. 따라서 fork를 호출한 스레드 이외의 스레드에서 획득한 자원은 아무것도 해제되지 않는다. 메모리 릭도 충분히 문제지만, 이는 단순한 메모리 릭만을 의미하지 않는다.  fork가 복사한 메모리에는 힙이나 스택 이외에 mutexcondition variable 들도 포함된다. 만약 mutexcondition variable이 다른 스레드에서 사용된 채로 fork 된다면 해당 변수로 보호되는 critical section에는 다시는 진입할 수 없게 된다. 특히나 malloc 같은 thread-safe 한 함수는 대부분 내부적으로 글로벌한 mutex를 사용하기 때문에 내 코드에 mutex가 없어도 안심할 수 없다. 따라서 멀티스레드 환경에서는 fork 하기 전에 다른 스레드가 전부 멈추는 것을 확인하는 것이 좋다.

 하지만 fork 후 바로 exec 하면 이런 것을 신경 쓸 필요 없다. exec의 경우 메모리를 완전히 새로 만들기 때문에 이런 문제가 발생하지 않는다.

2016-01-18

Flow vs TypeScript - 왜 나는 아직 타입스크립트를 쓰는가

 나는 개인적으로 동적 타입 언어보다 정적 타입 언어를 선호한다. 아니 동적 타입 언어를 싫어한다. 정확히 말하면 동적 타입 언어이기에 생기는 실수에 의한 버그들과 그로 인해 사고에 걸리는 부하를 싫어한다. 그래서 웹 개발을 몇 년을 했지만 여전히 동적 타입 언어인 자바스크립트로 코딩하는 것은 싫어한다. 그 때문에 어지간히 간단한 일이 아니라면, 자바스크립트를 사용할 때 반드시 다른 툴을 붙여 정적분석을 한다.
 자바스크립트의 정적 분석을 위해 사용되는 도구는 크게 2가지로 나뉜다. 마이크로소프트에서 만든 타입스크립트와 페이스북에서 만든 플로우다. 한때는 구글이 타입스크립트를 기반으로 만든 AtScript라는 것도 있었지만, 이는 다시 TypeScript로 합쳐지면서 사라지게 되었다.

 타입스크립트와 플로우는 둘 다 자바스크립트를 정적 타입 검사가 가능하도록 만들어주었다. 하지만 접근하는 방향은 완전 다른 방향으로 접근하였다.
 우선 타입스크립트는 2012년에 첫 버전이 나왔다. 타입스크립트의 목표는 자바스크립트로 변환되는 더 쓰기 편하고 안전한 언어를 만드는 것이었다. 어디까지나 자바스크립트로 변환되는 언어를 만드는 것이었기에 문법적 기반을 자바스크립트에 두었다. 다시 말해서 타입스크립트의 문법은 자바스크립트 문법의 슈퍼 셋이다. 자바스크립트의 문법을 기반으로 하여 그 위에 추가적인 기능을 더했다. 추가된 기능에는 classenum처럼 사용성을 올리기 위한 기능도 있고, private이나 타입 어노테이션 처럼 안전한 코드를 만들기 위해 추가된 기능도 있다.
 반면에 페이스북의 플로우는 새로운 언어를 정의하지 않는다. 플로우는 ECMAScript 6(a.k.a. ES6)의 타입 검증 도구일 뿐이다. 이미 타입스크립트의 많은 기능이 ES6에 표준으로 들어왔기 때문에 새로운 언어를 만들 필요가 없었다. 플로우는 새로운 언어를 정의하지 않기 때문에 이미 자바스크립트로 작성된 프로그램을 바로 분석할 수 있다. 타입이 모호한 경우에는 어노테이션을 추가하여 타입을 선언할 수도 있다. 이런 경우 바벨 등을 이용해서 어노테이션을 없애야 올바른 자바스크립트 코드가 되는 불편함이 있었지만, 0.4.0 이후부터는 어노테이션을 주석으로 처리할 수도 있다.

 2016년인 지금은 타입스크립트의 기능 대부분이 ES6에 추가되었다. 오히려 지금은 타입스크립트가 ES6나 ES7의 기능을 타입스크립트로 가지고 오고 있다. 게다가 ES6를 ES5로 변환시켜주는 바벨이 널리 사용되고 있고, 플로우가 ES6의 중요한 기능들은 대부분 처리할 수 있다.1) 따라서 더는 ES6 + 플로우를 사용하는 것에 비해 타입스크립트를 사용해서 얻는 장점은 없다.
 게다가 플로우가 타입 추론을 더 잘해준다. 물론 타입스크립트도 타입 추론을 한다. 하지만 함수형 언어들이 많이 사용하는 힌들리-밀러 타입 시스템처럼 추론이 강력하지 않다. 변수를 선언하는 시점에서 타입을 추론할 수 있어야 한다. 추론할 수 없는 변수는 any 타입으로 선언된다.
 반면에 플로우는 그 값이 사용되는 부분의 타입까지 고려하여 타입을 추론해준다. 그를 보여주는 좋은 예가 아래의 코드이다.
 위와 같이 함수를 선언한 경우, 플로우에서는 코드를 사용하는 부분에서 타입 에러를 검사한다. 따라서 length 함수의 인자로 numberboolean을 넣으면, numberboolean이 x가 되기 적절한지, 다시 말해 length 프로퍼티를 가지는지를 확인하기 때문에 잘못된 타입을 사용했다는 것을 알 수 있다.
 하지만 타입스크립트는 length 함수를 선언하는 순간 이 함수의 타입이 any -> any로 결정되기 때문에 length 프로퍼티가 없는 값을 넘겨도 타입 에러인지 알 수 없다. 이런 경우를 막기 위해 타입스크립트에서는 함수를 선언할 때 타입 어노테이션을 붙이는 것을 추천한다. 즉, 위의 length 함수는 아래와 같이 선언하여 사용하는 것이 좋다.

 지금까지 말한 것만 보면 ES6가 나온 지금은 플로우를 쓰는 것이 더 올바른 방식으로 보인다. 하지만 그럼에도 나는 아직 타입스크립트를 사용한다. 단순히 쓰던 것이 익숙해서가 아니다. 그저 플로우가 아직 미완성이기 때문이다. 플로우가 미완성이라는 것은 단순히 아직 1.0이 나오지 않았다는 것만을 말하지 않는다. 플로우는 사용하는 데 중요한 몇 가지 부분이 아직 미완성이다.
 우선 기본 API에 대한 타입 정의가 완전하지 못하다. 우선 DOM API의 타입 선언이 어떻게 되었는지 보자. 타입스크립트는 13,813줄로 선언되어 있지만 플로우의 경우 1,306줄의 파일로 선언되어 있다. bom.jscssdom.js를 합쳐도 2천 줄밖에 되지 않는다. ES6와 관련된 타입은 플로우는 614줄밖에 정의되지 않지만, 타입스크립트는 5,174줄로 정의되어 있다. node.js API는 타입스크립트의 타겟이 아니라서 DefinitelyTyped에 정의된 선언을 사용해야 해서 공정한 게임은 아니지만, 이 경우에도 플로우보다 타입스크립트가 더 상세하게 타입을 정의하고 있다. 플로우도 해당 이슈를 알고 있고 해결하려고 하지만 아직 해결되지 않았고, 내가 보기에 1.0이 나오기 전에 해결되면 다행일 것으로 보인다.
 게다가 아직 써드파티 라이브러리 지원이 완전하지 않다. 플로우는 타입 어노테이션이 없는 함수도 타입을 추론해준다. 따라서 플로우의 타입어노테이션을 사용하지 않은 라이브러리를 사용해도 플로우가 자동으로 타입 추론을 해준다.
하지만 자바스크립트처럼 오버로딩이 섞여 있는 언어를 분석하여 추론하는 경우 타입 추론은 쉽게 틀린다. 그래서 멀쩡한 라이브러리를 타입 에러라고 검사하기도 하고, 라이브러리를 잘못 사용한 경우를 잡지 못하기도 한다. 이런 경우는 라이브러리의 타입을 선언해놓은 인터페이스 파일을 사용하면 된다. 하지만 아직 플로우의 인터페이스 파일은 만들어진 것을 찾기 힘들어서 대부분의 경우 직접 만들어서 써야 한다. 2012년부터 기록을 쌓아서 이제는 1천 개가 넘는 라이브러리의 타입선언을 보유하고 있는 타입스크립트와는 상대되지 않는다.
 이에 대해서 타입스크립트의 타입선언을 읽을 수 있게 하는 방향으로 진행되고 있지만, 아직 완성되지 않았다.
 마지막 문제는 타입 추론의 완성도에 대한 문제다.
 위의 자바스크립트에서 많이 사용되는 object에 프로퍼티를 추가하는 코드이다. 새로운 프로퍼티를 추가하는 것이기 때문에 문제없어야 할 것 같지만, 플로우는 이를 { bar: number } 타입으로 추론하기 때문에 해당 코드를 타입 에러로 처리한다.
 위의 코드는 정반대의 경우라서 문제가 되는 경우이다. {}는 어떤 값이라도 키로 받을 수 있도록 해석하기 때문에 실제로는 obj.foo가 nullable한 ?number 타입이 되어야 하지만 number 타입으로 받아도 타입 에러라고 검사하지 못한다.

 다음 문제는 더 심각하다. 플로우는 리터럴 한 값 그 자체를 타입으로 사용할 수 있다. 이를 이용해서 쉽게 다른 언어의 enum에 해당하는 타입을 만들거나 tagged union을 만들 수 있다. 이는 자바스크립트의 관습적인 사용법에 맞기 때문에 enum타입을 새로 추가한 타입스크립트에 비해서 올바른 접근이라고 생각한다. 하지만 이에 대한 타입 추론이 완벽하지 않다는 것이 문제다.
 위와 같은 코드를 보면 인자로 올 수 있는 "red", "green", "blue"를 전부 처리하였기 때문에 모든 경우를 처리한 것이다. 하지만 플로우는 모든 경우를 처리하였다고 생각하지 않고, undefined가 반환될 수 있다는 에러를 출력한다.
 더 심각한 문제는 위와 같은 경우다. 위의 코드의 무엇이 잘못되었을까? case 문에서 "green"의 스펠링을 "greeen"으로 잘못 사용하였다. color 타입은 "red", "green", "blue"만 가능하므로 이는 있을 수 없는 경우다. 하지만 플로우는 이것을 타입 에러로 검사해주지 못한다. 심지어 작년 10월에 리포트되었는데 아직 아무런 피드백조차 없다.

 이런 예제들은 하나하나 들자면 끝이 없다. 하나하나는 사소한 사례로 보일 수 있지만, 타입 추론이 핵심인 툴에서 타입 추론을 못 하는 부분이 있다는 것은 플로우에 대한 신뢰도와 사용성을 떨어뜨린다.
 그렇다고 앞으로 계속 타입스크립트만을 사용하겠다는 것은 아니다. 앞에서도 말했듯이 설계 자체만을 놓고 보면 더는 타입스크립트가 ES6 + 플로우에비해 얻는 이득은 없다. 따라서 플로우가 기본적인 도구들을 갖추고, 많이 사용되는 패턴들에 대해 안전하게 타입 추론을 하게 된다면 플로우로 넘어갈 것이다. 일단 지금은 이것들이 1.0 릴리즈에는 해결될 것으로 보고 기다리고 있다.


1) ES6의 let/const를 플로우가 처리할 수 있게된게 2015년 9월의 일이다. 2014년 11월 플로우가 처음 공개된 이후 거의 1년 가까이 플로우는 ES6의 기본적인 기능인 스코프 단위 변수 할당을 하지 못했던 것이다.

2016-01-11

reflection과 introspection

 Reflection은 실행 시간에 객체의 메타 데이터에 접근하여 원래라면 접근할 수 없는 타입 정보, 프로퍼티, 멤버 함수 등을 수정하는 행위를 말한다. 이는 프로그래머에게 기존의 제약을 넘어서 더 많은 것을 할 수 있게 해준다.
 Reflection 중 실행 시간에 메타 데이터를 읽는 기능만을 구분해서 introspection이라고 부르기도 한다. 하지만 reflection이라고 하면 당연히 introspection을 포함해서 말하는 것이기 때문에, 특별한 이유가 없으면 그냥 전부 reflection이라고 부르기도 한다.

 Reflection은 매우 유용한 도구로 보인다. 하지만 개인적으로 좋아하는 기능은 아니다. 오히려 싫어하는 기능이다. Reflection이 주는 힘은 너무 과한 힘이고 대부분의 경우 득보다 실이 더 많다. Reflection은 기본적으로 실행 시간에 실행되기 때문에 컴파일 타임에 당연히 잡힐 문제를 잡지 못한다. 게다가 컴파일러가 코드 최적화를 하지 못하기 때문에 성능이 느려지는 등 여러 문제를 일으킨다.
 그래서 reflection을 사용해야 하는 일이 생기면, 일단 다른 방법이 없는지 알아보고, reflection을 사용하지 않을 방법이 알아보고, 그래도 방법이 없을 때만 사용한다. 지금까지 그래야 했던 경우는 디펜던시 인젝션을 하는 라이브러리를 이용하거나, 디버깅 로그를 찍기 위해 보통은 접근할 수 없는 정보가 필요하거나, 임의의 객체를 시리얼라이즈를 하는 함수를 만들거나, 내가 건드릴 수 없는 라이브러리에서 제공한 객체의 정보를 가져오는 등의 일이었다. 전부 수정할 수 없어서 어쩔 수 없이 쓴 경우다.

2016-01-10

[ECMAScript 6] class 선언하기

 JavaScript는 훌륭한 객체 지향 언어다. 하지만 프로토타입 기반 객체지향이라는 독특한 개념과 특유의 verbose한 문법 때문에 다른 언어에서 넘어온 사람들은 쉽게 적응하지 못하였고, 객체 지향스럽지 않은 코드를 작성하였다. 그럼에도 프로토타입은 다른 객체 지향 언어가 제공하는 class에 비해서 더 유연한 확장성 지원하기 때문에 많은 JavaScript 개발자들은 class가 필요 없다는 입장을 고수해왔다.
 하지만 프로토타입이 코드를 verbose 하게 만들고, 가독성을 떨어뜨린다는 주장은 꾸준히 제기되었고, 결국 ECMAScript 6에 드디어 class 키워드가 추가되어 보다 쉽게 객체 지향적 코드를 작성할 수 있게 되었다.  여기서 중요한 것은 class 키워드가 ES6에 추가되었다고 해서 클래스 기반 객체지향이라는 개념이 추가된 것은 아니라는 것이다. ES6도 여전히 프로토타입 기반의 객체 지향 언어이다. class는 프로토타입 기반 객체를 만드는 syntactic sugar일 뿐이다. 즉, 위와 같은 코드는 ES5를 기준으로 보면 아래와 같이 해석된다.1) class 스타일의 간결성과 prototype의 유연성을 동시에 갖기 위한 선택이었다.
1) 완전히 일치하는 것은 아니다. ES5에는 생성자로 쓸 수 없는 함수가 존재하지 않기 때문에 method를 완벽히 재현할 수 없다.

2016-01-09

[ECMAScript 6] property 선언하기

 property란 사용하는 코드는 member data에 접근하듯이 사용하지만 실제로는 함수가 호출되도록 하는 프로그래밍 언어의 기능을 말한다. C#, Python, Ruby 등 객체를 중요시하는 언어에는 대부분 존재하는 기능이며 당연히 기존의 JavaScript에도 있었다. 하지만 Object.defineProperties 함수를 이용해야 해서 코드가 복잡해진다는 문제가 있었다.  ES6에서는 property를 쉽게 선언할 수 있는 문법을 도입하였다. 메소드의 이름 앞에 get이나 set을 붙이는 것만으로 property를 선언할 수 있다.

2016-01-08

[ECMAScript 6] method 선언하기

 ECMAScript 5에는 메소드에 해당하는 개념이 없었다. 그저 함수가 first-class citizen이기 때문에 객체의 멤버변수로 함수를 할당하는 방식으로 메소드를 만들었다.

 ECMAScript 6에는 method를 만들기 위한 문법이 추가되어 메소드를 선언할 수 있게 되었다.

 이는 크게 보면 ES5에서 사용하던 함수를 멤버변수에 할당하는 방식과 다를 것 없다. 하지만 사소한 부분에서 약간 다르다.  메소드는 이름을 가지지만 new를 통해서 객체를 만들어낼 수 없다.

 이는 메소드만이 가지는 특징이다. 일반적인 함수는 모두 new를 통해 객체를 만들어낼 수 있다. 반면에 람다 함수는 new를 통해서 객체를 만들 수 없지만, 이름을 가지지 않는다.

2016-01-07

[잡담] 대수적 자료형과 정적 타입 분석이 필요하다


 그랬다면 최소한 위와 같은 버그는 막을 수 있다.