ORM(Object Relation Mapping)이란?

  • 객체와 RMDB의 데이터를 자동으로 매핑해주는 것을 말한다.

    • 객체 지향 프로그래밍은 클래스를 사용하고, 관계형 데이터베이스는 테이블을 사용한다. (golang은 struct로 클래스를 대신한다)

    • 객체 모델과 관계형 모델 간의 불일치가 존재한다.

    • ORM을 통해 객체 간의 관계를 바탕으로 SQL을 자동으로 생성하여 불일치를 해결한다.

  • 매핑된 정보를 바탕으로 자동으로 SQL을 작성해준다.

    • Object를 통해 간접적으로 데이터베이스를 다룰 수 있다.

ORM의 장단점

장점

  • 객체 지향적인 코드이기에 비즈니스 로직에 더 집중할 수 있다.

    • ORM을 사용하면 SQL Query가 아닌 작성 중인 언어로 DB에 접근하여 데이터를 접근할 수 있다.

    • 각종 객체에 대한 코드를 별도로 작성하기 때문에 코드의 가독성이 올라간다.

    • SQL의 절차적이고 순차적인 접근이 아닌 객체 지향적인 접근으로 인해 생산성이 증가한다.

  • 재사용 및 유지보수의 편리성이 증가한다.

    • ORM은 독립적으로 작성되어 있고, 해당 객체들을 재활용할 수 있다.

    • MVC 모델을 사용할 경우 Model에서 가공된 데이터를 Controller에 의해 뷰와 합쳐지는 형태로 디자인 패턴을 견고하게 다지는데 유리하다.

    • 매핑 정보가 명확하여, ERD를 보는 것에 대한 의존도를 낮출 수 있다.

  • DBMS에 대한 종속성이 줄어든다.

    • 객체 간의 관계를 바탕으로 SQL을 자동으로 생성하기 때문에 RDBMS의 데이터 구조와 코드의 객체지향 모델 사이의 간격을 좁힐 수 있다.

    • 대부분의 ORM 솔루션은 DB에 종속적이지 않다.

    • Object에 집중함으로써 극단적으로는 DBMS를 교체하는 거대한 작업에도 비교적 적은 리스크와 시간이 소요된다.

단점

  • 완벽한 ORM으로만 서비스를 구현하기가 어렵다.
     
    • 사용하기는 편하지만 설계는 매우 신중하게 해야한다.

    • 프로젝트의 복잡성이 커질 경우 난이도 또한 올라갈 수 있다.

    • 잘못 구현된 경우 속도 저하 및 심각할 경우 일관성이 무너지는 문제점이 생길 수 있다.

    • 일부 자주 사용되는 대형 쿼리는 속도를 위해 SP를 쓰는 등의 별도의 튜닝이 필요한 경우가 있다.

    • DBMS의 고유 기능을 사용하기 다소 어렵다. (-> 특정 DBMS의 고유 기능을 이용한다면, 이식성이 저하된다는 문제점이 있다.)


  • 프로시저가 많은 시스템이라면 ORM의 객체 지향적인 장점을 활용하기 어렵다.



gORM이란?

gORM은 golang에서 사용 가능한 ORM 라이브러리 이다.



DB 환경 세팅

1) root 계정으로 mysql 접속

$sudo su - 		# su를 통해 관리자 권한 가져옴
$mysql -u root 		# mysql에 root 사용자로 접속 (비밀번호를 설정해뒀다면, mysql -u roor -p)

 

2) 사용자 생성 및 권한 후여

# 유저 생성, CREATE USER '{username}'@'{hostname}' IDENTIFIED BY '{password}'
CREATE user 'tester'@'localhost' IDENTIFIED BY 'password123'

# 모든 권한을 해당 유저, 호스트 조합에 허용
GRANT ALL PRIVILLEGES ON *.* TO 'tester'@'localhost';

# grant 테이블을 reload하여 변경 사항을 즉시 반영하도록 함
FLUSH PRIVILLEGES


3) 생성 확인

SELECT user, host FROM user;


결과

+------------------+-----------+
| USER             | HOST      |
+------------------+-----------+
| mysql.infoschema | localhost |
| mysql.session    | localhost |
| mysql.sys        | localhost |
| root             | localhost |
| tester           | localhost |
+------------------+-----------+

 

관련 패키지 설치


1) gORM 설치

go의 프로젝트 경로에서 다음 명령어를 작성한다.

$ go get -u gorm.io/gorm


해당 명령어를 수행하면, 현재 환경 기준으로 GOPATH/pkg/mod/gorm 에 해당 패키지가 설치되고 프로젝트 경로에서는 go.mod와 go sum에서 이를 관리하게 된다.

 

