#2 Golang gin framework 웹서비스 만들기(DB 연동)

2019. 12. 11. 18:30 개발 이야기/Golang

 

틈틈이 Go언어를 공부하면서

 

간단한 웹서비스를 구축 중에 있는데요!

 

바로 Go 언어로 만들어진 웹 프레임워크

 

gin framework를 이용해서 만들고 있습니다

gin framework

 

gin 프레임워크는

 

쉽게 파이썬의 flask, django와 같은 웹 프레임워크입니다!

gin을 사용하는 이유는

 

위와 같이 go 웹 프레임워크 중 가장 많은 스타를 가지고 있고

 

검색을 하면 가장 먼저 나오기 때문입니다.

 

 

저는 이 프레임워크를 이용해서 간단한 웹서비스를 구축해보겠습니다.

 

먼저 설명을 한번 보겠습니다.

package main

import "github.com/gin-gonic/gin"

func main() {
	r := gin.Default()
	r.GET("/ping", func(c *gin.Context) {
		c.JSON(200, gin.H{
			"message": "pong",
		})
	})
	r.Run() // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080")
}

 

깃허브에 나와있는 부분인데 

 

요렇게 복붙을 하고 빌드를 한번 해보겠습니다.

 

컴파일 후 실행하면 아래와 같이 만들어 놓은 api가 쭉 ~ 나열되고

 

일단 방금 만든 /ping에 postman을 이용해서 리퀘스트를 날려보겠습니다!

(추후에 다 올리겠습니다)

 

 

postman을 보면

reponse에 pong으로 응답이 온 것을 확인할 수 있습니다.

 

 

자 그럼 간략하게 DB에 연결해서

 

바이너리를 실행했을 때 health check 용으로

 

만들 Time api를 만들어 보겠습니다.

 

Time api는 간단하게 리퀘스트를 하면 

 

서버의 현재 시간을 리턴해주는 api인데요!

 

물론 go 언어에도 time 함수가 있지만 DB에 연결하는 이유는

 

health 체크를 할 때 DB 커넥션까지 확인할 수 있기 때문입니다!

 

자 먼저 route를 추가해줍니다!

func main() {
    r := gin.Default()




	r.GET("/ping", func(c *gin.Context) {
		c.JSON(200, gin.H{
			"message": "pong",
		})
	})

    
    r.GET("/Time", api.Time)
    r.Run(":5000")
}

 

그리곤 Time 함수를 만들어 주면 되는데요!

 

그전에 할 것이 있습니다

 

 

먼저 말했듯, Time함수에서 가져오는 시간은 DB까지 연결해서 select 문을 이용해 시간을 가져올 건데요!

 

DB를 연동하는 함수를 먼저 만들어 보겠습니다!

func ConnectToDb() (*sql.DB, error) {
    db, err := sql.Open("postgres",
        "user=유저네임 password=비밀번호 dbname=디비명 host=IP주소 sslmode=disable")
    if db != nil{
        db.SetMaxOpenConns(100)  //  최대 커넥션 개수를 제한
        db.SetMaxIdleConns(10)  //   대기 커넥션 최대 개수를 설정
    }
    if err != nil {
        return nil, err
    }
    return db, err
}

 

Go 언어의 많은 부분 작성하게 되는 부분이 바로 error 처리인데요

 

DB가 오픈이 되지 않는 다면 에러를 반환하고 

 

오픈이 정상적으로 되면 DB 구초제의 포인터를 반환하게 됩니다!

 

DB 구조체는 아래와 같이 생겼는데요!

 

