[GO] Golang과 Gin을 이용한 백엔드 RESTFUL API 개발 (2)

Seung Pyo Lee·2024년 9월 15일

golang_gin_RestFulApi

목록 보기
2/4
post-thumbnail

이번에는 mariaDB를 설치한 후 gin 프레임워크를 이용하여 접근 해 보겠습니다. model structure를 만들어 마이그레이션을 진행한 후 데이터 읽기 및 저장을 해 보겠습니다.

golang에 관심이 있어 시작해보려고한다면 공식 홈페이지의 "A Tour of Go"를 해보는것을 강력하게 추천합니다. go를 설치하지않고 따라가기만하면 금방 습득할 수 있고 기초적인 문법들을 이해하기 쉽습니다. 한국어도 지원합니다.
https://go.dev/tour/welcome/1

개발순서

A. gin을 이용한 기본적인 웹 서버 구동
B. mariaDB를 이용한 회원가입(register)과 로그인 구현
C. jwt(JSON Web Tokens) 구현
D. 블로그 아티클을 DB에 저장 및 RESTFUL API 구현

사용하는 프로그램들과 버전

linux debian 12.0
golang 1.23.1 / https://go.dev/
gin-gonic / https://github.com/gin-gonic/gin
gorm / https://gorm.io/
golang-jwt / https://github.com/dgrijalva/jwt-go
(github.com/golang-jwt/jwt로 대체됨)
godotenv / https://github.com/joho/godotenv
go-crypto / https://cs.opensource.google/go/x/crypto
vscode
vscode extenstion - Go, Thunder-client

B. mariaDB를 이용한 회원가입(register)과 로그인 구현

1. mariaDB 설치 및 설정

mariaDB는 mysql이 인수되는 과정에서 라이선스 문제가 발생되어 인원들이 나와 새로 만든 데이터베이스 입니다. mysql과 대부분 흡사하며 query를 사용하여 데이터 접근이 가능합니다.

데비안 기준으로

$sudo apt update
$sudo apt install mariadb-server
$sudo systemctl start mysqld

를 입력하여 설치 후 실행해줍시다. 처음 설치시에 root계정으로 접속하여 새로운 계정을 생성 해보겠습니다.

$mysql -u root
#use mysql;
#update user set password=PASSWORD("본인비밀번호") where User='root';
#CREATE USER '아이디'@'%' IDENTIFIED BY '비밀번호';
#GRANT ALL PRIVILEGES ON 데이터베이스명.* TO '아이디'@'%' IDENTIFIED BY '비밀번호';
#flush privileges;

새로운 계정을 생성 후 권한부여를 진행하였습니다. 이제 gorm이라는 프레임워크로 데이터베이스에 접속해보겠습니다.

2. gin에서 데이터베이스 연결하기

새로운 models라는 폴더를 생성 한 후에 setup.go파일을 만들어 기초적인 데이터베이스 연결을 해 보겠습니다.

$mkdir models
$cd models
$touch setup.go

이제 setup.go에서 데이터베이스를 연결하고 계정형태의 모델을 만든 뒤 마이그레이션을 진행해 보겠습니다.

// modles/setup.go
package models

import (
	"fmt"
	"log"
	"os"

	"github.com/jinzhu/gorm"
	_ "github.com/jinzhu/gorm/dialects/mysql"
	"github.com/joho/godotenv"
)

var DB *gorm.DB

func ConnectDataBase(){

	err := godotenv.Load(".env")

	if err != nil {
	  log.Fatalf("Error loading .env file")
	}	
	
	Dbdriver := os.Getenv("DB_DRIVER")
	DbHost := os.Getenv("DB_HOST")
	DbUser := os.Getenv("DB_USER")
	DbPassword := os.Getenv("DB_PASSWORD")
	DbName := os.Getenv("DB_NAME")
	DbPort := os.Getenv("DB_PORT")

	DBURL := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8&parseTime=True&loc=Local", DbUser, DbPassword, DbHost, DbPort, DbName)
	
	DB, err = gorm.Open(Dbdriver, DBURL)

	if err != nil {
		fmt.Println("Cannot connect to database ", Dbdriver)
		log.Fatal("connection error:", err)
	} else {
		fmt.Println("We are connected to the database ", Dbdriver)
	}

	DB.AutoMigrate(&User{})
		
}

