이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.
안녕하세요
K-IN 입니다.
서론
"이것만 알면 나도 개발 전문가" 시리즈입니다.
"Gin 을 이용한 CRUD RESTful API 개발 Part-5" 를 진행하겠습니다.
Part-5 에서는 gin 웹 어플리케이션에서 mongodb 와 redis 를 연동하는 방법을 학습해보겠습니다.
Part-4 의 내용을 아직 학습하지 않았다면 아래의 링크로 접속하여 학습을 순서대로 이어가주세요.
2023.03.02 - [Go] - 고 (Golang) | 이것만 알면 나도 개발 전문가 | Gin 을 이용한 CRUD RESTful API 개발 Part-4
Part-4 코드 내려받기
코드는 이전 시간에 작성한 코드에 이어서 진행하도록 하겠습니다. 아래의 링크에 접근하여 clone 후 코드를 내려 받아 주세요.
https://bitbucket.org/kinstory/gin-tutorial/src/part4/
Golang 에서 init 함수의 의미
Golang 은 불편해보이지만 상당히 친절한 언어입니다.
만약 여러분이 mongodb 에 관련된 연결을 수행하는 로직을 main 함수 내에서 호출하는 방향을 고려하고 있다면
Golang 은 이러한 여러분에게 더 나은 대안을 제공합니다.
init 함수는 소스파일마다 선언할 수 있으며 선언된 init 함수는 프로그램 실행 전에 자동으로 실행됩니다.
심지어 한 소스파일 내에 여러 번의 정의도 가능합니다.
Clone 한 소스코드 내에서 cmd/main.go 소스파일에 접근하여 init 함수를 연달아 선언해 봅니다.
package main
import (
"fmt"
"log"
"net/http"
"github.com/gin-gonic/gin"
"k-in.com/gin-tutorial/config"
)
func init() { // (1)
fmt.Println("this is init1")
}
func init() { // (2)
fmt.Println("this is init2")
}
func main() {
config, err := config.LoadConfig(".")
if err != nil {
log.Fatal("Could not load config", err)
}
server := gin.Default()
server.GET("/health", func(ctx *gin.Context) {
ctx.JSON(http.StatusOK, gin.H{"message": "ok"})
})
log.Fatal(server.Run(":" + config.Port))
}
task dev 명령어를 통해서 실행하면 아래의 그림과 같이 정의된 init 함수가 모두 실행되고 main 함수내에 정의된 로직이 실행됩니다.
그런데 이상한 점이 있습니다. 동일한 함수의 이름을 여러번 선언한 것입니다.
다른 함수에도 적용되는지 살펴볼까요?
after 라는 함수를 init 처럼 두번 선언했습니다.
우선, 코드 검사에서 식별자를 여러번 정의하였다는 에러(DuplicateDecl)를 발생시킵니다.
init 함수의 핵심을 정리하면 아래와 같습니다.
- init 함수는 golang 에서 특별취급하는 함수로 메인 프로그램 실행 전에 실행된다.
- 패키지 내의 모든 변수 선언들을 초기화(evaluation) ▷ init 함수 실행 ▷ main 함수 실행
- init 함수 외에는 동일한 함수 이름을 여러번 재선언할 수 없음
- 보통 프로그램 실행 전에 프로그램 상태를 올바른 상태로 유지/수정/검증 하는데 이용한다.
위의 핵심 정리에서 "보통 프로그램 실행 전에 프로그램 상태를 올바른 상태로 유지/수정/검증 하는데 이용한다." 라는 문구가 선뜻 와닿지 않을 수 있습니다. 대표적인 예시로 웹 어플리케이션은 모든 데이터를 DB 에 저장합니다. 그런데 웹 어플리케이션이 실행 후에도 DB 연결이 완전하지 않다면 우리의 프로그램은 제대로된 상태라고 볼 수 없습니다. 이럴 때 DB 연결을 점검하고 연결된 커넥션 객체를 저장하는 용도로 init 함수를 주로 사용합니다. 이 외에도 아래와 같은 용도로 사용합니다.
init 함수의 용도는 보통 아래와 같습니다.
- DB 연결
- CLI 앱의 플래그 설정
- 템플릿 객체 초기화
- 컨트롤러, 라우팅, 서비스 객체 초기화
- Gin 엔진 인스턴스 생성
- 등등
init 함수를 이용한 mongodb, redis 연결 구성
init 함수에 대해 자세히 이해하였으니 mongodb 와 redis 를 Go 프로그램과 연결해보겠습니다.
우선, 필요한 패키지를 먼저 설치해주세요. redis 패키지는 일부러 v8 을 선택했습니다.
최신 버전(v9) 설치 시 코드가 조금 상이할 수 있으므로 유의해주세요.
# mongodb 를 위한 패키지 설치
$ go get github.com/mongodb/mongo-go-driver
# redis 를 위한 패키지 설치
$ go get github.com/go-redis/redis/v8
init 함수는 변수 선언 이후에 실행된다고 합니다. 이에, 전역 변수에 필요한 변수들을 미리 선언해둡니다.
// filename: cmd/main.go
package main
import (
"context"
"log"
"net/http"
"github.com/gin-gonic/gin"
"github.com/go-redis/redis/v8"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
"k-in.com/gin-tutorial/config"
)
var (
server *gin.Engine // (1)
ctx context.Context // (2)
mongoclient *mongo.Client // (3)
redisclient *redis.Client // (4)
)
// (생략)
- gin Engine 인스턴스를 저장할 객체입니다. 앞으로 gin 엔진 인스턴스는 init 에서 초기화를 수행합니다.
- Context 객체를 선언합니다. 해당 객체는 작업 취소(cancellation), 작업으 타임아웃 또는 데드라인 설정 등의 목적으로 이용합니다.
- mongoDB 와 통신하는 클라이언트 객체입니다.
- redis 와 통신하는 클라이언트 객체입니다.
mongodb, redis 클라이언트를 초기화하기 위해서는 환경 변수에 정의된 데이터를 불러와야 합니다.
이전 시간에 환경 변수를 정의하면서 MongoDB 및 Redis 의 정보를 아래와 같이 app.env 파일에 정의하였습니다.
# Filename: app.env
PORT=8001
# Mongo & Redis Configuration
MONGO_INITDB_ROOT_USERNAME=root
MONGO_INITDB_ROOT_PASSWORD=password123
MONGODB_LOCAL_URI=mongodb://root:password123@localhost:6000
REDIS_URL=localhost:6379
추가로 설정된 app.env 파일의 환경 변수들을 로드해야 하므로 config/default.go 소스파일의 Config 타입 객체를 조금 수정해봅시다.
package config
import "github.com/spf13/viper"
type Config struct {
Port string `mapstructure:"PORT"`
MongoUri string `mapstructure:"MONGODB_LOCAL_URI"` // (1)
RedisUri string `mapstructure:"REDIS_URL"` // (2)
}
func LoadConfig(path string) (config Config, err error) {
viper.AddConfigPath(path)
viper.SetConfigType("env")
viper.SetConfigName("app")
viper.AutomaticEnv()
err = viper.ReadInConfig()
if err != nil {
return
}
err = viper.Unmarshal(&config)
return
}
- Config 타입 내에 MongoUri 필드가 추가되었고 mapstructure 태그(Tag)를 통해 환경 변수 MONGODB_LOCAL_URI 와 맵핑되도록 선언하였습니다.
- MongoUri 와 동일하게 RedisUri 필드를 추가하였습니다.
다시 cmd/main.go 소스파일로 돌아와 본격적으로 init 함수를 작성해보겠습니다. 설계의 의도는 아래와 같습니다.
- 프로그램 실행 시 main 함수에 접근 전에 mongodb, redis 와 연결을 수행하고 언제든 사용할 수 있도록 전역 변수에 이를 할당
이 단순한 명제를 실현시키기 위해 init 함수, 전역 변수, 환경 변수 등의 개념이 필요했습니다. 바로 소스코드를 보실까요?
// FileName: cmd/main.go
func init() {
config, err := config.LoadConfig(".")
if err != nil {
log.Fatal("Could not load env file : ", err)
}
ctx = context.TODO() // (1)
mongoconn := options.Client().ApplyURI(config.MongoUri) // (2)
mongoclient, err := mongo.Connect(ctx, mongoconn) // (3)
if err != nil {
panic(err) // (4)
}
if err := mongoclient.Ping(ctx, readpref.Primary()); err != nil { // (5)
panic(err)
}
log.Println("MongoDB successfully connected .... ")
redisclient = redis.NewClient(&redis.Options{ // (6)
Addr: config.RedisUri,
})
if _, err := redisclient.Ping(ctx).Result(); err != nil {
panic(err)
}
err = redisclient.Set(ctx, "test", "Hello Go World!", 0).Err() // (7)
if err != nil {
panic(err)
}
log.Println("Redis connected successfully .... ")
server = gin.Default() // (8)
}
- mongodb 에 연결하기 위해선 context 객체가 필요합니다. mongo-driver 공식 문서에서는 호출 함수가 이를 결정한다고 하며 보통 연결을 수행할 최대 시간 타임아웃을 전달합니다. 이 장에서는 그러한 구현을 넣지 않기 때문에 empty context 인 TODO 를 선언해서 전달합니다. 만약, mongodb 연결의 타임아웃을 설정하고 싶다면 context.WithTimeout 을 이용하도록 합니다.
- Viper 를 통해서 로드되는 환경 변수 파일들을 Config 타입에 맵핑하였습니다. config 타입에 맵핑된 각각의 필드 정보를 이용해 Mongodb, redis 등의 연결 정보를 전달할 수 있으며 mongoclient 변수의 초기화를 위해 MongoUri 필드가 사용되었습니다.
- mongodb 와 실제 연결을 수행합니다.
- panic 은 프로그램에서 중대한 문제가 발생했을 때 호출하는 함수로 에러 내용을 출력하고 즉시 프로그램을 종료합니다. 웹 어플리케이션에서 매우 중요한 이슈들 중에 하나는 DB 등의 연결 실패입니다. 따라서, mongodb 연결에 실패할 경우 panic 함수를 호출하여 프로그램을 즉시 중단 시킵니다.
- mongodb 연결이 정상적으로 되었다면 Ping 함수를 통해 연결된 세션을 검증합니다. 여기서 Primary 란 mongodb 가 클러스터 모드로 운영되는 경우 Primary 서버를 선택할 수 있는 옵션을 의미합니다. 만약 nil (=null) 값을 전달하면 조금 다르게 동작합니다.
- redisclient 변수를 초기화하며 연결 주소 정보에 RedisUrl 필드를 전달합니다. 나머지 Ping 등의 절차는 Mongodb 코드와 동일하므로 설명을 생략하겠습니다.
- Redis 에 "test" 를 Key 로하는 문자열 데이터를 주입합니다. 이 정보는 main 함수가 실행되면서 다시 불러와 health 체크 API 를 통해 응답에 사용됩니다.
init 함수만 정의를 한 상태에서 Taskfile 을 통해 연결을 테스트해보겠습니다.
아직 컨테이너를 올리기 전이므로 아래와 같은 에러가 발생하면서 프로그램이 중단됩니다. panic 함수가 잘 작동하는 것을 볼 수 있습니다.
$ task dev
panic: server selection error: server selection timeout,
current topology: { Type: Unknown, Servers:
[{ Addr: localhost:6000, Type: Unknown,
Last error: dial tcp [::1]:6000: connect: connection refused }, ] }
goroutine 1 [running]:
main.init.0()
실제 명령어 실행화면 은 아래와 같습니다.
task up 명령을 실행해서 필요한 컨테이너들을 올려줍니다. task up 은 Taskfile 을 통해 미리 정의된 명령어 모음 중에 하나입니다.
만약 관련 내용이 기억이 나지 않는다면 아래의 Part4 를 한번 읽어보시면 이해가 편합니다.
2023.03.02 - [Go] - 고 (Golang) | 이것만 알면 나도 개발 전문가 | Gin 을 이용한 CRUD RESTful API 개발 Part-4
컨테이너가 잘 세팅이 되었으므로 정상적으로 실행되는 것을 기대할 수 있습니다. 다시 task dev 명령어를 실행해보면 init 함수에서 정의한 로깅이 잘 출력되는 것을 확인할 수 있어 프로그램이 정상적으로 실행됨을 확인하였습니다. 또한, main 함수 실행 전에 mongodb, redis 연결을 미리 진행하기에 깔끔한 로직 전개가 이루어졌습니다.
이제 main 함수를 변경해서 redis 에 저장되어 있는 "test" 키의 value 정보를 받아와 health 체크 API 를 통해 출력해보도록 하겠습니다. mongodb 는 다음 Part 에서 인증 로직을 구현하는 과정을 통해 상세하게 다룰 예정이니 참고해주세요.
func main() {
config, err := config.LoadConfig(".")
if err != nil {
log.Fatal("Could not load config", err)
}
value, err := redisclient.Get(ctx, "test").Result() // (1)
if err == redis.Nil {
log.Println("`test` key does not exist")
} else if err != nil {
panic(err)
}
// server := gin.Default() // (2)
server.GET("/health", func(ctx *gin.Context) {
ctx.JSON(http.StatusOK, gin.H{"message": value, "status": "success"}) // (3)
})
log.Fatal(server.Run(":" + config.Port))
}
- redisclient 는 init 함수를 통해서 이미 연결된 상태이므로 별도의 설정없이 Get 함수를 실행해 test 키의 value 를 받아옵니다.
- gin Engine 인스턴스 또한 init 함수에서 생성이 완료되었으므로 주석 처리하여 실행되지 않도록 합니다.
- 불러온 redis 키 값을 health 체크 API 를 통해 리턴하도록 수정합니다.
변경된 로직을 확인하기 위해 task dev 명령을 실행합니다.
그리고 health 체크 API 에 접근하여 정상적으로 구동되는지 확인해보겠습니다.
브라우저를 통해 http://localhost:8001/health URL 에 접근하면 우리가 의도한대로 결과가 리턴됩니다.
맺음말
오늘은 "Gin 을 이용한 CRUD RESTful API 개발" 시리즈의 Part-5 를 진행하였습니다.
Gin 웹 어플리케이션에서 init 이라는 특수한 함수를 통해서 우아하게 MongoDB 와 Redis 연결을 진행하였습니다.
다음 파트에서는 JWT 를 이용한 실제 로그인 기능을 구현해보도록 하겠습니다. (짝짝짝)
오늘 진행한 튜토리얼의 소스는 아래의 경로에서 다운받아서 살펴볼 수 있습니다.
아래 웹 사이트에 방문하셔서 Clone 버튼을 꾹 눌러주세요.
https://bitbucket.org/kinstory/gin-tutorial/src/part5/
이상으로 K-IN 이었습니다.
즐거운 하루되세요