type DB struct {
	// Atomic access only. At top of struct to prevent mis-alignment
	// on 32-bit platforms. Of type time.Duration.
	waitDuration int64 // Total time waited for new connections.

	connector driver.Connector
	// numClosed is an atomic counter which represents a total number of
	// closed connections. Stmt.openStmt checks it before cleaning closed
	// connections in Stmt.css.
	numClosed uint64

	mu           sync.Mutex // protects following fields
	freeConn     []*driverConn
	connRequests map[uint64]chan connRequest
	nextRequest  uint64 // Next key to use in connRequests.
	numOpen      int    // number of opened and pending open connections
	// Used to signal the need for new connections
	// a goroutine running connectionOpener() reads on this chan and
	// maybeOpenNewConnections sends on the chan (one send per needed connection)
	// It is closed during db.Close(). The close tells the connectionOpener
	// goroutine to exit.
	openerCh          chan struct{}
	resetterCh        chan *driverConn
	closed            bool
	dep               map[finalCloser]depSet
	lastPut           map[*driverConn]string // stacktrace of last conn's put; debug only
	maxIdle           int                    // zero means defaultMaxIdleConns; negative means 0
	maxOpen           int                    // <= 0 means unlimited
	maxLifetime       time.Duration          // maximum amount of time a connection may be reused
	cleanerCh         chan struct{}
	waitCount         int64 // Total number of connections waited for.
	maxIdleClosed     int64 // Total number of connections closed due to idle.
	maxLifetimeClosed int64 // Total number of connections closed due to max free limit.

	stop func() // stop cancels the connection opener and the session resetter.
}

 

func OpenDB(c driver.Connector) *DB {
	ctx, cancel := context.WithCancel(context.Background())
	db := &DB{
		connector:    c,
		openerCh:     make(chan struct{}, connectionRequestQueueSize),
		resetterCh:   make(chan *driverConn, 50),
		lastPut:      make(map[*driverConn]string),
		connRequests: make(map[uint64]chan connRequest),
		stop:         cancel,
	}

	go db.connectionOpener(ctx)
	go db.connectionResetter(ctx)

	return db
}
func Open(driverName, dataSourceName string) (*DB, error) {
	driversMu.RLock()
	driveri, ok := drivers[driverName]
	driversMu.RUnlock()
	if !ok {
		return nil, fmt.Errorf("sql: unknown driver %q (forgotten import?)", driverName)
	}

	if driverCtx, ok := driveri.(driver.DriverContext); ok {
		connector, err := driverCtx.OpenConnector(dataSourceName)
		if err != nil {
			return nil, err
		}
		return OpenDB(connector), nil
	}

	return OpenDB(dsnConnector{dsn: dataSourceName, driver: driveri}), nil
}

 

Go 언어에는 C, C++와 같이 포인터 개념이 존재하기에 포인터로 리턴을 해줍니다

(포인터를 사용하면 리소스 사용이 작아지겠죠 ? 리소스를 위해서 포인터 리턴)

 

자 이렇게 정상적으로 DB 포인터 리턴이 완료되면

 

이 구초제를 이용해서 쿼리를 날릴 수 있습니다.

func Time(c *gin.Context) {
    db, err := model.ConnectToDb()
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()

    var time string
    err = db.QueryRow("" +
        "SELECT now()").Scan(&time)
    if err != nil {
        log.Println(err)
    }
    c.JSON(http.StatusOK, map[string]string{
        "Time": time,
    })
}

ConnectToDb 함수를 이용해 DB 포인터를 받습니다.

 

그다음에

 

"""

defer db.Close()

"""

 

를 볼 수 있는데요 Go언어에는 defer라는 것이 존재하는데요

(다른 언어에도 존재하는 개념인지는 모르겠습니다)

 

이 defer는 지연 실행 개념으로

 

정확히는 해당 함수가 리턴되기 전에 실행하라는 의미입니다

 

그래서 결과적으로 Time 함수가 종료되기 직전에

 

db.close() 메서드를 실행하라는 의미죠!

 

DB 커넥션을 했으면 끝나면 종료해주는 게 맞죠!

 

그리고 db.QueryRow() 메서드를 이용해 

time이라는 string 변수에 담겠습니다.

 

    var time string
    err = db.QueryRow("" +
        "SELECT now()").Scan(&time)
    if err != nil {
        log.Println(err)
    }
    c.JSON(http.StatusOK, map[string]string{
        "Time": time,
    })

 

. Scan 메서드를 이용해 time 변수에 값을 넣어줍니다

그리고 JSON 방식으로 reqonse를 날려줍니다.

 

이렇게 하면 DB와 연결된

 

heath 체크용! Time api를 완성했습니다.

 

이후에는 간단하게 계정 CRUD를 만들어보도록 하겠습니다~