-u 명령어의 경우, 패키지 및 관련된 종속성을 업데이트하는 명령어로 go get과 세트로 자주 쓰이는 옵션이다.

 

2) gORM 용 MySQL 드라이버 설치

데이터 베이스 별로 해당하는 드라이버가 필요하다.

$ go get -u github.com/go-sql-driver/mysql

 

3. 쿼리 로그 설정

  • 작성된 쿼리를 추적하기 위해, sql로 수행되는 쿼리를 파일에 로그로 담도록 지정

  • gorm을 사용할 경우, 실제 sql이 어떻게 수행되고 있는지가 궁금할 수 있다.

mysql>  SET GLOBAL general_log = 1;
Query OK, 0 rows affected (0.03 sec)

mysql> SELECT @@log_output, @@general_log, @@general_log_file;
+--------------+---------------+-----------------------------------------------------+
| @@log_output | @@general_log | @@general_log_file                                  |
+--------------+---------------+-----------------------------------------------------+
| FILE         |             1 | /usr/local/var/mysql/youngmki-MacBookPro.log 	     |
+--------------+---------------+-----------------------------------------------------+


로그를 사용하도록 설정하고, 설정 여부를 확인했다.

 

다음 명령어를 통해 실제 로그 파일이 존재하는지를 확인할 수 있다.

$cd  /usr/local/var/mysql && find "youngmki-MacBookPro.log"

 

DB 연결 예제

테스트를 위해 다음과 같이 main.go를 작성했다.

package main

import (
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

func main() {
	// "{username}:{password}@tcp(127.0.0.1)
	dsn := "tester:password123@tcp(127.0.1:3306)/"
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})

	if err != nil {
		panic("could not connect to the database")
	}
	
	_ = db	// 일단 컴파일이 되도록 설정
}

 

간단한 웹서버 예제

먼저 fiber라는 웹서버 라이브러리를 사용할 예정이므로, 다음 명령어를 통해 모듈을 설치한다.

$ go get -u 	"github.com/gofiber/fiber/v2"


그 다음으로는 mysql 내에 테이블을 생성하고자 한다.

 

gorm에서는 AutoMigrate라는 메서드를 통해, struct를 테이블로 옮길 수 있도록 지원한다.

 

그러기 위해서 먼저 다음을 models/user.go 에 작성했다.

package models

type User struct {
   Id       uint
   Name     string
   Email    string
   Password []byte
}


그 다음에 이를 database/connection.go 라는 파일에서 DB 연결과 함께 다뤄줬다.

package database

import (
	"github.com/fatih/color"
	"github.com/mukmookk/simple-rest/models"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

var DB *gorm.DB

func Connect() {
	// "{username}:{password}@tcp(127.0.0.1)
	dsn := "tester:password123@tcp(127.0.1:3306)/"
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})

	if err != nil {
		panic("could not connect to the database")
	}

	DB = connection
	color.Green("Connection Opened to Database... Success!")

	connection.AutoMigrate(&models.User{})
	color.Green("Database Migrated... Success!")
}


여기서 color라는 모듈을 추가로 사용했는데, 터미널 출력에 다음처럼 색을 깔끔하게 입혀준다.

"github.com/fatih/color"


routes/routes.go 라는 파일을 생성하고, REST-API 메소드, url, handler를 정의해주었다.

package routes

import (
	"github.com/gofiber/fiber/v2"
	"github.com/mukmookk/simple-rest/models"
)

func Setup(app *fiber.App) {
	app.Post("/api/register", controllers.Register)
}

 

여기까지 작성했다면, 이젠 실제 핸들러를 구현해야 한다.

 

controller/controller.go를 생성하고 핸들러를 구현한다.

 

func Register(c *fiber.Ctx) error {
	var data map[string]string

	err := c.BodyParser(&data)
	if err != nil {
		return err
	}

	password, _ := bcrypt.GenerateFromPassword([]byte(data["password"]), 14)
	user := models.User{
		Name:     data["name"],
		Email:    data["email"],
		Password: password,
	}
	database.DB.Create(&user)
	return c.JSON(user)
}


먼저 map을 정의하여 들어온 JSON을 담아준다.


-> c.BodyParser(&data)

 

비밀번호의 경우, 그냥 넣어지지가 않는다. 때문에 한번 해쉬 변환을 거쳐준다.

-> bcrypt.GenerateFromPassword([]byte(data["password"]), 14)

 

data에 담아진 정보를 model/user.go에 정의한 struct 형식으로 옮겨주고,

 

실제 ROW를 생성한다.

-> database.DB.Create(&user)

 

package main