setup.go에서는 .env파일을 생성하여 DB연결에 필요한 정보들을 다른 파일에 입력하여 숨깁니다.

$cd $GOPATH/blog
$touch .env
DB_HOST=127.0.0.1                       
DB_DRIVER=mysql                          
DB_USER=<Username>
DB_PASSWORD=<password> 
DB_NAME=blogDB
DB_PORT=3306 

DB연결에 필요한 정보들을 저장하고, 유저 이름과 패스워드를 다른 파일에 저장합니다.

setup.go 마지막줄의 DB.AutoMigrate(&User{})를 보면 User라는 모델을 기반으로 자동으로 마이그레이션을 진행합니다. 그럼 User struct를 만들어봅시다.

$touch ./models/user.go
// models/user.go
package models

import (
	"github.com/jinzhu/gorm"
)

type User struct {
	gorm.Model
	Username string `gorm:"size:255;not null;unique" json:"username"`
	Password string `gorm:"size:255;not null;" json:"password"`
}

gorm에서는 gorm.Model이라는 데이터를 위한 기본적인 모델을 제공해줍니다. ID라는 primary key를 설정해주며, 생성일자,수정일자,삭제일자를 추가적으로 생성해줍니다. 변수 선언 옆에 gorm:"--"의 경우 DB에서 필요한 설정들을 추가적으로 할 수 있습니다. 예를들어 unique key를 넣어 중복되는 Username이 존재하지 않도록 할 수 있습니다.

main.go에서 ConnectDataBase()를 콜해주는 코드를 추가합니다

package main

import (
  	"github.com/gin-gonic/gin"
	"github.com/seefnasrul/jwt-gin/controllers"
	"blog/models"
)

func main() {
	models.ConnectDataBase()
	r := gin.Default()
	public := r.Group("/api")
	public.POST("/register", controllers.Register)
	r.Run(":8080")

}

go run .을 실행하여 DB가 정상적으로 연결이 되었는지 확인해봅시다.

$go run .
We connected DataBase mysql

터미널에서 mysql커맨드를 이용하여 접속해봅시다.

$mysql -u <User> -p
#use blogDB;
#desc users;


데이터베이스가 정상적으로 생성되었을 경우 Users라는 테이블이 생성되며 이러한 스키마를 가지게됩니다.

3. 회원가입 구현

models.User를 기반으로 회원가입을위한 struct를 생성하여 계정을 데이터베이스에 저장해봅시다. auth.go에서 RegisterInput이라는 회원가입을 위한 새로운 struct를 생성하고, JSON형식의 데이터가 POST형식으로 전송될시 데이터를 저장해보겠습니다.

// controllers/auth.go
package controllers

import (
	"net/http"
  	"github.com/gin-gonic/gin"
	"blog/models"
)

type RegisterInput struct {
	Username string `json:"username" binding:"required"`
	Password string `json:"password" binding:"required"`
}

