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 fm…

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

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

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

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

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

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의 패턴에 따라서 어떤 압축 알고리즘을 사용하는 것이 정확한 비교가 되는지 결정하기 어렵다는 단점이 있다.

Balanced graph와 unbalanced graph

이미지
사람과 사람 사이의 관계를 그래프로 나타내보자. 그렇다면 사람은 node로 표현될 것이고, 두 사람이 서로를 알고 있다면 두 node 사이의 edge를 그릴 수 있다. 실제로는 내가 모르는 사람이 나를 알고 있을 수도 있고, 내가 아는 사람이 나를 모를 수도 있지만, 그런 복잡한 경우는 생각하지 않고 서로 간에 알고 있는 경우만을 고려하도록 하자.1)
 이때 두 사람은 친구이거나 적일 것이다. 현실적으로는 그냥 아는 사람이고 적인지 친구인지 별생각 없을 수도 있거나, 내가 친구라고 생각하는 사람이 나를 적으로 생각할 수 있지만, 이런 경우 문제가 복잡해지니 생각하지 않도록 하자.
 그러면 친구인 사람 사이의 edge에는 +사인을, 적인 사람 사이의 관계에는 -사인을 추가하여 signed undirected graph2)로 표현할 수 있다.  사람 사이의 관계를 signed graph로 표현한다면, 이는 아래의 4가지 모습 중 하나일 것이다.
 만약 친구일 확률과 적일 확률이 똑같이 1/2로 랜덤하게 그래프를 생성하였다면, 모두가 친구로 생각하거나 모두가 적으로 생각할 확률은 각각 1/8이고, 한 명은 다른 두 명을 적이라고 생각하는데 두 사람은 서로 친구라고 생각하거나, 친구로 생각하는 두 사람이 서로 적일 확률은 각각 3/8으로 나타날 것이다.
 하지만 실제 인간관계를 그래프로 그렸을 때는 첫 번째나 세 번째 삼각형이 나올 확률은 기대했던 것보다 높게 나오고, 두 번째나 네 번째 삼각형이 나올 확률은 기대했던 것보다 낮게 나온다. 이 두 삼각형은 다른 삼각형으로 쉽게 변하기 때문이다.

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

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일 필요도 없고, 사실 전기 신호일 …

[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)라고 표현하고 있다. 만약 길게 실행되는 일을 백그라운드에서 실행하고 싶으면 스레드를 직접 사용하거나 다른 방법을 이용해야 한다. 그 이유는 몇 가지 있다.
 대표적인 문제는, AsyncTask가 Activity에 종속되지 않기 때문에, AsyncTask를 호출했던 Activity가 먼저 죽었을 경우 처리가 복잡해진다.
 또 다른 문제는 백그라운드 스레드를 점유해버린다는 것이다. AsyncTask는 AsyncThread가 새로 만들어질 때마다 스레드를 생성하지 않는다. 고정된 크기의 스레드 풀이 있고 이 스레드 풀을 모든 AsyncTask가 공유한다. API 버전에 따라서 1개의 태스크만 실행될 수도 있고, 여러 개의 태스크가 병렬적으로 동시에 수행될 수도 있는데, 최신 버전은 1개만 실행되며 모든 태스크는 순차적으로 실행하게 되어 있다. 따라서 어떤 태스크가 데 시간이 오래 걸린다면 다른 태스크가 실행되지 못한다.AsyncTask를 사용하는 대략적인 과정은 다음과 같다. Asyn…

멀티 쓰레드 환경에서 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가 없어도 안심…