Golang 학습 아카이브

Go를 제대로 공부해보고 싶었다. 이름은 오래전부터 알고 있었는데, 주변에서 쓰는 곳이 점점 늘어나니 더 이상 미루기 어렵다는 생각이 들었다. learning-golang이라는 이름으로 저장소를 만들고 공식 문서와 예제를 따라가면서 사흘 동안 기본기를 익혔다.

프로젝트 레이아웃

처음부터 Standard Go Project Layout을 기준으로 잡았다. 학습용이지만 실제 구조에 익숙해지는 게 낫겠다는 판단이었다.

learning-golang/
├── cmd/
│   ├── hello/          # 기본 패키지 호출 예제
│   ├── rest/           # Echo 기반 REST 서버
│   └── rest-failed/    # CockroachDB 연동 시도 (미완성)
├── pkg/
│   ├── greetings/      # Hello 함수 + 테스트
│   └── music/          # 맵 활용 예제
├── internal/
│   └── dict.go         # 내부 유틸리티 (Keys, Values)
├── Dockerfile
└── docker-compose.yml

cmd에 진입점을 두고, pkg에는 외부에서 쓸 수 있는 패키지를, internal에는 이 모듈 안에서만 쓸 유틸리티를 넣었다. Go의 internal 패키지는 상위 모듈 밖에서는 임포트가 안 된다. 직접 확인해보니 컴파일 에러로 막힌다.

패키지 구조

pkg/greetings는 공식 Go 튜토리얼(go.dev/doc)에서 처음 나오는 예제다. 이름을 받아서 인사 문자열을 반환하는 함수인데, 에러 처리를 함께 보여준다는 점이 인상적이다. Java나 Python에서는 예외를 던지는 게 보통인데, Go는 에러를 반환값으로 명시적으로 돌려주는 방식이라 코드 흐름이 훨씬 눈에 잘 들어온다.

func Hello(name string) (string, error) {
    if name == "" {
        return "", errors.New("empty name")
    }
    message := fmt.Sprintf("Hi, %v. Welcome!", name)
    return message, nil
}

pkg/musicmap을 써보기 위해 만든 예제다. 가수와 노래 제목을 매핑해두고 조회하는 구조다. internal.Keysinternal.Values를 호출해서 internal 패키지 연결이 잘 되는지도 확인했다.

cmd/hello에서는 greetings, music, 그리고 외부 패키지인 rsc.io/quote를 모두 불러와서 한꺼번에 출력한다.

테스트

Go 테스트는 별도 프레임워크 없이 표준 라이브러리 testing만으로 작성한다. greetings_test.go에 두 가지 케이스를 넣었다. 정상 이름을 넣었을 때 응답에 그 이름이 포함되는지 정규식으로 검증하고, 빈 문자열을 넣었을 때 에러가 반환되는지 확인한다.

func TestHelloName(t *testing.T) {
    name := "Gladys"
    want := regexp.MustCompile(`\b` + name + `\b`)
    msg, err := Hello("Gladys")
    if !want.MatchString(msg) || err != nil {
        t.Fatalf(`Hello("Gladys") = %q, %v, want match for %#q, nil`, msg, err, want)
    }
}

t.Fatalf로 실패 메시지를 포맷팅해서 출력한다. 테스트 실패 시 어떤 값이 들어왔고 무엇을 기대했는지 한눈에 보이게 쓰는 게 Go 테스트의 관행인 것 같다.

Docker 연동

Dockerfilegolang:1.20-alpine 기반으로 빌드하고, ARG module로 어떤 cmd를 빌드할지 선택할 수 있게 했다.

ARG module=rest
RUN go build -o main ./cmd/${module}

빌드 시 --build-arg module=hello처럼 넘기면 원하는 커맨드를 이미지로 만들 수 있다. 기본값은 rest다.

docker-compose.yml에는 Echo 서버와 CockroachDB를 함께 띄우는 구성을 넣었다. 환경 변수로 DB 접속 정보를 주입하고, 서버가 DB보다 먼저 뜨는 상황을 backoff로 재시도하는 방식이다.

CockroachDB 연동 시도

cmd/rest-failed가 이름 그대로다. Echo + CockroachDB + cockroach-go 드라이버를 연결해서 메시지를 저장하고 조회하는 REST API를 만들려 했는데, 로컬 환경에서 CockroachDB 접속이 제대로 안 된다. 디버그 로그만 잔뜩 남긴 채 멈춰있다.

err = backoff.Retry(openDB, backoff.NewExponentialBackOff())

재시도 로직은 cenkalti/backoff를 써서 지수 백오프로 구현했다. 연결은 아직 못 잡았지만 트랜잭션 처리를 crdb.ExecuteTx로 감싸는 패턴은 눈에 익었다.

마치며

사흘 동안 Go의 핵심 개념 몇 가지를 잡았다. 에러를 반환값으로 다루는 방식, internal 패키지로 접근 범위를 제어하는 방법, 표준 라이브러리만으로 충분한 테스트 환경, 그리고 멀티 스테이지 없이도 간결한 Dockerfile. 일단 여기서 마무리하고 다음 단계로 넘어가려 한다.

References