포스트

2023-02-15-TIL

2023-02-15-TIL

Today I Learned

Go Clean Architecture

무엇이 Go스러운걸까? Go스럽다는 말이 과연 절대적인 가치일까? 이 말은 세부적인 코드의 부분부분에서는 맞는 말이기도 하다. 하지만 전체적인 애플리케이션 아키텍처에 있어서는 언어가 아키텍처를 주도하기는 어렵다. 언어의 고유한 특징이 어느 정도 반영되겠지만, 애플리케이션의 목적에 따라서 전체적인 아키텍처는 유사성을 띌 수 밖에 없다. 예를 들어, 엔터프라이즈 웹 애플리케이션의 경우, MVC, Layered Architecture 등 스프링이 강제하는 틀을 결국 따라가게 되는 경우가 많다.

자주 인용되는 bxcodec/go-clean-arch 예제는 Models, Repository, Usecase, Delivery의 네 계층으로 나뉜다. Models는 도메인 구조체, Repository는 DB 접근, Usecase는 비즈니스 로직, Delivery는 HTTP 핸들러 같은 입출력을 맡는다. 계층 사이는 인터페이스로 연결하고 의존 방향은 바깥에서 안쪽으로만 향한다. 스프링의 Controller, Service, Repository와 이름만 다를 뿐 역할 분담은 거의 같다는 점이 위 생각을 뒷받침한다. Go에서 다른 점은 인터페이스를 쓰는 쪽(usecase)에 선언하는 관례가 있고, 암묵적 인터페이스 구현 덕분에 구현체가 인터페이스를 알 필요가 없다는 것이다. 함께 본 ASP.NET Core 문서도 같은 구조를 C#으로 설명한다.

  • https://github.com/bxcodec/go-clean-arch/tree/master/domain
  • https://medium.easyread.co/golang-clean-archithecture-efd6d7c43047
  • https://urunimi.github.io/architecture/go/clean-arch-go/
  • https://evrone.com/go-clean-template
  • https://www.mitrais.com/news-updates/the-benefits-of-using-clean-architecture-in-golang/
  • https://www.c-sharpcorner.com/article/clean-architecture-in-asp-net-core-web-api/
  • https://learn.microsoft.com/en-us/dotnet/architecture/modern-web-apps-azure/common-web-application-architectures

Middleware Patterns in Go

Go 표준 라이브러리에서 미들웨어는 http.Handler를 받아 http.Handler를 돌려주는 래퍼다. Dave Stearns의 튜토리얼은 로깅 미들웨어를 예로 든다. 감쌀 핸들러를 필드로 가진 구조체가 ServeHTTP를 구현하고, 그 안에서 전후 작업을 한 뒤 원래 핸들러의 ServeHTTP를 호출한다. 함수 형태로 쓰면 더 짧다.

1
2
3
4
5
6
7
func Logging(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        next.ServeHTTP(w, r)
        log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start))
    })
}

미들웨어를 여러 개 겹쳐 감싸면 스프링의 필터 체인과 같은 효과가 난다.

  • https://drstearns.github.io/tutorials/gomiddleware/
  • https://umi0410.github.io/blog/golang/how-to-backend-in-go-middleware/
  • https://bourbonkk.tistory.com/63
  • https://www.joinc.co.kr/w/man/12/golang/middleware

Dependency Injection in Go

Go에는 스프링 같은 DI 컨테이너가 기본으로 없고, 생성자 함수의 파라미터로 의존성을 넘기는 수동 주입이 기본이다. 객체가 많아지면 main의 조립 코드가 길어지는데, Google의 Wire는 이를 코드 생성으로 해결한다. README에 따르면 Wire는 컴포넌트 사이의 의존성을 함수 파라미터로 표현하고, 런타임 상태나 리플렉션 없이 컴파일 전에 초기화 코드를 생성한다. 다만 지금 저장소는 보관 처리되어 더 이상 유지보수되지 않는다.

  • https://syntaxsugar.tistory.com/entry/Golang-Dependency-Injection
  • https://medium.com/avenue-tech/dependency-injection-in-go-35293ef7b6
  • https://blog.logrocket.com/go-dependency-injection-wire/

X-Request-ID

