: 데이터를 생성한 프로그램이 종료되더라도 사라지지 않는 데이터 특성
Object Persistence
라고 부른다.Persistence Layer
라고 한다.Persistence Framework
라고 한다.: 객체와 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으로 클래스의 모양에 맞는 쿼리문을 알아서 실행해줬기 때문이다.
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, 무엇이 좋을까?