func Register(c *gin.Context){
	
	var input RegisterInput

	if err := c.ShouldBindJSON(&input); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	u := models.User{}

	u.Username = input.Username
	u.Password = input.Password

	_,err := u.SaveUser()

	if err != nil{
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	c.JSON(http.StatusOK, gin.H{"message":"registration success"})

}

ShouldBindJSON을 호출하여 입력된 JSON형식의 데이터를 input 변수에 저장하고 값을 models.User 형식으로 변환 저장한 후 성공을 반환합니다.

이제 데이터베이스에서 값을 저장하는 saveUser()를 구현해봅시다. User 스트럭트를 선언한 user.go에 코드를 추가해봅시다.

// models/user.go
package models

import (
	"html"
	"strings"
	
	"github.com/jinzhu/gorm"
	"golang.org/x/crypto/bcrypt"
)

type User struct {
	gorm.Model
	Username string `gorm:"size:255;not null;unique" json:"username"`
	Password string `gorm:"size:255;not null;" json:"password"`
}

func (u *User) SaveUser() (*User, error) {

	var err error
	err = DB.Create(&u).Error
	if err != nil {
		return &User{}, err
	}
	return u, nil
}

func (u *User) BeforeSave() error {

	//turn password into hash
	hashedPassword, err := bcrypt.GenerateFromPassword([]byte(u.Password),bcrypt.DefaultCost)
	if err != nil {
		return err
	}
	u.Password = string(hashedPassword)

	//remove spaces in username 
	u.Username = html.EscapeString(strings.TrimSpace(u.Username))

	return nil

}

Golang에서 사용되는 method continued 방식을 사용하여 User 스트럭트에서 사용할 수 있는 Function을 만들어봅시다. gorm의 경우 오브젝트를 만들경우에 자동적으로 실행되는 HOOK이 존재합니다. BeforeSave의 경우 세이브 전 에 실행되는 훅으로써 비밀번호를 해시화하여 저장하는 기능을 구현하였습니다.
참조: https://gorm.io/docs/hooks.html

go run .을 실행하여 정상적으로 계정이 저장되는지 확인해 봅시다.

데이터베이스에서도 어떻게 저장되어있는지 확인해봅시다

#SELECT * FROM USERS;

번외로는 현재 TLS보안 시스템이 구현되어있지 않아 평문으로 패스워드를 전송하는것은 대단히 위험합니다. 추후 Nginx 또는 Gin에서 지원하는 TLS보안을 설정하는것이 좋습니다.

4. 로그인 구현

같은방식으로 로그인도 구현해봅시다. 로그인의 경우 데이터베이스에서 데이터를 찾아 일치하는지 확인하는 방식으로 구현합니다.
main.go에서 /login에 새로운 엔드포인트를 추가합니다.

// main.go
package main

import (
  	"github.com/gin-gonic/gin"
	"blog/controllers"
	"blog/models"
)

func main() {

	models.ConnectDataBase()
	
	r := gin.Default()

	public := r.Group("/api")

	public.POST("/register", controllers.Register)
	public.POST("/login",controllers.Login)

	r.Run(":8080")

}

auth.go 파일에 Login()을 만들어봅시다.

...
// controllers/auth.go
type LoginInput struct {
	Username string `json:"username" binding:"required"`
	Password string `json:"password" binding:"required"`
}

func Login(c *gin.Context) {
	
	var input LoginInput

	if err := c.ShouldBindJSON(&input); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	u := models.User{}

	u.Username = input.Username
	u.Password = input.Password

	token, err := models.LoginCheck(u.Username, u.Password)

	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "username or password is incorrect."})
		return
	}

	c.JSON(http.StatusOK, gin.H{"token":token})
}
...

LoginInput이라는 새로운 스트럭처를 만들어 사용합니다. model 패키지에서 LoginCheck()이라는 펑션을 만들어 데이터베이스에 접근, 성공시 JWT 토큰을 리턴합니다.

user.go 파일에 입력한 정보를 확인하는 코드를 추가합니다.

...
// controllers/user.go
func VerifyPassword(password,hashedPassword string) error {
	return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
}

func LoginCheck(username string, password string) (string,error) {
	
	var err error

	u := User{}

	err = DB.Model(User{}).Where("username = ?", username).Take(&u).Error

	if err != nil {
		return "", err
	}

	err = VerifyPassword(password, u.Password)

	if err != nil && err == bcrypt.ErrMismatchedHashAndPassword {
		return "", err
	}

	token,err := token.GenerateToken(u.ID)

	if err != nil {
		return "",err
	}

	return token,nil
	
}
...

입력한 username가 데이터베이스에 존재하는지, 그리고 해시화되어 저장된 패스워드가 입력 후 해시화 한 패스워드 값과 일치하는지 확인합니다.위 과정은 VerifyPassword()에서 bcrypt.CompareHashAndPassword()를 이용하여확인합니다. 모두 일치 시에는 GenerateToken()이라는 다음 글에서 작성할 펑션을 실행하여 JWT토큰을 발행합니다. 패스워드가 어떤식으로 저장되는지, 왜 해시화하는지에 대해서는 다른 글을 찾아보길 바랍니다. Authorization부분에서는 중요한 개념이기 때문에 반드시 이해하는것이 좋습니다.
(읽기 좋은 블로그:https://st-lab.tistory.com/100)

다음 글에서는 JWT토큰을 발행 후 토큰에 따른 인증시스템을 구현하겠습니다.

profile
Seung Pyo Lee / Computer Science in UCR

0개의 댓글