안녕하세요, 다채널 광고 분석 솔루션 매직테이블의 제품개발팀입니다. 매직테이블의 주요 개발 백엔드 언어로 흔히 Golang이라 불리는 구글의 Go 언어를 사용한 지 벌써 1년이 되었네요. 기존 제품은 파이톤(python)으로 백엔드를 구성했기 때문에 작년에 Golang을 매직테이블의 메인 개발언어로 선택할 당시 걱정이 앞섰습니다.
1년 정도 사용하면서 단점보다는 장점이 더 많은 언어라는 확신을 가지게 되었습니다. Golang 언어를 백엔드에 적용하며 우리 제품개발팀이 현장에서 경험한 장점을 설명드리고자 합니다.
- 예제가 실행되는 환경은 별도의 언급이 없는 한 맥OS의 터미널이 기준입니다.
- Golang에서의 패키지는 파이톤의 모듈, 자바(Java)의 패키지와 비슷한 개념입니다. import해 사용할 수 있습니다.
1. 실행 파일 생성
Golang으로 OS별 실행 파일을 생성하는 방법은 간단합니다. 모든 개발 언어의 단골 예제인 Hello, world!를 터미널에 보여주는 프로그램을 만들고 싶다고 가정해 봅시다. 다음과 같이 main.go 파일을 생성합니다.
그리고 터미널에서 다음과 같은 명령어를 입력합니다.
그러면 같은 폴더에 main이라는 이름의 실행 파일이 생성됩니다. 이 실행 파일은 main() 함수의 내용을 실행합니다. 해당 파일에 실행 권한을 부여한 후, 실행해보죠.
정말 쉽네요. 맥 OS에서 리눅스(linux)용 실행 파일을 빌드하는 것도 환경 변수만 지정하면 가능합니다.
Golang 문서에는 Free BSD, 리눅스, 맥 OS, 윈도를 지원한다고 되어 있습니다. 주요 OS는 모두 지원하네요! 실행 파일 생성이 쉬운 점은 OS 상에 데몬으로 띄우기가 간편하다는 장점으로 이어집니다. 실무에서 자주 쓰이는 리눅스(Ubuntu, Centos)의 경우로 예를 들면 별도의 프로그램 없이 service 파일을 작성해 systemctl로 직접 관리할 수 있습니다.
Golang은 별도의 라이브러리 없이 내장 라이브러리로 웹 어플리케이션 생성이 가능하기 때문에 명령어 한줄로 웹 어플리케이션을 실행파일로 빌드 후 리눅스의 시스템 데몬으로 띄워주기만 하면 됩니다. service 파일은 다음과 같이 작성하면 됩니다.
파일 이름이 webapp.service고/etc/systemd/system 폴더에 있다면 다음과 같은 명령어로 바로 웹 어플리케이션을 시작할 수 있습니다.
맥OS 같은 경우도 launchctl을 이용해 실행 파일을 직접 데몬으로 띄워 사용 및 관리할 수 있습니다. 이처럼 실행 파일 생성이 쉬우면 서버에 간단하게 배포가 가능합니다. 특히 마이크로아키텍쳐를 적용하는 서비스의 경우, 서버의 확장이나 배포가 쉽기 때문에 장점이 극대화됩니다. 또한 작은 앱을 만들어서 바로 실험을 해보고 싶을 때, 별도의 설정 없이 빠르게 개발 통합 환경에 올려서 테스트해볼 수 있다는 장점이 있습니다.
2. 에러 처리
go의 에러 처리는 직관적입니다. 파이톤의 경우와 비교를 해봅시다. 정수 값이 2 이하면 Exception을 내보내는 함수를 만들고, 그 함수를 사용하는 파이톤 예제를 만들어 보겠습니다.
동일한 코드를 go로 구현해 보겠습니다.
터미널에 나오는 결과는 동일합니다.
가장 큰 차이는 go 같은 경우 에러를 변수로 돌려준다는 점입니다. 에러가 발생했는지 확인해 보고 싶다면 에러가 nil인지만 확인하면 됩니다. 일반적으로 고급 언어에서 사용하는 try, catch 방식이 아닌 인자로 에러를 넘기는 방식을 사용하면 복잡한 exception 처리도 일련의 코드로 할 수 있습니다.
3. 태그 사용
Go 언어는 정적 언어 입니다. 변수별로 타입을 지정해야 합니다. 정적 언어는 빌드(compile)할 때, 정적 분석을 통해 문법 오류를 잡아내는 장점이 있지만 유연하지 않다는 단점이 있습니다. 이런 단점을 보완하기 위해 Go에서는 동적으로 변수를 생성하거나 관리할 수 있는 인터페이스, 리플렉트, 태그를 제공합니다. 그 중 Go에만 있는 특수한 기능인 태그를 설명해 드릴까 합니다.
Go는 C언어와 비슷하게 클래스 개념으로 “type 타입명 struct”를 사용하는데, 위 코드와 같이 Person struct를 type으로 정의했다고 해봅시다. Name, Age는 타입으로부터 생성되는 인스턴스의 프로퍼티라 보시면 됩니다. Name, Age 정의 뒤에 json:”name” 형식으로 들어가 있는 부분이 tag 입니다. 이 json tag는 Go의 내장 라이브러리인 “encoding/json” 라이브러리를 사용하기 위한 태그입니다.
main 함수에 있는 json.Unmarshal은 json 정보를 담은 byte array를 person 객체에 넣어주는 역할을 합니다. json의 key가 type에 태그로 정의가 되어 있는 key가 일치할 경우, person 객체에 값을 넣어줍니다. 실행 결과를 보면 객체에 값이 잘 들어간 걸 알 수 있습니다.
태그를 해석하는 패키지(라이브러리)나 함수를 만들려면, reflect라는 녀석을 알아야 합니다. 여기서 리플렉트까지 다루게 되면 내용이 길어지므로 생략하겠습니다. 기회가 있으면 다음에 다루어 보겠지만, 우선 궁금하시다면 해외 블로그를 검색하면 예제가 많으니 참고하시면 됩니다. 대신 태그를 사용한 패키지(라이브러리)를 몇 개 소개해 드릴게요.
- ㅌgorm: ORM 라이브러리. ORM에서 DB column 명과 설정을 매칭 시키는 역할에 태그를 사용한다.
- validator: struct의 각 필드에 할당된 값을 validation하는 패키지. struct의 필드별 validation 기준에 태그를 사용한다.
4. goroutine
goroutine은 concurrency를 위해 go runtime에서 직접 관리하는 lightweight 스레드입니다. 스레드라고 해서 OS 스레드를 의미하는 건 아니고, go runtime의 가상 공산에 존재합니다.
goroutine을 하나 생성하는 데 초기에 필요한 메모리가 2KB 밖에 안 되기 때문에 기존 OS thread를 생성하는데 필요한 1MB와 비교했을 때 비교가 안 될 만큼 작습니다. 1,000개를 동시에 생성한다고 생각하고 계산해보세요. 각각 2MB와 1GB의 메모리를 차지합니다. 차이가 크죠?
Goroutine은 2KB로 현재 goroutine 유지가 힘들다고 판단되면, 메모리의 다른 영역에 X2 만큼의 영역을 확보해 복사하는 방식으로 메모리 관리를 해줍니다. 간단한 goroutine을 사용하는 프로그램을 만들어 봅시다.
“//”가 들어간 부분은 주석이므로 우선 무시하셔도 됩니다. go hello() 부분은 새로운 goroutine을 생성해, hello() 함수를 실행하라는 뜻입니다. world()는 main goroutine에서 실행되고요. 위 코드를 실행하면 hello, world가 전부 찍힐 것 같지만 world 만이 콘솔에 찍힙니다. main() 함수 내부는 main goroutine이 실행되는 영역인데 이 부분이 종료되면 main goroutine에서 생성된 goroutine은 무시가 됩니다. 그래서 world만 나오는 것입니다.
그렇다면 주석을 제거해 볼까요? 그러면 1초 정도 main goroutine이 sleep을 하게 되는데, 이는 go hello()가 실행되기 충분한 시간인 것 같네요. 역시 제거하고 실행해보면 hello와 world 모두 console에 찍힙니다. 사실 좀 찝찝하지 않나요? 사실 이렇게 감으로 시간을 추정해 병렬 코드가 돌아가도록 기다리는 방식은 초보적인 방법입니다.
실행 시간이 긴 goroutine의 경우 이렇게 코드를 작성하면 실행되는 중간에 main goroutine이 종료되어 빛을 보지도 못할 수도 있습니다. goroutine을 사용하면서도, 확실히 끝날 때까지 기다리는 방법이 없을까요? 이렇게 하려면 새로 생성된 goroutine에서 main goroutine으로 “야, 나 끝났어” 하고 신호를 보내는 방법이 필요합니다. 이 역할을 하는 게 채널(channel)입니다. 우선 채널을 사용한 코드를 보시죠.
위 코드를 보면 task라는 채널 변수를 만들어 사용합니다. 채널은 goroutine 사이에 공유되는 변수이고, 들어온 순서대로 밀어내는(FIFO) 큐(queue) 버퍼입니다.
make(chan int)는 int가 담기는 채널 변수를 생성하는 코드입니다. hello() 함수를 실행하기 위해 goroutine을 생성하고 task를 함수의 인자로 보냅니다. 그리고 main 함수는 계속 진행되겠죠. “<-task”를 다음으로 실행하는데, 이는 task 채널에 값이 들어올 때까지 기다리고, 들어오면 그 값을 내보내라는 의미를 가집니다.
hello 함수 내에 “Hello”가 인쇄되고 task<-0이 실행되기 전까지 main goroutine는 “<-task”에서 기다립니다. 이처럼 채널을 사용하면 main goroutine과 goroutine 사이에 통신을 쉽게 할 수 있습니다. 또한 다음과 코드와 같이 생성하면 2개의 버퍼 공간이 있는 채널 변수도 만들 수 있습니다.
즉 2개의 int는 미리 담아 놓을 수 있습니다. 버퍼 공간이 없는 경우, 채널 변수에 하나의 값을 받으면, 그 채널 변수는 그 값이 빠져나갈 때까지, 추가로 값을 못 받습니다. 이런 큐의 버퍼 특성은 병렬 작업에 대해, 최대 생성되는 goroutine 개수를 제한하는 카운팅 세마포어(counting semaphore)에 사용될 수 있습니다.
이처럼 go 언어는 concurrency 처리 코드를 쉽고 유연하게 작성할 수 있습니다. goroutine의 성능은 당연한 이점이고요!
마치며
지금까지 1년 동안 Golang을 사용하면서 느낀 장점을 적어보았습니다. 저희도 1년 밖에 사용해보지 않아 아직 전문가라 할 수 없지만 Golang을 사용해보고 싶거나 관심이 있는 분에게는 충분한 동기를 주지 않았나 하는 생각이 드네요. 동기를 더 부여하기 위해 Golang을 사용하는 서비스나 오픈소스 라이브러리를 소개하며 글을 마치겠습니다.
- Docker: 요즘은 모르시는 분이 없겠죠? 🙂
- 트위치(Twitch): 게임 스트리밍 서비스를 제공합니다. 엄청난 트래픽과 로드 덕분에 Golang의 발전에 기여한 서비스입니다. 최근에 Twirp라는 Golang 기반 RPC 프레임워크도 공개했습니다.
- Kubernates: 디플로이 자동화, 스케일링, 컨테이너화된 애플리케이션의 관리를 위한 오픈 소스 시스템입니다. 원래 구글에 의해 설계되었고 현재 리눅스 재단에 의해 관리된다고 하네요.
- Prometheus: pull 방식의 오픈소스 모니터링 시스템.
- Google: golang은 구글이 만든 언어입니다.
그 외 우버, 드롭박스, 트위터 등 저명한 서비스도 부분적으로 golang을 사용합니다.
원문: 매직테이블의 브런치