프로그래밍 언어를 익히는 과정에서 가장 큰 난관은 기본 문법을 익힌 후에 실제 프로그램을 만드려고 할 때 맞닥뜨린다. CLI 툴 이상의 프로그램을 만들기 위해서는 프레임워크가 필수적이다. 요즘은 프로그래밍 언어보다는 킬러-프레임워크(그 자체가 언어 선택의 기준이 되도록 하는 프레임워크)를 중심으로 배우는 것이 기본으로 변해가는 추세라 이러한 어려움을 겪는 사람이 적을 것이다. 하지만 golang은 JS 진영의 React나 express와 같은 킬러-프레임워크가 존재하지 않다보니 언어 중심으로 배우게 된다. 이렇게 프레임워크 없이 바로 CLI 너머의 프로그램을 만드려다 보니 어려움을 겪을 수 밖에 없다.
이번 글에서는 이런 어려움에 처해 있는 입문자들이 실제 프로그램을 만드는 데에 도움을 주고자 대중적이면서 필자에게 도움이 되었던 패키지들을 몇가지 소개하고자 한다.
testify
testify는 golang에서 테스팅 시에 사용되는, 사실상 표준에 준하는 패키지이다. 예전 글에서도 간단하개 소개한 바 있다.
기본적인 기능은 golang의 기본 패키지인 testing과 동일하다. 다만 해당 패키지에서 제공하는 여러 함수들은 이름이 직관적이고 사용하기 쉽게 되어있다.
func TestSomefunc(t *testing.T) {
...
for i, v := range testSet {
assert.Equal(t, v.result1, Somefunc(v.arg1, v.arg2), fmt.Sprintf("Errors in case %d", i))
assert.NotEqual(t, v.result2, Somefunc(v.arg1, v.arg2), fmt.Sprintf("Errors in case %d", i))
}
}
func TestSomefunc1(t *testing.T) {
...
for i, v := range testSet {
value, err := Somefunc1(v.arg1)
if v.hasError {
assert.Error(t, err, "Error in test case %v", i)
assert.Nil(t, value, "Error in test case %v", i)
} else {
assert.NoError(t, err, "Error in test case %v", i)
assert.NotNil(t, value, "Error in test case %v", i)
}
}
}
gin
이 항목에 들어가기에 앞서서 짚고 넘어가야할 것이 있다. go언어에는 go-web-framework-stars에서 확인할 수 있다시피 웹 프레임워크가 여러가지 존재한다. 이 중 gin을 선택했던 건 처음 사용했을 당시에 가장 높은 순위에 있었고 사용하는 데에 특별한 불편이 없었기 때문이었다. 엄밀한 비교를 통해 고른 것이 아니기 때문에 위의 리스트에서 마음에 드는 프레임워크를 찾는 것도 좋은 방법이라고 생각한다.
gin은 golang의 대표적인 웹 프레임워크로 api 엔드포인트와 handler, 즉 함수를 매핑해준다. Query, Path parameter 등 엔드포인트에 쓰이는 요소들을 편하게 파싱할 수 있으며 body 역시 JSON, XML, Form 등의 형식으로 받아서 파싱할 수 있다.
func SomeHandler(ctx *gin.Context) {
user := ctx.Query("user")
id := ctx.Param("id")
var body SomeData
ctx.ShouldBindBodyWithJSON(&body)
result, err := someFunc(user, id, body.Target)
if err != nil {
ctx.JSON(400, gin.H{
"success": false,
"data": err.Error(),
})
return
}
ctx.JSON(200, gin.H{
"success": true,
"data": result,
})
}
middleware 기능 역시 눈여겨볼만 한하다. 로깅, 인증 등과 같은 기능들은 여러 api에서 공통적으로 사용된다. 이러한 기능을 하는 함수를 모든 api에 넣기보다 사전에 정의하여 자동적으로 실행되는 함수가 바로 middleware이다.
func corsHandler(c *gin.Context) {
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, Origin, Cache-Control, X-Requested-With")
c.Writer.Header().Set("Access-Control-Allow-Methods", "*")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(204)
return
}
c.Next()
}
func initserver() {
r := gin.Default()
r.Use(corsHandler)
r.POST("/some-endpoint", SomeHandler)
r.Run(":8080")
}
sqlc
golang에서 sql문을 사용하는 데에는 여러가지 방법-기본적인 sql문을 이용하는 방법, gorm 등 ORM을 이용해 좀 더 구조화하는 방법, sqlx를 이용해 정의된 구조체에 바로 파싱하는 방법 등-이 있다. 필자도 여러가지 패키지를 써보면서 시행착오를 겪었고, 결과적으로는 sqlc에 정착하게 되었다.
sqlc는 postgresql, mysql, sqlite를 사용할 때 sql 파일을 통해 golang 객체 및 함수를 생성하는 패키지이다. schema는 테이블, 뷰 등을 정의하는 부분으로 golang의 객체를 담당한다. query의 경우는 SELECT, INSERT, UPDATE문 등을 정의하는 부분으로 golang의 데이터조작, 즉 함수를 담당한다. 지정된(혹은 사용자가 직접 설정한) 폴더에 각각에 알맞게 sql문을 넣고 sqlc generate를 실행하면 해당 항목에 걸맞는 golang 코드가 생성된다.
CREATE TABLE "accounts" (
"id" bigserial PRIMARY KEY,
"owner" varchar NOT NULL,
"balance" bigint NOT NULL,
"currency" varchar NOT NULL,
"created_at" timestamptz NOT NULL DEFAULT (now())
);
-- name: CreateAccount :one
INSERT INTO accounts (owner, balance, currency)
VALUES ($1, $2, $3)
RETURNING *;
func someFunc(query db.Queries) error {
account, err := query.CreateAccount(context.Background(), db.CreateAccountParams{
Owner: "owner",
Balance: 1000,
Currency: "USD",
})
if err != nil {
return error
}
fmt.Println(account)
return nil
}
다만 단점 역시 명확하다. 우선 postgresql, mysql, sqlite 만 지원하기 때문에 다른 DB를 쓸 경우에는 다른 패키지를 찾는 것이 좋을 것이다. 또한 Dynamic Query를 지원하지 않기 때문에 필터링 기능을 구현할 때 일일히 구현해야 하는 번거로움이 있다.
zap
zap은 로그 패키지이다. 속도가 빠르면서도 로그 레벨 지정 및 동적 변경, 포매팅 등 기본적인 기능은 지원하는 것이 장점인 패키지이다.
logger, _ := zap.NewProduction()
defer logger.Sync() // flushes buffer, if any
sugar := logger.Sugar()
sugar.Infow("failed to fetch URL",
"url", url,
"attempt", 3,
"backoff", time.Second,
)
sugar.Infof("Failed to fetch URL: %s", url)
다만 로그의 필수 기능 중 하나인 로그 로테이션(로그 파일을 일정 수/용량으로 제한하는 기능)은 자체적으로 지원하지 않는데 공식적인 답변으로는 로그 로테이션은 구현하지 않을 것이고 다른 패키지(lumberjack 등)을 사용하라고 한다.
그 외
tcell의 경우 필자가 한 때 가장 많이 사용했던 라이브러리였다. 따로 항목을 만들지 않은 이유는 해당 라이브러리가 CLI(TUI) 관련 라이브러리라 일반적인 상황에서는 별로 쓰이지 않을 것이라 생각했다.
반면 wails의 경우에는 데스크톱 앱을 만들 때 유용한 라이브러리로, Gophercon Korea 2024에도 소개된 바가 있었지만 아직 본격적으로 쓰기 전이라서 추천하기에는 좀 더 써봐야 할 것 같았다. 물론 사용 경험은 괜찮았다.
마치며
여기까지가 필자가 생각했을 때 알아두면 좋은 패키지 목록이었다. 결국 입문자들이 겪는 근본적인 어려움의 원인은 '모른다는 것을 모른다'라고 생각한다. 이 글은 '모르는 것'을 알려주는 것, 즉 실제 프로그램을 만들기 위해 추가적으로 공부해야 할 것들을 소개한 글이다. 이 글이 golang 입문자들의 다음 단계로 넘어갈 수 있는 발판이 되기를 바란다.