
이번에는 새로운 데이터베이스 테이블을 만들고, RESTFUL API를 구현해봅시다. GET,POST,PUT,DELETE 를 요청할시에 데이터베이스를 사용하여 관리하는 방법에 대해 알아봅시다.
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
우리는 블로그의 글을 작성한다고 생각하고 모델을 구현해봅시다.
models폴더에 article.go라는 새로운 파일을 생성해봅시다.
$touch ./models/article.go
새로운 글은 제목, 내용, 작성자의 값을 가지고 있으며, 작성자는 users 데이터베이스에서 forign key로 가지게해 봅시다.
package models
import(
"github.com/jinzhu/gorm"
)
type Article struct {
gorm.Model
Title string `gorm:"not null;" json:"articleTitle"`
Content string `gorm:"not null;" json:"articleContent"`
Uploader uint `gorm:"foreignKey: ID;not null;" json:"articleUploader"`
}
gorm에서 지원하는 기본 Model과 Title,Content,Uploader라는 데이터를 가집니다.models/setup.go파일에 이 블로그 글을 기반으로 마이그레이션을 진행하여 새로운 테이블을 만듭니다.
...
// models/setup.go
DB.AutoMigrate(&Article{})
...
다음 main.go에서 어떤 방식으로 작동할지 엔드포인트를 정하고, 그에 따른 컨트롤러를 설계해봅시다.
...
protected := r.Group("/api/auth")
protected.Use(middlewares.JwtAuthMiddleware())
protected.GET("/user", controllers.CurrentUser)
protected.GET("/articles", controllers.ShowAllArticles)
protected.GET("/article/:articleID", controllers.ShowArticle)
protected.PUT("/article/:articleID", controllers.PutArticle)
protected.DELETE("/article/:articleID", controllers.DeleteArticle)
protected.POST("/article", controllers.SaveArticle)
...
protected 그룹에 새로운 엔드포인트들을 추가하였습니다. 특히 GET의 경우, 모든 글들을 리턴하는 /articles와 글의 ID를 기반으로 특정 글을 리턴하는 /articles/:articleID가 있습니다. GIN에서는 :<variable>라는 키를 통해 패리미터를 받습니다.
이제 controllers폴더에 새로운 파일을 만들고 기능을 추가해봅시다.
$touch ./controllers/article_controller.go
// controllers/article_controller.go
package controllers
type ArticleInput struct {
Title string `gorm:"not null;" json:"articleTitle"`
Content string `gorm:"not null;" json:"articleContent"`
}
func ShowAllArticles(c *gin.Context) {
articles, err := models.GetAllArticles()
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"err": err})
return
}
c.JSON(http.StatusOK, gin.H{"data": articles})
}
func ShowArticle(c *gin.Context) {
articleID, err := strconv.Atoi(c.Param("articleID"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"err": err})
return
}
article, err := models.GetArticle(articleID)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"err": err})
return
}
c.JSON(http.StatusOK, gin.H{"data": article})
}
먼저 모든 아티클을 출력하는 컨트롤러를 추가했습니다. 사용자가 새로운 글을 작성할때에는 제목과 내용밖에 입력하지 않기 때문에 새로운 데이터 스트럭처를 만들어줍시다. main.go에서 설정한 파라메터를 c.Param()으로 호출 한 후에 int형식으로 변환합니다. 이제 그 데이터를 기반으로 GetArticle()을 호출합니다. 이제 models/article.go 에 GetAllArticles()와 GetArticle()를 구현해봅시다.
...
// models/article.go
func GetAllArticles() ([]Article, error) {
var articles []Article
result := DB.Model([]Article{}).Order("ID desc").Find(&articles)
if result.Error != nil {
return []Article{}, errors.New("DB error")
}
return articles, nil
}
func GetArticle(id int) (Article, error) {
var article Article
result := DB.First(&article, id)
if result.Error != nil {
return Article{}, errors.New("DB error")
}
return article, nil
}
...
GetAllArticles()는 데이터베이스에 있는 Article 모델을 사용하는 테이블에 ID의 내림차순으로 리턴합니다. GetArticle()은 파라미터로 받은 데이터로 데이터베이스에서 서치합니다. DB.First()의 경우 primary key를 기반으로 검색할 수 있습니다. Article의 경우 gorm.Model에서 ID를 primary key로 설정하기 때문에 그것을 기반으로 검색 후 처음 해당 데이터를 발견하면 리턴합니다.
POST의 경우 필요한 데이터들이 있습니다. 제목과 내용 그리고 JWT키에서 USER의 ID을 받아 블로그 글을 저장해야합니다.
// controllers/article_controller.go
func SaveArticle(c *gin.Context) {
var input ArticleInput
user_id, err := token.ExtractTokenID(c)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"err": err})
return
}
if err := c.ShouldBindBodyWithJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
article := models.Article{}
article.Title = input.Title
article.Content = input.Content
article.Uploader = user_id
_, err = article.SaveArticle()
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
}
c.JSON(http.StatusOK, gin.H{"data": article, "status": "success"})
}
우리는 token과 user.go에서 만들었던 ExtractTokenID()와 GetUserById()를 이용하여 JWT값에 저장한 User의 ID값을 찾아 새로운 아티클 변수를 만든 뒤 글을 저장해 보았습니다. Continued function으로 Article 스트럭처 변수에서 사용 가능한 function을 구현해 봅시다.
...
// models/article.go
func (a *Article) SaveArticle() (*Article, error) {
err := DB.Create(&a).Error
if err != nil {
return &Article{}, err
}
return a, nil
}
...
간단하게 Create()라는 기능을 사용해 해당 아티클을 데이터베이스에 저장합니다.
이제 서버를 실행해 시험해 봅시다.