요청 하나를 여러 서비스, 프록시, 로그 시스템에 걸쳐 추적하기 위한 비공식 HTTP 헤더다. 클라이언트나 첫 번째 프록시가 고유한 값을 만들어 붙이고, 경로상의 모든 서비스가 이 값을 그대로 전달하며 로그에 남긴다. 서버는 같은 값을 응답 헤더에도 넣어 준다. http.dev에 따르면 표준은 아니지만 Nginx, HAProxy, Heroku와 여러 API 게이트웨이가 생성하거나 전달하는 사실상의 표준이고, 값은 보통 UUID v4다. 장애 문의를 받을 때 이 ID만 있으면 해당 요청의 서버 로그를 바로 찾을 수 있다.

  • https://http.dev/x-request-id
  • https://stackoverflow.com/questions/25433258/what-is-the-x-request-id-http-header
  • http://daplus.net/http-x-request-id-http-%ED%97%A4%EB%8D%94%EB%8A%94-%EB%AC%B4%EC%97%87%EC%9E%85%EB%8B%88%EA%B9%8C/

Go Web Frameworks: Gin vs Echo

둘 다 표준 net/http 위에 라우팅, 미들웨어, 바인딩을 얹은 경량 프레임워크다. Mattermost 블로그의 비교에 따르면 Gin은 Martini와 비슷한 API에 성능을 강조하고, radix tree로 경로를 찾는 HttpRouter 위에 만들어져 빠르고 메모리 사용이 적다. Echo는 간결한 문법과 잘 정리된 문서가 장점으로 꼽힌다. 기능 차이는 크지 않아서 팀의 취향과 생태계로 고르는 경우가 많다.

  • https://blog.logrocket.com/5-top-go-web-frameworks/
  • https://blog.outsider.ne.kr/1371
  • https://mattermost.com/blog/choosing-a-go-framework-gin-vs-echo/
  • https://yuiltripathee.medium.com/go-gin-vs-echo-comparison-edf1536e2e25

Go Error Best Practices

Go는 예외 대신 error를 마지막 반환값으로 돌려준다. Effective Go는 에러 문자열에 출처를 담으라고 권한다. os.PathError처럼 연산, 파일 경로, 원인 에러를 담으면 호출 지점에서 멀리 떨어져 출력되어도 의미가 통하고, 패키지 이름을 접두어로 붙이면(image: unknown format) 어디서 난 에러인지 알 수 있다. 호출자는 타입 단언으로 구체적인 에러 타입을 꺼내 처리한다.

Go 1.13부터는 fmt.Errorf의 %w 동사로 원인 에러를 감쌀 수 있고, errors.Is로 감싼 체인 안에 특정 값이 있는지, errors.As로 특정 타입이 있는지 확인한다. panic은 프로그램이 더 진행할 수 없는 경우에만 쓴다.

  • https://levelup.gitconnected.com/go-error-best-practice-f0864c5c2385
  • https://blog.logrocket.com/error-handling-golang-best-practices/
  • https://go.dev/doc/effective_go

Go Testing Best Practices

Go 테스트는 대상 코드와 같은 패키지에 _test.go로 끝나는 파일을 두고, func TestXxx(t *testing.T) 형태의 함수를 go test로 실행한다. 실패는 t.Error, t.Fatal 등으로 알린다. t.Run으로 서브테스트를 만들 수 있어서, 입력과 기대값을 슬라이스로 나열하고 반복하며 서브테스트로 돌리는 테이블 주도 테스트가 Go의 관용적인 방식이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func TestAdd(t *testing.T) {
    tests := []struct {
        name     string
        a, b     int
        expected int
    }{
        {"positive", 1, 2, 3},
        {"negative", -1, -2, -3},
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            if got := Add(tt.a, tt.b); got != tt.expected {
                t.Errorf("Add(%d, %d) = %d, want %d", tt.a, tt.b, got, tt.expected)
            }
        })
    }
}
  • https://climbtheladder.com/10-golang-testing-best-practices/
  • https://blogs.halodoc.io/golang-unit-testing/
  • https://medium.com/@matryer/5-simple-tips-and-tricks-for-writing-unit-tests-in-golang-619653f90742
  • https://fossa.com/blog/golang-best-practices-testing-go/
  • https://blog.devgenius.io/5-tips-for-better-unit-testing-in-golang-b25f9e79885a
  • https://blog.logrocket.com/a-deep-dive-into-unit-testing-in-go/
  • https://www.digitalocean.com/community/tutorials/how-to-write-unit-tests-in-go-using-go-test-and-the-testing-package
참고한 자료외부 출처 7

외부 출처

이 글은 저작권자의 CC BY 4.0 라이선스를 따릅니다.

댓글

아직 댓글이 없습니다