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

Seung Pyo Lee·2024년 9월 18일

golang_gin_RestFulApi

목록 보기
4/4
post-thumbnail

이번에는 새로운 데이터베이스 테이블을 만들고, 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

D. 블로그 아티클을 DB에 저장 및 RESTFUL API 구현

1. 새로운 모델 생성 및 GET 구현

우리는 블로그의 글을 작성한다고 생각하고 모델을 구현해봅시다.
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.goGetAllArticles()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에서 IDprimary key로 설정하기 때문에 그것을 기반으로 검색 후 처음 해당 데이터를 발견하면 리턴합니다.

2. POST 기능 구현

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"})
}

우리는 tokenuser.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()라는 기능을 사용해 해당 아티클을 데이터베이스에 저장합니다.

이제 서버를 실행해 시험해 봅시다.

정상적으로 작동하는것을 확인할 수 있습니다.

3. PUT 기능 구현

이제 등록된 데이터를 변경하는 기능을 구현해봅시다. 여기서 중요한것은 글이 저장한 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형식으로 변경하고 데이터를 읽어봅시다.

4. Delete 기능 구현

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를 작성해봅시다.

profile
Seung Pyo Lee / Computer Science in UCR

0개의 댓글