정상적으로 작동하는것을 확인할 수 있습니다.
이제 등록된 데이터를 변경하는 기능을 구현해봅시다. 여기서 중요한것은 글이 저장한 aritcleUploader 데이터가 현재 로그인한 유저의 데이터와 일치한지 확인하는 작업이 필요합니다. 다른 유저가 해당 데이터를 변조할 수 있기 때문에 엑세스 컨트롤이 필요합니다.
// controllers/article_controller.go
func PutArticle(c *gin.Context) {
var input ArticleInput
user_id, err := token.ExtractTokenID(c)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"err": err})
return
}
articleID, err := strconv.Atoi(c.Param("articleID"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"err": err})
return
}
err = c.ShouldBindBodyWithJSON(&input)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"err": err})
return
}
article := models.Article{}
article.Title = input.Title
article.Content = input.Content
article.Uploader = user_id
article.ID = uint(articleID)
result, err := article.EditArticle()
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"err": err})
return
}
c.JSON(http.StatusOK, gin.H{"data": result})
}
register과 비슷하게 진행되지만, 변경된 데이터에서 글의 ID도 추가해서 저장합니다. 데이터를 변경한다고 생각하기보다는 새로운 데이터로 덮어씌운다 라고 생각하는것이 편합니다.
// models/article.go
func (a *Article) EditArticle() (*Article, error) {
var article Article
result := DB.First(&article, a.ID)
if result.Error != nil {
return &Article{}, errors.New("DB error")
}
if article.Uploader != a.Uploader {
return &Article{}, errors.New("you don't have permission to edit this article")
}
err := DB.Model(Article{}).Where("ID = ?", a.ID).Updates(a).Error
if err != nil {
return &Article{}, errors.New("DB error")
}
return a, nil
}
데이터의 아이디를 기반으로 해당 데이터를 찾고, 작성자를 비교한 후 데이터를 덮어씌웁니다.

데이터가 정상적으로 변경되는것을 확인 할 수 있습니다. GET형식으로 변경하고 데이터를 읽어봅시다.
Delete는 PUT에서 데이터 저장하지 않고, 사용자 비교(엑세스 컨트롤)만 진행한 후에 데이터베이스에서 삭제하는것으로 구현하였습니다.
// controllers/article_controller.go
func DeleteArticle(c *gin.Context) {
user_id, err := token.ExtractTokenID(c)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"err": err})
return
}
articleID, err := strconv.Atoi(c.Param("articleID"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"err": err})
return
}
models.RemoveArticle(user_id, articleID)
c.JSON(http.StatusOK, gin.H{"status": "sucess"})
}
// models/article.go
func RemoveArticle(user_id uint, articleid int) error {
var article Article
result := DB.First(&article, articleid)
if result.Error != nil {
return errors.New("DB error")
}
if article.Uploader != user_id {
return errors.New("you don't have permission to edit this article")
}
err := DB.Delete(&Article{}, articleid).Error
if err != nil {
return err
}
return nil
}
Delete()의 경우 First()와 같이 primary key를 기반으로 데이터를 검색 한 후에 삭제합니다.

정상적으로 작동시에 success를 반환합니다.
이제 필수적인 RESTFUL API의 CRUD를 구현하였습니다. 번외로는 swagger를 이용하여 document를 작성해봅시다.