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를 사용하지는 않을 것 같다.

댓글

  1. 장단점을 잘 정리해주신, 진짜 제대로 사용하고 쓴 글로 느껴지네요.
    잘 읽었습니다. 많은 참고가 되었습니다.

    답글삭제

댓글 쓰기

이 블로그의 인기 게시물

[C++] enum class - 안전하고 쓰기 쉬운 enum

RAII는 무엇인가

Log Aggregator 비교 - Scribe, Flume, Fluentd, logstash

[Python] cache 데코레이터로 최적화하기

[Web] SpeechSynthesis - TTS API