Skip to content

Commit

Permalink
프로덕션 서버 개발을 위한 golang 패키지 추천
Browse files Browse the repository at this point in the history
  • Loading branch information
winterjung committed Oct 13, 2024
1 parent eb3022e commit 5f48cc5
Show file tree
Hide file tree
Showing 2 changed files with 216 additions and 0 deletions.
216 changes: 216 additions & 0 deletions src/posts/2024-10-13-golang-pkgs-for-production-server.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
---
title: 프로덕션 서버 개발을 위한 golang 패키지 추천
image: images/golang-pkgs.png
---

전 직장에서 go 언어를 처음 쓰기 시작해 현 직장에서도 계속 쓰며 약 5년간 활발히 사용하고 있다.
예전과 비교하면 go 생태계는 점점 풍부해지고, [고퍼콘 코리아](https://gophercon.kr/ko)를 비롯한 다양한 커뮤니티도 늘어나고, 레퍼런스도 많아지고 있다.
그럼에도 여전히 (비단 go 언어만 그런 건 아니지만) 잘 정리된 한국어 자료는 적고 시행착오를 겪어봐야 하는 부분이 많아 그동안 여러 동료와 함께 개발하며 겪었던 시행착오와 권장 사항을 [프로덕션 환경에서 사용하는 golang과 gRPC](https://blog.banksalad.com/tech/production-ready-grpc-in-golang/), [뱅크샐러드 Go 코딩 컨벤션](https://blog.banksalad.com/tech/go-best-practice-in-banksalad/) 두 편의 글로 정리하기도 했다.
그와 비슷한 결로 이번엔 주로 go 언어로 서버 개발을 하며 자주 사용하고 유용했던 패키지<sub>라이브러리</sub>를 정리해 봤다.
go 언어에서 기본으로 제공하는 내장 패키지에도 [`net/http/httptest`](https://pkg.go.dev/net/http/httptest), [`crypto/rand`](https://pkg.go.dev/crypto/rand) 패키지처럼 믿고 쓸 수 있고 잘 만들어진 패키지는 많으나 이 글에선 서드 파티 패키지에 집중했다.

> 글을 쓸 때 마다 항상 드는 고민이 '이 정도면 "golang xxx package" 같은 키워드로 검색해도 금방 나올 텐데 괜히 적는 거 아닌가?'라는 부분이다. 그럼에도 누군가의 의사결정 비용과 고민을 줄여주길 바라며, 또 이 글을 읽은 다양한 사람들이 얹어주는 한마디씩을 통해 이 글이 더 발전되길 바라며 소개해 본다.
## [stretchr/testify](https://github.com/stretchr/testify)
```go
func TestSomething(t *testing.T) {
assert.Equal(t, expected, got, "they should be equal")
assert.NoError(t, err)
assert.Len(t, result, 1)
assert.JSONEq(t, `{"name": "hello"}`, msg)
}
```
> A toolkit with common assertions and mocks that plays nicely with the standard library
- go 언어의 강력한 테스트 기능을 더 풍부하고 작성하기 즐겁게 만들어 준다.
- `if got != expected { t.Errorf("...") }` 대신 `assert.Equal(t, expected, got)` 코드로 쓸 수 있어 좀 더 읽기 쉬워지는 측면도 있다.
- 주로 [assert](https://pkg.go.dev/github.com/stretchr/testify/assert), [require](https://pkg.go.dev/github.com/stretchr/testify/require) 패키지를 적극 사용한다.
- require 패키지는 assert와 거의 같은데 실패하면 그 테스트가 그대로 종료되는 점이 다르다.
- 아마 다른 언어를 쓰다 go 언어로 넘어왔다면 require 패키지의 동작이 좀 더 익숙할 수 있다.
- assert 패키지를 사용하면 이후 라인의 assertion까지 실행되기에 한 번에 여러 실패를 볼 수 있어 좀 더 디버깅을 도와주는 측면이 있다.
- given, when, then으로 나누어진 구조에서 client 초기화나 성공해야만 하는 given 부분에선 require를 애용하기도 한다.
* testify엔 다양한 assertion 함수가 있으니 이를 최대한 활용해 보길 추천한다.
* [suite 패키지는 객체 지향 언어가 익숙했던 사람을 위해 제공되는 패키지](https://github.com/stretchr/testify#suite-package)라 처음 시작하는 사람은 assert 패키지만으로도 충분하다.
* 현재 v1 기준으로 병렬 테스트를 지원하지 않아 이 점은 참고해야 한다.
* `SetUp`, `TearDown`보단 [table driven tests](https://go.dev/wiki/TableDrivenTests)를 하자.
* [Antonboom/testifylint](https://github.com/Antonboom/testifylint) 린터와 같이 사용해도 좋다.

## [rs/zerolog](https://github.com/rs/zerolog) & [sirupsen/logrus](https://github.com/sirupsen/logrus)
```go
import (
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/rs/zerolog/pkgerrors"
)

func main() {
zerolog.ErrorStackMarshaler = pkgerrors.MarshalStack
// ...
log.Error().Stack().Err(err).Msg("failed to insert row to db")
}
```
> rs/zerolog: Zero Allocation JSON Logger
```go
import (
log "github.com/sirupsen/logrus"
)

func main() {
log.WithFields(log.Fields{
"animal": "walrus",
}).Info("A walrus appears")
}
```
> sirupsen/logrus: Structured, pluggable logging for Go.
- 두 로깅 패키지 모두 contextual하고 structed한 로깅을 지원한다.
- 서버에서 로깅을 할 땐 언제나 one line json logging을 하길 권장한다.
- zerolog가 좀 더 성능이 좋다고 하나 둘 다 프로덕션 레벨에서 사용하기 좋은 패키지다.
- logrus는 메인테넨스 모드이나 이미 기능이 풍부하고 보안 패치는 계속 이루어지는 점은 참고해야 한다.
- 개인적으론 logrus가 `WithFields` 메서드 때문에 zerolog 대비 사용성이 좋았다.
- 둘 다 hook을 지원해 로그와 함께 메트릭을 찍을 때나 스택트레이스를 주입해주는 용도로 쓰기 좋았다.
- go 1.21 이상 버전부터 표준 라이브러리로 포함된 `log/slog`도 사용해볼만한 대안이다.
- [Structured Logging with slog - The Go Programming Language](https://go.dev/blog/slog)
- [Logging in Go with Slog: The Ultimate Guide](https://betterstack.com/community/guides/logging/logging-in-go/)

## [pkg/errors](https://github.com/pkg/errors)
```go
resp, err := userSvc.GetUser(ctx, &GetUserRequest{...})
if err != nil {
return errors.Wrap(err, "user.GetUser")
}
```
> Simple error handling primitives
- 에러에 컨텍스트와 스택트레이스를 추가해준다.
- go 1.13 이전 시절 `errors` 표준 라이브러리의 기능 부족으로 생겼으나 [`Unwrap`, `Is`, `As`가 편입된 지금](https://go.dev/blog/go1.13-errors)까지도 de-facto로 사용된다.

## [hashicorp/go-multierror](https://github.com/hashicorp/go-multierror)
```go
var errs *multierror.Error
if err := step1(); err != nil {
errs = multierror.Append(errs, err)
}
if err := step2(); err != nil {
errs = multierror.Append(errs, err)
}
return errs.ErrorOrNil()
// Output:
// 2 errors occurred:
// * error 1
// * error 2
```
> A Go (golang) package for representing a list of errors as a single error.
- config나 요청의 유효성을 검사할 때 한 번에 좀 더 풍부한 에러 정보를 얻을 수 있어 종종 사용한다.
- 해당 패키지에 들어있는 `multierror.Group`도 고루틴을 fanout해 실행하고 각각의 에러를 취합해야할 때 사용하곤한다.
- 비슷하게 준 표준 라이브러리인 [golang.org/x/sync/errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) 패키지도 존재한다.
- 해당 패키지는 고루틴 중 하나에서 에러가 발생하면 다른 고루틴으로 에러가 전파되어 작업이 취소되니 상황에 맞게 취사선택하자.
- multierror는 모든 고루틴을 실행한 뒤 에러를 병합한다.

## [samber/lo](https://github.com/samber/lo)
```go
names := lo.Uniq([]string{"Samuel", "John", "Samuel"})
// []string{"Samuel", "John"}
```
> A Lodash-style Go library based on Go 1.18+ Generics (map, filter, contains, find...)
- go 1.18 이상에서 제네릭을 사용해 성능 손해 없이 좀 더 보일러플레이트 없는 코드를 짤 수 있게 도와준다.
- 주로 `Map`, `SliceToMap`, `Keys` 함수를 많이 사용했다.
- 예전과 달리 go 1.21부터 생긴 [maps](https://pkg.go.dev/maps), [slices 패키지](https://pkg.go.dev/slices)가 담당해주는 부분이 많아졌으니 참고해보면 좋겠다.
- 메인테이너의 다른 패키지로 의존성 주입을 위한 [samber/do](https://github.com/samber/do), 모나드를 위한 [samber/mo](https://github.com/samber/mo) 패키지가 있는데 개인적으로 선호하는 go 코드 컨벤션과 맞지 않아 사용하진 않았다.

## [shopspring/decimal](https://github.com/shopspring/decimal)
```go
func main() {
price, err := decimal.NewFromString("136.02")
quantity := decimal.NewFromInt(3)
subtotal := price.Mul(quantity)

fmt.Println("Subtotal:", subtotal) // Subtotal: 408.06
}
```
> Arbitrary-precision fixed-point decimal numbers in Go
- 다른 decimal 패키지로 [ericlagergren/decimal](https://github.com/ericlagergren/decimal)도 존재하나 사용성은 `shopspring/decimal`가 훨씬 좋았다.
- orm 라이브러리의 decimal 타입과 잘 매핑된다.
- 성능이 중요하다면 readme에서 소개하는 다른 패키지와 함께 벤치마크를 돌려보길 권장한다.

## [dgraph-io/ristretto](https://github.com/dgraph-io/ristretto)
```go
func main() {
cache, _ := ristretto.NewCache(&ristretto.Config[string,string]{
NumCounters: 1e7, // number of keys to track frequency of (10M).
MaxCost: 1 << 30, // maximum cost of cache (1GB).
BufferItems: 64, // number of keys per Get buffer.
})
cache.Set("key", "value", 1)
value, found := cache.Get("key")
fmt.Println(value)
}
```
> A high performance memory-bound Go cache
- 프로덕션에서 고성능 로컬 캐시가 필요할 때 무난히 쓸 수 있다.
- 패키지의 탄생 배경과 주안점은 [Introducing Ristretto 블로그 글](https://dgraph.io/blog/post/introducing-ristretto-high-perf-go-cache/)을 참고하자.
- 최적의 설정은 상황에 따라 다르므로 여러 메트릭과 벤치마크를 통해 결정하길 권장한다.
- 제네릭도 지원한다.
- [patrickmn/go-cache](https://github.com/patrickmn/go-cache) 패키지는 제네릭을 지원하지 않고 유지보수 되지 않고 있어 추천하지 않는다.
- 대용량 트래픽에서 좀 더 성능 튜닝이 필요하다면 [creativecreature/sturdyc](https://github.com/creativecreature/sturdyc), [maypok86/otter](https://github.com/maypok86/otter) 패키지도 참고해볼 수 있다.
- 성능을 위해선 로컬 캐시에서 무엇이 중요한지 [Writing a very fast cache service with millions of entries in Go](https://blog.allegro.tech/2016/03/writing-fast-cache-service-in-go.html) 블로그 글에서 다루고 있으니 읽어볼 만하다.

## [volatiletech/sqlboiler](https://github.com/volatiletech/sqlboiler)
```go
import (
"github.com/volatiletech/sqlboiler/v4"
)
func main() {
users, err := model.Users().All(ctx, db)
token, err := model.Tokens(model.TokenWhere.AccessToken.EQ(accessToken)).One(ctx, db)
}
token.Update(ctx, db, boil.Whitelist(
model.TokenColumns.AccessToken,
model.TokenColumns.AccessTokenExpiredAt,
))
```
> Generate a Go ORM tailored to your database schema.
- go 코드로 db 모델을 관리하는게 아닌, 이미 존재하는 db 모델을 따르는 go 코드를 생성해준다.
- 쿼리를 만들 때도 string을 직접 사용하지 않고 이미 생성된 타입을 그대로 사용할 수 있어 경험이 좋았다.
- 다른 패키지도 많은데 개인적인 경험으론 [sqlc-dev/sqlc](https://github.com/sqlc-dev/sqlc)는 사용하기가 불편했고, [go-gorm/gorm](https://github.com/go-gorm/gorm)는 취향에 맞다면 사용해볼만 하다 느껴졌다.

## [DATA-DOG/go-sqlmock](https://github.com/DATA-DOG/go-sqlmock)
```go
func TestShouldUpdateStats(t *testing.T) {
db, mockDB, err := sqlmock.New()
require.NoError(t, err)
t.Cleanup(db.Close)

mockDB.ExpectQuery(regexp.QuoteMeta(
"SELECT * FROM `token` WHERE (`user_id` = ?);"
)).WithArgs("some-valid-user-id").WillReturnRows(...)
}
```
> Sql mock driver for golang to test database interactions
- mysql, postgresql 등을 쓸 때 의도한 sql 문이 수행되는지 유닛테스트 하는 용도로 쓸만하다.
- 쓰다보면 패키지 경로가 이상한데 싶긴 한데 그래도 별 문제는 없다.
- '왜 사람이 직접 sql 문을 쓰고 검사해야하지?!' 싶은 사람에겐 추천하지 않는다.

## 그 외
- 슬랙으로 뭔가를 보내야 한다면 [slack-go/slack](https://github.com/slack-go/slack)을 쓰자.
- github api가 필요하다면 [google/go-github](https://github.com/google/go-github)을 쓰자.
- 버전업이 잦으니 틈틈이 업데이트 해주자.
- sentry가 필요하다면 공식 문서를 참고해 [getsentry/sentry-go](https://github.com/getsentry/sentry-go)를 쓰자.
- 참고로 그냥 쓰면 에러 그룹핑이 잘 안돼 튜닝이 필요한데 이 부분은 별도의 블로그 글로 다루고자한다.
- 고루틴에서 map에 동시에 써야한다면 [syncmap](https://pkg.go.dev/golang.org/x/sync/syncmap)을 쓰자.
- 참고로 map은 동시 쓰기를 시도하면 panic이 나서 알아채기가 쉬운데 slice는 그렇지 않아 감지하기 어려우니 주의하자.
- 시맨틱 버저닝으로 뭔갈 해야한다면 [Masterminds/semver](https://github.com/Masterminds/semver)를 쓰자.
- uuid가 필요하다면 [google/uuid](https://github.com/google/uuid)를 쓰자.
- uuid v7도 지원한다.
- kubernetes에 배포한다면 [uber-go/automaxprocs](https://github.com/uber-go/automaxprocs)로 cpu 세팅을 맞춰주자.
- 안그러면 pod에 할당된 cpu 리소스는 1인데 32가 들어가있고 그런다.
- statsd 메트릭을 찍는다면 [smira/go-statsd](https://github.com/smira/go-statsd) 를 쓰자.
- 쓰는 쪽에서 별도의 인터페이스를 선언해 noop client 구현과 함께쓰길 추천한다. 그래야 테스트가 편하다.
- redis를 쓴다면 [redis/rueidis](https://github.com/redis/rueidis)를 쓰자.
- 빌더 패턴이 호불호가 갈릴 수 있는데 성능이 좋다.
- 그냥 [redis/go-redis](https://github.com/redis/go-redis)를 써도 기본은 한다.
- 유닛테스트를 하고 싶다면 [alicebob/miniredis](https://github.com/alicebob/miniredis)와 같이 쓰자.
- 통합 테스트를 한다면 [testcontainers/testcontainers-go](https://github.com/testcontainers/testcontainers-go)도 쓸만하다.
- 통합 테스트 방법은 조직마다 상황에 따라 많은 방법이 있겠으나 테스트컨테이너도 옵션 중 하나로 고려해보기 좋다.

---

이렇게 그 동안 자주 사용하고 유용한 패키지를 정리해봤다. 위 내용은 서버 개발에 치중되어 있고, 너무 지엽적인 패키지(e.g. 레벤슈타인 거리 구하는 패키지)는 제외했는데 이 외에도 추천할만한 패키지가 있다면 계속 업데이트 할 예정이다.
Binary file added static/images/golang-pkgs.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 5f48cc5

Please sign in to comment.