gorm, ORM in GO

POII·2022년 11월 12일
0

go_study

목록 보기
7/7
post-thumbnail

영속성 (Persistence)

: 데이터를 생성한 프로그램이 종료되더라도 사라지지 않는 데이터 특성

  • 일반적으로 생성한 객체는 메모리에 저장되고 프로그램이 종료되면 사라진다.
    • 즉, 객체에 영속성은 직접 부여해주어야 한다.
  • 객체가 가지는 영속성은 Object Persistence라고 부른다.
  • 프로그램에서 이러한 영속성을 부여해주는 계층을 Persistence Layer라고 한다.
    • 이 계층에서의 작업을 편리하게 해주는 frame work 를 Persistence Framework라고 한다.

ORM (Obejct-Relation-Mapping)

: 객체와 RDB의 데이터를 소통할 수 있게 매핑해주는 행위 자체

  • 객체지향언어로 짜여진 프로그램에서 데이터는 보통 객체로 존재하지만, RDB에는 데이터가 테이블의 레코드 형태로 존재한다. 둘의 소통을 위해서는 데이터의 형태를 같게 해주는 변환 과정이 필요하다. 이 과정을 ORM 이라고 한다.

  • 사용 예시

from django.db imprt models

class Person(models.Model):
	first_name = models.CharField(max_length=50)
	last_name = models.CharField(max_length=50)
위와 같은 파이썬 코드가 있다고 하자.

위 코드를 migiration 하면 DB에 쿼리를 작성하지 않아도 first_name, last_name 이라는 
char 컬럼을 가진 Person이라는 테이블이 자동으로 생성된다. 

이는 장고가 ORM으로 클래스의 모양에 맞는 쿼리문을 알아서 실행해줬기 때문이다.
  • 장점
    • 객체 지향적 코드 작성을 가능하게 해주기 때문에 코드의 직관성 자체가 높아진다. 개발자는 비즈니스 로직에 더욱 집중할 수 있다.
    • 객체와 테이블의 매핑 관계가 명확해진다.
    • DB에 종속적이지 않게 된다.(구현방법을 넘어 자료형 타입 등에도 종속적이지 않게 된다)
    • SQL문을 일일이 작성하지 않아도 되기 때문에 생산성이 향상된다.
  • 단점
    • 내부에서 한 단계의 변환을 거치므로 직접 쿼리를 하는 것 보다는 성능이 떨어진다.
    • 익숙해지는데 시간이 걸릴 수 있다.
    • 직접 SQL문을 작성할 수 없기 때문에 쿼리의 유연성이 떨어진다.

GO에서의 ORM

  • go 언어에는 클래스가 존재하지 않는다. 대신, struct를 클래스처럼 이용할 수 있다.

  • 그렇기 때문에 앞서 본 파이썬과 마찬가지로 ORM 솔루션을 사용하면 지정한 struct와 field의 이름으로 적절한 테이블을 자동으로 생성할 수 있다.

  • go의 ORM 솔루션 사용 예시를 코드로 살펴보자

예제에서는 ORM을 제공하는 프레임워크는 gorm, db는 mysql, route를 만들기 위해서는 gin-gonic을 사용하였다.

$ go get gopkg.in/gin-gonic/gin.v1
$ go get -u github.com/jinzhu/gorm
$ go get github.com/go-sql-driver/mysql

먼저 터미널에서 위 명령어들을 입력해 사용할 패키지들을 설치해준다.

package main

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

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

	testGroup := router.Group("/api/testGroup/gorm-test")
	{
		testGroup.POST("/", createTodo)
		testGroup.GET("/", fetchAllTodo)
		testGroup.GET("/:id", fetchSingleTodo)
		testGroup.PUT("/:id", updateTodo)
		testGroup.DELETE("/:id", deleteTodo)
	}
	router.Run()
}

main 함수에 gin-gonic을 이용해 db에 접근할 때 사용할 api를 추가해준다.

package main

import (
	"github.com/gin-gonic/gin"
	"github.com/jinzhu/gorm"
	_ "github.com/jinzhu/gorm/dialects/mysql"
)
var db *gorm.DB

func init() {
	//db 커넥션을 열어줌
	var err error
	db, err = gorm.Open("mysql", "유저이름:비밀번호@/db이름?charset=utf8&parseTime=True&loc=Local")
	if err != nil {
		panic("faild to connect database")
	}

	db.AutoMigrate(&todoModel{})
}

func main() {
...
}

패키지가 로드될 때 수행되는 init 함수 안에 데이터베이스와 커넥션을 여는 코드를 추가해준다. 사용할 db driver가 gorm.Open의 첫 인자로 들어가며, 뒤는 db에 대한 설정이다.
db.AutoMigrate(&todoModel{}) 코드를 통해 연결한 데이터베이스에 자동으로 todoModel 이라는 struct와 같은 구조를 가진 테이블을 생성할 수 있다.

...
type (
	todoModel struct {
		gorm.Model        // id, createdat,updatedat,deletedat 을 가지고 있는 sturct를 임베디드해서 사용
		Title      string `json:"title"`
		Completed  int    `json:"completed"`
	}

	transformedTodo struct { // 사용자에게 gorm.Model 안에 있는 Created At, Updated At 등의 정보를 공개하지 않기 위해 만듬
		ID        uint   `json:"id"`
		Title     string `json:"title"`
		Completed bool   `json:"completed"`
	}
)
...

여기서 선언한 todoModel struct가 데이터베이스에 매핑된다.
gorm.Model을 임베디드하여 사용함으로 db에 기본적으로 필요한 정보들을 struct와 데이터베이스 양쪽에 넣어줄 수 있다.
transformedTodo struct는 api의 response용으로 사용할 sturct이다. todoModel안에서 사용자에게 노출시키고 싶지 않은 정보를 제거하였다.
이 코드를 작성한 후 db.AutoMigrate(&todoModel{})이 실행되면 데이터 베이스 안에 다음과 같은 테이블이 생성된다.

