2016년은 다사다난한 해였던것 같습니다. 개인적으로도, 사회적으로도 말입니다 :) JVM 위에서만 놀던 제가 Golang (이하 Go) 을 배운것을 보면요. 이 글에서는 1달 남짓한 기간동안 Go 를 배우며 들었던 느낌들을 튜토리얼 형태로 적어보았습니다.
사실 Go 를 처음 봤을땐 그다지 탐탁치 않았습니다. 많은 이들이 말하듯 낡은 C 언어를 현대적으로 포장된 했다는 느낌을 많이 받아서 내게 필요할까 의문이 들기도 했었구요. 구체적으로는
$GOPATH
아래 코드를 위치하도록 하는 이상한 강제사항도 있을뿐만 아니라Go 는 필요 없다고까지 느꼈던 제가 Tutorial 을 따라 읽고, 컨퍼런스 영상까지 찾아보며 새로운 언어를 배운 이유는 팀을 옮기며 맡게된 업무에서 기존에 사용하던 언어 (Java, Scala) 의 한계를 절실히 느꼈기 때문입니다. JVM 위에서 돌아가는 대규모 오픈소스 프로젝트에서는 다음과 같은 문제들이 종종 발생하곤 합니다.
모든 작업에 앞서서 제가 가장 먼저 하는 일은 “빠른 피드백 루프를 갖추기” 입니다. 테스트를 작성하거나, 기능을 변경하거나 심지어 버그가 발생해 가설을 세우는 상황에서도 피드백 루프가 간단하다면 몇 번이고 실험하는데 부담이 없습니다. 이것이 우리가 복잡한 로직에서 println 를 수십번 적는 대신 debug 버튼을 클릭하는 이유입니다.
그런데, 빌드가 느리면 답이 없습니다. 단위 테스트야 IDEA 에서 실행한다고 해도 PR 이 머지되려면 CI 에서 돌아가는 통합 테스트는 물론이고 라이센스 검증 등 수 많은 단계를 거쳐야 합니다. 빌드가 느리면 이 모든 과정이 느려지고, CI 큐가 꽉차고 피드백을 위해 더 많이 기다려야 하고, 머지가 느려지고, 이 PR 을 기다리는 다른 PR 에 영향을 주고, 전체적인 개발 속도가 느려집니다.
“우리는 만드는 방법을 혁신하지 않고서는, 제품을 혁신할 수 없습니다.”
자유도가 높은 문법은 같은 일을 하는 다양한 스타일의 코드를 만듭니다. Scala 에는 심지어 Principle Of Least Power 라는 가이드 마저 있습니다. 결국 많은 기능을 가진 언어로 작성된 PR을 리뷰한다는 것은 읽어야 할 것이 많고 시간이 부족한 리뷰어에게 큰 부담이 됩니다. 또한 컨트리뷰터는 코드를 작성하고 떠날 수 있지만, 그가 작성한 코드는 남아 유지보수의 대상이 됩니다.
바꾸어 말하면, 누가 봐도 어떤 일을 하는 코드인지 쉽게 알 수 있고, 변경과 수정이 용이한 idiomatic 한 코드를 누구나 작성할 수 있는 언어가 필요합니다.
When reading code, it should be clear what the program will do.
When writing code, it should be clear how to make the program do what you want. by Rob Pike in Simplicity is Complicated
Error 는 Go 의 철학을 보여주는 한 예입니다. Go 에서는 Exception 이 없습니다. Exception 은 만들어진 장소에서 처리되지 않는다면 GOTO 문과 같아 코드를 읽기 어렵게 만듭니다. 대신, Go 는 Error 를 값으로 취급해 항상 리턴하도록 하여 코드가 어떤 예외 처리를 하는지 알기 쉽게 만듭니다. 사실 Exception 을 즉시 처리되어야 하는 것으로 본다면, 다른 곳에서 처리되야 할 이유가 없습니다.
… exceptional code is hopelessly serial. There is only one exception in flight, how quaint is that? There can be only one exception at any moment in flight. … [they] require immediate an exclusive attention. [The exception] comes to the fore, you must handle it right now.
동일한 스타일의 코드를 강제하기 위해서, 우리는 많은 오픈소스에서 lint tool (e.g checkstyle) 등을 사용하는 것을 보아 왔습니다. 그러나 이것은 validation 일뿐 reformatting 이 아닙니다. 수정하려면 사람의 노력이 필요합니다. reformatting 을 위한 scalafmt 이나 google-java-format 툴들은 많은 문법을 지원해야 하기 때문에 성숙도가 떨어집니다. 이 툴들을 이용해 수정된 코드를 보고 있노라면 Idiomatic 하다고 말하긴 어렵습니다.
Java 는 50개 정도의 키워드를 가지고 있고 Scala 는 그것보다는 좀 더 많습니다. 반면 Go 는 25개의 키워드로만 구성되어 있습니다. 문법이 간단하므로 idiomatic 한 코드를 작성하기 쉽고, 완성도 높은 gofmt 를 만들 수 있습니다. 우스갯소리로, 어느 누구도 gofmt 의 스타일을 좋아하지 않는다는 이야기도 있지만, 적어도 통일성 있는 스타일링을 사람의 도움 없이 기계적으로 수행할 수 있습니다.
Gofmt’s style is no one’s favorite, yet gofmt is everyone’s favourite.
Java 와 Scala 는 훌륭한 언어입니다. 많은 사람들이 즐겨 사용하는 Spring, Play, Akka 와 같은 좋은 프레임워크를 가지고 있고, 안정적입니다. 그 외에도 수 많은 장점이 있습니다만, 이 두 부분에서 미흡합니다. 솔직히 말하자면, 저는 2016 년에도 이런 상황에서 개발해야 된다는 사실이 믿기지 않습니다. 더 놀라운 사실은, 각 언어들은 현란한 기능을 추가하고 자신을 뽐내는것에만 바쁘지 우리가 매일 매일 겪는 진짜 문제를 해결하지 하려 않는다는 것입니다. 이런것들은 결국 커뮤니티가 해결해야 될 숙제로 남습니다.
Leading edge language features don’t usually address what you really want. Golang is designed for large applications, large teams and large dependencies. By Rob Pike Go at Google
언어를 배우는 데에는 다양한 방법이 있습니다. 저는 주로 간단한 튜토리얼을 읽은 후 언어의 철학과 디자인을 살펴보고 충분한 동기를 얻은 후에야 다른 기능들을 살펴보곤 합니다. 다행히도, Go 블로그, Go 언어 홈페이지에는 Google Go 팀이 작성한 읽을거리들이 많습니다. 그 중에서 꼭 봐야 할 몇 가지를 소개합니다.
언어의 문법이 눈에 익고 문법 설계의 이유를 알고 나면 자신이 사용했던 언어와의 차이점을 묻게 됩니다. Go 언어 초보자로서 제가 가장 먼저 궁금했던 내용은 Struct, Interface, Concurrency, Error Handling 와 관련된 것들이었습니다. 좋은 아티클들이 많기 때문에 링크를 걸도록 하고 설명은 제가 특이하게 느낀 점들만 간단히 적도록 하겠습니다.
Go 언어에는 멤버 변수를 가진 Struct
는 있지만 멤버 함수까지 포함한 Class 는 없습니다. 대신 Struct
의 멤버 함수처럼 쓰일 수 있는 Method 나 Pointer Recevier 가 있습니다.
type Vertex struct {
X, Y float64
}
func (v Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
func (v *Vertex) Scale(f float64) {
v.X = v.X * f
v.Y = v.Y * f
}
또한 Inheritance 를 위한 직접적인 키워드가 없습니다. Embedding 을 이용할 수 있습니다. 예를 들어 아래 코드에서는 Job
내에서 log.Logger
의 메소드를 호출하는 것을 볼 수 있습니다.
type Job struct {
Command string
*log.Logger
}
job.Log(“…”)
If C++ and Java are about type hierarchies and the taxonomy of types, Go is about composition
이런 문법들을 보고 있노라면, Go 에서는 해당 Data (Struct
) 가 와 Action (Method
) 은 별개인 것 처럼 느껴집니다. 메소드의 정의도 Java 의 클래스와 달리 외부에 하기때문인데요, 마치 Scala 의 case class 를 값으로 이용하고 함수는 companion object 에만 정의하는 것과 비슷합니다. 그리고 상속대신 합성을 이용하는 부분은 더 유연해 보이기도 합니다.
Q. Hey gophers, what was the best/worst moment of your experienes lenaring golang?
A. The worst was interface, but the best was also interface
Struct
가 데이터라면 Interface 는 메소드의 집합입니다. Struct
와 마찬가지로 Embedding 을 이용할 수 있습니다.
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
// ReadWriter is the interface that combines the Reader and Writer interfaces.
type ReadWriter interface {
Reader
Writer
}
Go 에서는 주로 작은 인터페이스, 즉 1개의 메소드만 선언하고 있는 인터페이스를 이용하는것이 권장됩니다. 이유는 해당 인터페이스를 쉽게 구현할 수 있어 유용한 구현체를 많이 만들 수 있기 때문입니다. 예를 들어 위에서 언급한 Writer
의 경우에는 bytes.Buffer
, io.multiWriter
, gzip.Writer
, net.Pipe
, net.Conn
, httpResponseWriter
등 수 많은 유용한 구현체를 가지고 있습니다. 예를 들어 이런 코드 작성도 가능합니다.
var but bytes.Buffer
…
var wr io.Writer = &buf
if test.chunked {
wr = internal.NewChunkedWriter(wr)
}
if test.compressed {
buf.WriteString("Content-Encoding: gzip\r\n") wr = gzip.NewWriter(wr)
}
...
아래는 인터페이스와 관련된 아티클들입니다.
Error values in Go aren’t special, they are just values like any other, and so you have the entire language at your disposal.
Error 와 관련해서는 읽을거리를 좀 많이 찾아봤습니다. 제가 이해가 안되었던 부분이 많아서 인데요, 각각의 내용은 길지 않으니 모두 읽어보셔도 좋을것 같습니다.
Go solves the exception problem by not having exceptions. … The decision to not include exceptions in Go is an example of its simplicity and orthogonality. Using multiple return values and a simple convention, Go solves the problem of letting programmers know when things have gone wrong and reserves panic for the truly exceptional.
Concurrency is about structure, while Paralleism is about execution
channel
과 goroutine
은 는 Go 가 자랑하는 기능중 하나입니다. 아래 나오는 자료들은 꼭 보시길 추천드립니다. Concurrency is not Parallelism 에서는 Concurrency 의 개념과 이를 구현하기 위해 Go 에서 사용가능한 빌딩 블럭들을 알아봅니다. Go Concurrency Patterns 에서는 Mock 형태의 Google Search 서비스를 만들고 select, timeout 등을 이용해서 개선하는 방법을 보여줍니다. Curious Channels 에서는 channel 등을 사용할 때 주의해야 할 부분에 대해서 이야기 합니다.
혹시나 여유가 되신다면 아래의 영상들도 보시길 추천드립니다. 특히 Complex Concurrency Patterns with Go 는 높은 성능이 요구되는 Kafka Client 를 Go 로 만들면서 겪었던 문제들을 이야기 합니다.
새로운 언어를 배울때는, 책을 읽는것도 좋은 방법이지만 다른사람의 경험으로 부터도 많은것을 배울 수 있습니다.
Go 에는 언어 자체적으로 제공되는 기본 툴들이 많습니다. Tooling in Action 에서는 이런 기본 툴 뿐만 아니라, 프로파일링에 사용할 수 있는 툴등 다양한 것들을 쉽게 설명합니다.
IDE 관련해서는 최초에는 Emacs 에 Spacemacs 를 얹고 Golang layout 을 이용했으나 불편함을 많이 느꼈습니다. 그 다음에는 Intellij 를 사용했었고, 가장 최근에는 Atom 을 이용하고 있습니다. 써보진 않았지만 VS Code 도 괜찮다고 들었습니다. IDE 는 무엇이든 본인에게 제일 편한걸 쓰는게 좋은것 같습니다.
언어는 도구일 뿐입니다. 따라서 문제에 맞는 도구를 골라 사용하면 될 일입니다. 다만, 저와 비슷한 문제들을 고민하고 계시거나, 위에서 언급했던 Go 의 특성 / 기능들이 괜찮다고 생각된다면 한번 도전해 보시는것도 나쁘지 않을것 같습니다 :)
위에 나온 모든 링크는 https://github.com/1ambda/golang 에서 모아보실 수 있습니다.