import (
	"github.com/gofiber/fiber/v2"
	"github.com/mukmookk/simple-rest/database"
	"github.com/mukmookk/simple-rest/routes"
)

func main() {
	app := fiber.New()

	database.Connect()
	routes.Setup(app)
	app.Listen(":3000")
}

 

서버를 실행시키고

$ go run main.go

 

POSTMAN으로 동작 여부를 다음과 같이 확인할 수 있다.

json으로 POST를 했을 때의 결과


id의 경우는 autoincrement이기에 여러번 실행하였더니 5가 되었다. 

 

앞서 설정한 로그 파일을 출력해보면 다음과 같은 쿼리가 실행되었음을 알 수 있다.

$ cat  /usr/local/var/mysql/gim-yeongmug-ui-MacBookPro.log
/usr/local/Cellar/mysql/8.0.27_1/bin/mysqld, Version: 8.0.27 (Homebrew). started with:
Tcp port: 3306  Unix socket: /tmp/mysql.sock
Time                 		   Id Command	Argument
2022-01-27T15:25:46.193725Z	   62 Query	SELECT VERSION()
2022-01-27T15:25:46.200413Z	   62 Query	SELECT DATABASE()
2022-01-27T15:25:46.236453Z	   62 Prepare	SELECT SCHEMA_NAME from Information_schema.SCHEMATA where SCHEMA_NAME LIKE ? ORDER BY SCHEMA_NAME=? DESC limit 1
2022-01-27T15:25:46.238041Z	   62 Execute	SELECT SCHEMA_NAME from Information_schema.SCHEMATA where SCHEMA_NAME LIKE 'testdb%' ORDER BY SCHEMA_NAME='testdb' DESC limit 1
2022-01-27T15:25:46.246686Z	   62 Close	stmt	
2022-01-27T15:25:46.262921Z	   62 Prepare	SELECT count(*) FROM information_schema.tables WHERE table_schema = ? AND table_name = ? AND table_type = ?
2022-01-27T15:25:46.262984Z	   62 Execute	SELECT count(*) FROM information_schema.tables WHERE table_schema = 'testdb' AND table_name = 'users' AND table_type = 'BASE TABLE'
2022-01-27T15:25:46.276699Z	   62 Close	stmt	
2022-01-27T15:25:46.276761Z	   62 Query	SELECT DATABASE()
2022-01-27T15:25:46.278598Z	   62 Prepare	SELECT SCHEMA_NAME from Information_schema.SCHEMATA where SCHEMA_NAME LIKE ? ORDER BY SCHEMA_NAME=? DESC limit 1
2022-01-27T15:25:46.278646Z	   62 Execute	SELECT SCHEMA_NAME from Information_schema.SCHEMATA where SCHEMA_NAME LIKE 'testdb%' ORDER BY SCHEMA_NAME='testdb' DESC limit 1
2022-01-27T15:25:46.279462Z	   62 Close	stmt	
2022-01-27T15:25:46.284871Z	   62 Prepare	SELECT column_name, is_nullable, data_type, character_maximum_length, numeric_precision, numeric_scale , datetime_precision FROM information_schema.columns WHERE table_schema = ? AND table_name = ? ORDER BY ORDINAL_POSITION
2022-01-27T15:25:46.284923Z	   62 Execute	SELECT column_name, is_nullable, data_type, character_maximum_length, numeric_precision, numeric_scale , datetime_precision FROM information_schema.columns WHERE table_schema = 'testdb' AND table_name = 'users' ORDER BY ORDINAL_POSITION
2022-01-27T15:25:46.292756Z	   62 Close	stmt	
2022-01-27T15:26:21.872960Z	   62 Query	START TRANSACTION
2022-01-27T15:26:21.893559Z	   62 Prepare	INSERT INTO `users` (`name`,`email`,`password`) VALUES (?,?,?)
2022-01-27T15:26:21.893656Z	   62 Execute	INSERT INTO `users` (`name`,`email`,`password`) VALUES ('name','author','$2a$14$DuauYNQjhtLiYn1Pa7NGyupKiG2YA.lCp3Tjh84BPjAD/jADnzy8i')
2022-01-27T15:26:21.916066Z	   62 Close	stmt	
2022-01-27T15:26:21.916193Z	   62 Query	COMMIT


이처럼 복잡한 SQL 쿼리문을 코드 단계에서 비교적 편하게 관리할 수 있다는 것이 직관적으로 느껴지는 ORM의 가장 큰 장점이다. 

'Programming Language > Go' 카테고리의 다른 글

grpc 소개  (0) 2022.11.15
protocol 버퍼에 대한 소개  (0) 2022.11.15
[go 모듈 소개 - 2. logrus] 더 자세한 로깅 모듈  (0) 2022.11.15