사진 출처: https://tv.naver.com/v/16972079
시작하며
원래 간간히 재미삼아 개발 컨퍼런스들을 찾아보곤 해왔는데 그냥 쓰윽 듣기만 하니까 머리에 남는 게 별로 없는 것 같아 가끔은 이렇게 내용을 정리해보는 글을 써볼까한다. 나는 학교 시험과는 그닥 맞지 않는 스타일 같아서 시험 기간이 되면 더 이런 실무적인 내용들이 괜히 더 끌리는 것 같다. 소프트웨어 공학 수업을 듣다가 지루해져서 네이버 d2를 돌아다니던 중 네이버 데뷰 2020에 Golang 관련 영상이 있길래 “오잉?! 네이버 채용에 가끔 Golang이 뜨긴 하던데 Golang 어떻게 쓰려나?!” 싶은 마음에 영상을 시청해봤다.
내용을 간단히 요약하자면 기존에는 C++로 검색 엔진 라이브러리를 개발해왔고 해당 라이브러리를 이용하는 C++ 검색 서비스를 개발해왔다고 하는데, 이를 Golang으로 마이그레이션하면서 성능은 유지하면서 Golang 특유의 간결하고 강력한 기능들(unit test, benchmark, fmt, build 등등)과 함께 데브옵스 문화에 잘 녹아들 수 있었고 팀원들도 편리함을 느끼며 생산성까지 잡을 수 있었다는 이야기이다.
본 글은 https://tv.naver.com/v/16972079 영상을 바탕으로 해당 내용을 정리해본 글입니다.
검색 서비스의 특징
자세히 검색 엔진이 어떻게 구성되어 있는지, 내부적으로 데이터를 어떻게 저장하고 질의하는지, 검색 서비스는 검색 라이브러리를 어떻게 사용하는지 등등 이런 내용들을 잘 몰라서인지 자세히 검색 엔진이나 검색 서비스가 어떤 식으로 동작하는지, 어떤 형태로 개발되는지에 대해서는 감이 잘 잡히지 않는다. 혹시 자세히 코드를 볼 수 있었다면 좋았겠지만, 아무래도 그러기는 힘들겠지…ㅜㅜ
아무튼 검색 서비스는 단언 성능이 중요했으며 주로 질의를 분석하고, 검색 결과를 정렬하는 방식, 랭킹에 대한 모델링 등의 로직이 구현되어 있다고 한다.
각 서비스들은 자신들의 고유한 검색 로직을 구현하고, 실제 검색을 수행하는 것은 검색 엔진 라이브러리라고 한다. 점점 검색 엔진의 규모와 신규 검색 서비스들이 증가하게 되면서 관리가 어려워졌다. 서비스들의 크기와 수가 증가하면서 높은 생산성을 유지하기 위해 개발 프로세스를 표준화하고 자동화했지만 추가적으로 개발 언어적인 관점에서의 고민이 들기 시작했다고 한다.
개인적으로는 아무래도 서비스가 많아질 수록 데브옵스 문화가 많이 녹아들어야한다고 생각하는데 아무래도 많은 것들을 자동화하기 위해서는 언어 차원에서 강력하고 편리한 CLI 기능들일 잘 지원되어야한다고 생각하고, 지속적인 통합을 위해서도 편리한 테스트가 지원되어야한다고 생각한다. C++을 많이 해보진 않아 잘 모르겠지만, 아무래도 Go가 간단한 명령어들을 통해 IaC도 곁들이며 많은 작업들을 자동화하기 더 편리하고, unit test를 작성해가며 TDD식으로 개발하기에도 편리하지 않을까 싶다.
개인적인 의견이었고, 어쨌든 이 팀에서는 결국 효율적으로 DevOps 문화에 녹일 수 있으면서 어느 정도 높은 성능이 보장되고 생산성을 유지하기도 쉬운 언어를 생각하게 되었고 Go가 적합하다고 판단했다고 한다.
Go의 특징
(이해를 돕고자 발표 PPT에서 하나 캡쳐해왔습니다.)
성능이 높으면서 GC를 지원한다.
출처 - 디스코드 블로그(https://blog.discord.com/why-discord-is-switching-from-go-to-rust-a190bbca2b1f)
- 과거 디스코드에서는 Go의 GC로 인해 간혹 CPU가 Spike치는 경험을 했고 Rust로 옮겨갔다는 이야기가 있는데, 이에 대해서는 Go community에서도 다양한 의견이 있었던 것 같다. 최근 버젼의 Go에서는 해당 이슈가 픽스되었다는 의견도 있고, 그 정도 Spike가 있어도 GC가 가져다 주는 생산성이 더 크기 때문에 감수해야한다는 의견도 있었던 것 같다. 하지만 여태까지는 네이버에서 Golang을 이용하면서 모니터링해봤을 때는 GC로 인한 말썽은 없었다는 것 같다!
Pointer의 개념이 여전히 존재하지만 배우기 쉽다.
- 이 부분 같은 경우는 내가 Docker를 처음 배우던 시절에 느꼈던 것과 유사한 것 같다. 사용법이 어려운 게 아니라 그게 왜 필요한 지 이해하고 잘 사용하는 게 어려운 느낌이다.
- 근데 Go가 어려운 것은 보통 “어떻게 해야 Concurrency programming의 장점을 잘 이끌어 낼 수 있을까?”, “channel을 통한 communication, synchronization을 어떻게 해야 잘 이용할까?” 와 같은 내용들인데 물론 Go가 그런 부분에서 장점을 가진 것은 맞지만 이런 부분을 꼭 이용하지 않더라도 이제는 Go가 충분히 그 외의 매력도 많다는 것이 점점 인정받고 있는 것 같아 다양하게 사용되고 있는 것 같다. 예를 들어
prometheus
나docker
cli 같은 Go로 작성된 유명한 오픈소스들의 코드를 까봐도 그닥 ‘와 이걸 동시성을 이용해서 풀어냈네..!’, ‘와 이걸 channel들로 pipeline을 만들어서 진행하네?!’ 이런 부분은 찾기 힘들었던 것 같다.
패키지 관리가 쉽다.
- 따로 npm이나 pypi 같은 곳에 업로드 할 필요도 없고 Git을 기반으로 해서 release나 commit 단위로도 패키지의 버젼을 지정할 수 있어 개인적으로도 아주 맘에 든다!
- 패키지를 설치할 때에도
$ go get ...
명령어를 이용하면 되지만 보통은 그냥Jetbrains
사의Goland
IDE에서 그냥 Alt + Enter를 연타해서 이용 중이다 ㅎㅅㅎ
Unit test를 기본적으로 지원해주고 테스트 하기가 아주 편하다.
- 내가 처음 TDD를 시작하고 Unit test 작성을 시작했던 것도 Go를 공부하면서 였다! 몇 년 전 node.js를 막 공부했을 때 Test code를 짜볼까싶어서 test framework들에 대해 알아봤는데 그냥 테스트 프레임워크를 고르는 것부터가 힘들어서 ‘에잇…ㅎㅎ 테스트는 다음에..ㅎㅎ’ 했던 기억이 난다.
- 요즘은 주로 Spring으로 Test code를 짜고 있다. 너무나도 편리하다. 하지만 Spring은 그런 편리함을 누리기 위해 알아야할 내용이 너무도 많은 게 사실이다. test시에는 어떤 profile을 이용할지, Configuration은 어떤 걸 사용할 것인지, Bean을 어떻게 초기화 할 것인지 등등을 설정해줘야하고 어떻게 동작하는지 알아야한다. 그리고 솔직히… 한 번 그렇게 공부하고 난 뒤에 테스트를 짜다가 다른 Layer의 코드에 대한 테스트를 작성하려면 다시 ‘음..? 어떻게 동작하더라…..’ 하고는 다시 내용들을 찾아봐야하는 경우도 많았던 것 같다.
- 반면 Go는 간결하고 직관적으로 쉽게 쉽게 테스트 할 수 있다. 별로 고민할 것들도 없다.
- 다만 초큼 아쉬운 건 mocking을 하려면 다 interface로 선언해야한다는 것과 mocking type을 자동으로 관리하기 힘들고 mocking library를 선택하고 익혀야한다는 것..?
SWIG를 도입했다.
C/C++ 등의 라이브러리를 Go(혹은 그 외의 다양한 언어)에서 사용할 수 있도록 추상화 시켜주는 오픈소스
SWIG에 대한 설명을 하긴 하지만 결국 요점은 C++ ⇒ Go 로 넘어갈 때 기존 코드를 모두 버린 것이 아니라 다른 방법은 없을까 알아보고 적절한 기술을 빠르게 채택했다는 점이 중요한 점 같다.
만약 C++⇒ Golang을 고려중인 기업이나 개발자분이 계시다면 좋은 참고자료가 될 것 같긴하다.
나에게는 별로 필요 없으니 패스하겠다~!
SWIG를 이용했지만 그럼에도 존재했던 Go와 C++의 언어적 차이
Go에서 C++로 마이그레이션하면서 목표는 “Go는 Go답게!” 이용하는 것으로 정했다고 한다. 이 파트도 거의 SWIG 특화 내용이라 패스하도록 하겠다.
다만 무슨 작업을 할 때 저런 식으로 슬로건이나 목표를 정해놓고 나아가는 것은 좋은 방향인 것 같았다.
가끔 작업을 하거나 프로젝트를 진행하다보면 일관성이 흐려지는 경우가 있는 것 같다. 예를 들면 얼마전 토스의 컨퍼런스에서도 결제 SDK, API를 만들면서 API Naming 같은 것을 할 때의 팀 내 룰을 설명해줬던 것 같다. 그 정도로 협업을 할 때 일정한 룰을 정해놓고 그것을 준수하는 것은 참 중요할 것 같다는 게 다시 한 번 와닿았다!
Shared memory를 이용해 멀티 프로세스로 작업하던 기존 방식
기존의 Apache MPM Prefork를 이용하던 방식에서는 멀티 프로세싱을 통해 병렬성, 동시성을 이용했다고한다. 이때 여러 프로세스가 공유하는 Shared cache를 이용했고 이 캐시는 Key-value 형식의 Map이었다고 한다.
하지만 Go는 멀티프로세싱 방식보다는 Goroutine을 이용한 동시성 프로그래밍과 channel을 이용한 goroutine간의 communication을 권장한다.
기존에는 multi-process에서 shared memory 형태의 shared cache를 이용했는데 이것을 유사하게 이용할 방법을 잘 찾지 못했다고 한다. 그 이유는 Goroutine간의 데이터 공유는 직접 공유가 아닌 channel을 이용한 communication 방식을 이용할 것이 대체로 권장(즉 기존의 방식과 너무 다른 방식)되었고, 그 이외의 방법들은 서버/클라이언트 방식이나 gRPC 방식이었기 때문이라고 한다. 그리고 결과적으로는 이런 방식들보다는 Memory상의 SQLite를 이용하기로 했다고 한다.
아마 여러 Goroutine이 동시적으로 접근해도 SQLite도 나름의 DB라서 여러 goroutine이 동시적으로 동작해도 동시성 문제가 없을 것이고, 드라이브가 아닌 메모리를 바탕으로 하도록 설정도 가능해 속도도 빠르기 때문이 아닐까 싶다. 하지만 개인적으로는 여기서 이해되지 않는 내용이 꽤 많이 존재한다. Go는 goroutine간의 데이터 공유를 주로 communication을 이용할 것이 권장된다고 하셨는데 그런 경우는 다음과 같지 않을까 싶다.
- goroutine이 서로간에 데이터를 ping pong하며 주고 받으며 동작하는 경우
- 여러 스레드(혹은 고루틴 혹은 프로세스)가 동시에 이용 가능하도록 동기화가 지원되는 큐처럼 이용할 경우
- semaphore처럼 작업량을 조절하려는 경우
하지만 이런 communicate 방식 말고도 Go에도 충분히 Mutex가 제공되고 게다가 Read/Write에 대해 다르게 Lock을 걸 수 있는 기능도 있는데, 굳이 SQLite3를 사용할 필요가 있었을까 싶기도하다. 아니면 다른 관점에서 봤을 땐 Go의 Mutex 기능을 이용하는 것이 그리 쉬운 편은 아니라는 점에서 너무 Go스러운 방식을 적극 활용하는 쪽 보다는 그냥 일반적인 개발자들이 친숙한 SQLite와 Database적인 특징을 이용하는 쪽이 협업하기에 더 좋을 수도 있을 것 같다.
성능
Go가 C++ 보다 나은 성능을 보여주냐보다는 Go가 C++만큼의 성능을 낼 수 있는가의 관점에서 평가했다고한다. 그리고 Go는 기존의 C++ 검색엔진 만큼의 작업 처리량을 보여줬다고한다.
기본적으로 Go Binary가 큰 편이고 Shared memory를 이용하던 것을 SQLite3를 쓴 점에서 메모리의 차가 좀 크게 난 것 같다고 한다.
생산성이 정말 좋아졌는가?
4명이서 시작한 킥오프, 5개월만에 검색엔진을 Go기반의 모듈로 전환 완료
- Go는 C++에 비해 러닝 커브가 낮다고 생각
- 테스트를 짜는 것도 돌리는 것도 쉬움
- go build, go test, go test -bench, godoc 등의 기본 명령어를 통해 빌드, 테스트, 벤치마크, 문서화등을 이용가능
- gofmt나 goimport 통일된 기능과 같은 깐깐한 Compiler로 인해 깔끔한 코드 유지 가능.
- 패키징이 아주 간단하다. go.mod에 한 줄만 추가하면 됨. 소스도 까보기 쉬움.
C++ ⇒ Golang으로 마이그레이션 한 뒤 고객의 소리 (검색 엔진 라이브러리를 이용한 타 부서 사람들의 의견)
- 로직 파악이 쉽고, 다른 개발자의 코드를 Import 하기 쉬웠다.
- TDD를 적용해볼 수 있었다.
- 표준 라이브러리(marshaling 등등)이 파워풀했다.
- 단순하고 러닝 커브가 적어서 좋았다.
개인적으로는 확실히 Go 코드가가 엄청 읽기 쉽긴하다. 불필요한 코드는 애초에 컴파일러가 허가해주지 않으니 읽는 사람 입장에서는 모든 코드가 필요한 내용들로 구성되어있으니 간결하다. 또한 강한 Type 강제 언어답게 어떤 메소가드가 어떤 것을 리턴하는지, 어떤 인자를 필요로 하는지가 분명하다. 게다가 Type 언어이면서도 Java와 달리 앞서 말했듯이 간결하면서도 강력하고 통일된 기능들을 제공한다. 약간 자바는 기능도 엄청 많은 대신 설정해야하는 것도 너무 많아 번거로울 때가 있는 반면 Go는 “우리가 표준으로 정의한 대로 해!“라고 강제하는 느낌이 있는데 그 표준이 꽤나 깔끔해서 반박의 여지가 적은 것 같다. 마치 CISC(Complex Instruction Set Architecture)의 형태로 발전하다 “아 몰라! 너무 복잡해. 다시 간단하게 해!!!” 라며 RISC의 형태로 발전된 느낌이랄까…ㅎㅎ
총평 및 의견
실제로 기존 C++ 라이브러리를 Go로 마이그레이션 해보았으며, Go를 모르던 타부서 개발자들도 Go로 마이그레이션된 라이브러리를 무리 없이 이용했다는 썰이 흥미로웠고, 당근마켓이 그러했듯 네이버에도 어쩌면 Go 붐이 이를 수 있지 않을까싶다. 만약 그렇게 된다면 나중에 네이버에 지원하는 신입 중엔 꽤 Go를 잘 이해하고 잘 사용할 자신이 있을 듯 하다..!
성능이 C++보다 잘 나오느냐의 관점보다는 C++만큼의 작업 처리량을 낼 수 있느냐는 점도 좀 재미있었다. 이제는 Go도 그 매력을 인정받으며 점점 대중화되고 있기 때문에 “꼭 Go를 써야할 절대적인 이유가 있어? 다른 언어들도 그런 이유는 어떻게든 커버 가능하잖아.” 의 관점에서 바라보지 않는다는 것이다. Go가 다방면에서 좋은 장점이 있기 때문에 “꼭 Go만의 절대적인, 유일한 장점, 기능을 써먹는 케이스가 아니더라도 기존 작업량을 따라 갈 수만 있으면 성능도 괜찮고, 관리도 편리하고, 러닝 커브도 적고, 테스트도 쉬운 좋은 언어인데 도입해볼만하지 않아?” 라는 관점에서 접근하게 된 것이다.
실제로 Go를 적용해봤는데 발표자분이 속한 팀에서도 또 그 외의 부서에서도 사람들이 잘 따라온다고하니 역시 Go가 러닝 커브는 높지 않고 간결한 언어인 것 같구나 싶기도 하고, 사실 한 언어를 잘 하는 것도 힘들 수 있는데 곧잘 다들 Go라는 언어를 익히신다니 역시 Naver의 개발자들이구나 싶은 생각도 들었다.