type Model struct {
	ID        uint `gorm:"primary_key"`
	CreatedAt time.Time
	UpdatedAt time.Time
	DeletedAt *time.Time `sql:"index"`
}

gorm.Model의 내부 구조는 다음과 같다.

...
func createTodo(c *gin.Context) {
	completed, _ := strconv.Atoi(c.PostForm("completed"))
	todo := todoModel{Title: c.PostForm("title"), Completed: completed}
	db.Save(&todo)
	c.JSON(http.StatusCreated, gin.H{"status": http.StatusCreated,
		"message": "Todo item created succcessfully!", "resourceId": todo.ID})
}
...

api를 추가할 때 url들과 매핑해줬던 함수를 구현하자. 여기서 함수의 인자 gin.Context는 요청, 응답에 대한 정보를 담고있다.
c.PostForm(”키 이름”) 으로 POST 요청안의 form 형태의 body에서 값을 꺼내올 수 있다.
이들을 적절한 형태로 todo라는 이름의 todoModel 구조체에 저장한 뒤 db.Save(&todo)로 연결되어있는 db에 저장해주었다.
그 후 c.JSON 함수를 이용해 http 응답코드, 응답메시지, db에 저장된 todo의 id를 JSON형태의 response로 보내준다.

func fetchAllTodo(c *gin.Context) {
	var todos []todoModel
	var _todos []transformedTodo
	db.Find(&todos)
	if len(todos) <= 0 {
		c.JSON(http.StatusNotFound, gin.H{"status": http.StatusNotFound, "message": "No todo found!"})
		return
	}
	for _, item := range todos {
		completed := false
		if item.Completed == 1 {
			completed = true
		} else {
			completed = false
		}
		_todos = append(_todos, transformedTodo{ID: item.ID, Title: item.Title, Completed: completed})
	}
	c.JSON(http.StatusOK, gin.H{"status": http.StatusOK, "data": _todos})
}

db.Find(&todos) 메소드는 struct와 매핑된 테이블에서 조건에 맞는 레코드들을 전부 가져와 struct 형태로 바꾼뒤 첫 인자로 준 slice에 저장해준다. 우리는 Find 메소드에 두번째 인자(조건)을 주지 않았기 때문에 테이블 전체의 데이터를 가져오게 된다.
이후 필요한 정보만을 가진 transFormedTodo struct 형태로 바꾸어 _todos에 저장해주고, 이를 response로 보낸다.

func fetchSingleTodo(c *gin.Context) {
	var todo todoModel
	todoID := c.Param("id")
	db.First(&todo, todoID)
	if todo.ID == 0 {
		c.JSON(http.StatusNotFound, gin.H{"status": http.StatusNotFound, "message": "No todo found!"})
		return
	}
	completed := false
	if todo.Completed == 1 {
		completed = true
	} else {
		completed = false
	}
	_todo := transformedTodo{ID: todo.ID, Title: todo.Title, Completed: completed}
	c.JSON(http.StatusOK, gin.H{"status": http.StatusOK, "data": _todo})
}

func updateTodo(c *gin.Context) {
	var todo todoModel
	todoID := c.Param("id")
	db.First(&todo, todoID)
	if todo.ID == 0 {
		c.JSON(http.StatusNotFound, gin.H{"status": http.StatusNotFound, "message": "No todo found!"})
		return
	}
	db.Model(&todo).Update("title", c.PostForm("title"))
	completed, _ := strconv.Atoi(c.PostForm("completed"))
	db.Model(&todo).Update("completed", completed)
	c.JSON(http.StatusOK, gin.H{"status": http.StatusOK, "message": "Todo updated successfully!"})
}

// deleteTodo remove a todo
func deleteTodo(c *gin.Context) {
	var todo todoModel
	todoID := c.Param("id")
	db.First(&todo, todoID)
	if todo.ID == 0 {
		c.JSON(http.StatusNotFound, gin.H{"status": http.StatusNotFound, "message": "No todo found!"})
		return
	}
	db.Delete(&todo)
	c.JSON(http.StatusOK, gin.H{"status": http.StatusOK, "message": "Todo deleted successfully!"})
}

나머지 메소드의 구현 코드이다

go build main.go
./main

위 명령어를 통해 8080포트에서 main을 실행할 수 있으며 터미널로 디버그 로그를 보여준다.

(gin.Default 로 만든 route는 기본적으로 8080 포트를 사용하고 디버그 로그를 보여주게 설정되어있기 때문)

포스트맨을 이용해 요청을 보내보았다. db에 잘 저장되는 것을 확인할 수 있다.

마무리

위에서 확인 할 수 있듯 gorm을 이용하면 복잡한 쿼리문 없이 데이터에 영속성을 부여할 수 있다.

익숙한 언어의 코드만으로 데이터베이스를 컨트롤 할 수 있는 것이 정말 편리하게 느껴지지만, 생각없이 코드를 수정하면 서버를 실행시키는 것 만으로 데이터베이스의 일관성이 깨질 수도 있다는 무시무시한 단점 또한 존재한다.

또한 gorm.Model 을 이용하면 설계했던 Domain Model 자체가 강제로 수정되기 때문에 예상치 못한 side effect가 생길 수도 있다.

명확한 장점, 단점이 존재하는 방법인 만큼, 사용하기전 이에 대해 충분히 이해하고 필요한 곳에 적절히 사용하도록 하자.


references
GORM Guide
Build RESTful API service in golang using gin-gonic framework
Golang ORM, 무엇이 좋을까?

profile
https://github.com/poi1649/learning

0개의 댓글