티스토리 뷰

반응형

백엔드 서버를 개발 할때, 데이터의 저장은 DB를 연동해서 하는 것이 일반적인데요. DB의 종류도 SQL로 할 꺼냐, NoSQL로 할꺼냐의 근본적인 결정도 있지만 SQL의 경우 데이터의 모델을 정의하고 이에 따라 이를 쉽게 처리해줄 수 있는 프로그래밍 스킬이 필요한데 이것이 바로 ORM(Object-relation mapping) 입니다. ORM을 사용할 경우 실질적으로 프로그래밍 언어에서 사용할 수 있는 가상 객체 데이터베이스를 생성하는데, 이 가상 객체 데이터베이스는 클래스(혹은 스트럭쳐)와 매핑됩니다. 결과적으로 개발자는 객체를 다루는 것처럼 데이터를 다룰 수 있으며, 데이터베이스를 코드에 자연스럽게 녹여낼 수 있게 됩니다.

RDBMS 란?

Relational Database Management System. 데이터를 테이블에 나누어 담고 테이블 간 관계를 정의하여 사용하는 형식의 데이터 베이스이며 특징은 Strict Schema, Relations 입니다. 

ORM 란?

Object Relational Mapping, 객체-관계 미핑, 객체와 관계형 데이터 베이스의 데이터를 자동으로 매핑 해주는 것을 말합니다. 객체 간의 관계를 바탕으로 SQL을 자동 생성해서 sql 쿼리문 없이도 데이터베이스의 데이터들을 다룰 수 있습니다.

장점으로는, 객체 지향적인 코드로 인해 더 직관적이고 비즈니스 로직에 더 집중할 수 잇게 도와줍니다. 그리고 재사용 및 유지보수의 편리성이 증가합니다. 특정 DB에 한정 되지 않기 때문에 나중에 어떤 DB로든 쉽게 migration 가능합니다.

단점으로는, 완변하게 ORM으로만 서비스를 구현하기가 어렵습니다. 프로시저가 많은 시스템에선 ORM의 객체 지향적인 장점을 활용하기가 어렵습니다.

GORM 란?

gorm은 go 언어를 위한 ORM 입니다. 

https://gorm.io/

 

GORM

The fantastic ORM library for Golang aims to be developer friendly.

gorm.io

특징은 아래와 같이 설명하고 있는데요. 기능을 한번 구현해보면서 살펴보도록 하겠습니다.

  •  Full-Featured ORM
  •  Associations (has one, has many, belongs to, many to many, polymorphism, single-table inheritance)
  •  Hooks (before/after create/save/update/delete/find)
  •  Eager loading with Preload, Joins
  •  Transactions, Nested Transactions, Save Point, RollbackTo to Saved Point
  •  Context, Prepared Statement Mode, DryRun Mode
  •  Batch Insert, FindInBatches, Find/Create with Map, CRUD with SQL Expr and Context Valuer
  •  SQL Builder, Upsert, Locking, Optimizer/Index/Comment Hints, Named Argument, SubQuery
  •  Composite Primary Key, Indexes, Constraints
  •  Auto Migrations
  •  Logger
  •  Extendable, flexible plugin API: Database Resolver (multiple databases, read/write splitting) / Prometheus…
  •  Every feature comes with tests
  •  Developer Friendly

MariaDB 설치

저는 RDB로 mysql 계열인 MariaDB를 로컬에 먼저 설치한뒤 gorm을 이용해서 데이터를 쿼리해보도록 하겠습니다.

$ brew install mariadb

설치를 한 뒤, 시작을 해 줍니다.

$ mysql.server start

정상적으로 시작이 되었다면 root 계정의 패스워드를 부여해야하는데요 아래와 같이 진행할 수 있습니다.

$ sudo mysql -uroot
Password:
MariaDB [mysql]> CREATE DATABASE eiffel;
Query OK, 1 row affected (0.007 sec)
MariaDB [(none)]> use eiffel;

Database changed
MariaDB [eiffel]> set password for root@'localhost' = PASSWORD('1111');

이러면 eiffel 이름을 가진 DB에 root 계정 비번 1111로 접근할 수 있는데요, 아래와 같이 테이블 하나를 생성해보도록 하겠습니다.

MariaDB [eiffel]> CREATE TABLE user(
    -> user_id int NOT NULL AUTO_INCREMENT PRIMARY KEY,
    -> user_email varchar(100) NOT NULL,
    -> user_name varchar(100),
    -> organization varchar(100),
    -> tag varchar(100),
    -> created_at int(13);
    -> updated_at int(13);
Query OK, 0 rows affected (0.043 sec)
MariaDB [eiffel]> desc eiffel.user;

잘 생성됬는지 확인해보도록 합니다.

MariaDB [eiffel]> desc eiffel.user;
+--------------+--------------+------+-----+---------+----------------+
| Field        | Type         | Null | Key | Default | Extra          |
+--------------+--------------+------+-----+---------+----------------+
| user_id      | int(11)      | NO   | PRI | NULL    | auto_increment |
| user_email   | varchar(100) | NO   |     | NULL    |                |
| user_name    | varchar(100) | YES  |     | NULL    |                |
| organization | varchar(100) | YES  |     | NULL    |                |
| tag          | varchar(100) | YES  |     | NULL    |                |
| created_at   | int(13)      | YES  |     | NULL    |                |
| updated_at   | int(13)      | YES  |     | NULL    |                |
+--------------+--------------+------+-----+---------+----------------+
7 rows in set (0.004 sec)

user 테이블에 데이터를 하나 인서트 하도록 하겠습니다. user_id는 auto incremental 방식으로 자동 생성되는데, 이전에 1개를 제가 생성했다가 지웠기 때문에 user_id 2번으로 생성되어 정상적으로 보여지는 것을 확인 할 수 있습니다.

MariaDB [eiffel]> insert into user(user_email, user_name, organization, tag, created_at, updated_at) values('captaintech.hero@gmail.com', 'hero', 'captaintech', 'admin', 1656633699, 1656633699);
Query OK, 1 row affected (0.001 sec)

MariaDB [eiffel]> select * from user;
+---------+----------------------------+-----------+--------------+-------+------------+------------+
| user_id | user_email                 | user_name | organization | tag   | created_at | updated_at |
+---------+----------------------------+-----------+--------------+-------+------------+------------+
|       2 | captaintech.hero@gmail.com | hero      | captaintech  | admin | 1656633699 | 1656633699 |
+---------+----------------------------+-----------+--------------+-------+------------+------------+
1 row in set (0.001 sec)

물론 created_at과 updated_at의 경우 데이터베이스에 있는 타입으로 만들어 낼 수 도 있는데 데이터베이스의 디펜던시는 최소한으로 하기위해서 데이터에 저장하는 서버에서 입력하는데로 저장되도록 하였습니다. 추후에 데이터 베이스가 디펜던시를 갖고 있다면 삭제하기도 어렵고 변경하기도 어려워지는 문제에 봉착하는데요, 관계형 DB의 장점이기도 하지만, 한 편으로 단점이 될 수도 있기에 초반에 잘 판단할 필요가 있습니다. 본 글에서는 데이터 베이스보단 gorm을 이용해서 관계형 DB와 연결시키는 것에 초첨을 두고 있으므로 넘어가도록 하겠습니다.

Gorm 연동을 위한 모델을 생성합니다. user 테이블의 항목에 맞게 생성하도록 하겠습니다.

package models

type User struct {
	ID           int    `json:"userId" gorm:"column:user_id;AUTO_INCREMENT;PRIMARY_KEY;not null"`
	Email        string `json:"email" gorm:"column:user_email;size:100;unique;not null"`
	Name         string `json:"name" gorm:"column:user_name;size:100"`
	Organization string `json:"organization" gorm:"column:organization;size:100"`
	Tag          string `json:"tag" gorm:"column:tag;size:100"`
	CreatedAt    int    `json:"createdAt" gorm:"column:created_at;type:int(13)"`
	UpdatedAt    int    `json:"updatedAt" gorm:"column:updated_at;type:int(13)"`
}

// TableName gets table name
func (u *User) TableName() string {
	return "user"
}

db 패키지에 DB 내 GetUser 라는 인터페이스를 생성합니다. 인풋 파라메터는 email이며 email을 통해 실제 DB를 검색하게 됩니다.

package db

import (
	"gormTest/models"
)

type DB interface {
	GetUser(email string) (models.User, error)
}

db 패키지 내의 orm.go를 생성하고 아래와 같이  실제 mysql 드라이버와, 로컬에 mariaDB에 접속할 때 필요한 정보들을 넣어서 ORM 객체를 생성할 수 있게 합니다.

package db

import (
	"gormTest/models"

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

type DBORM struct {
	*gorm.DB
}

func NewORM(dbname, con string) (*DBORM, error) {
	db, err := gorm.Open(mysql.New(mysql.Config{
		DSN:                      con + "?charset=utf8&parseTime=True&loc=Local", // data source name, refer https://github.com/go-sql-driver/mysql#dsn-data-source-name
		DefaultStringSize:        256,                                            // add default size for string fields, by default, will use db type `longtext` for fields without size, not a primary key, no index defined and don't have default values
		DisableDatetimePrecision: true,                                           // disable datetime precision support, which not supported before MySQL 5.6

		DontSupportRenameIndex:    true,  // drop & create index when rename index, rename index not supported before MySQL 5.7, MariaDB
		DontSupportRenameColumn:   true,  // use change when rename column, rename rename not supported before MySQL 8, MariaDB
		SkipInitializeWithVersion: false, // smart configure based on used version
	}), &gorm.Config{})
	return &DBORM{
		DB: db,
	}, err
}

func (db *DBORM) GetUser(email string) (user models.User, err error) {
	return user, db.Where(&models.User{Email: email}).Find(&user).Error
}

인터페이스로 생성했던 GetUser를 보면 db.Where를 통해 email을 넣어서 찾고 결과를 user 모델 객체에 넣어지도록 깔끔하게 프로그래밍됨을 알 수 있습니다.

저의 경우에는 REST API 서버를 만들고, 요청에 따라 DB에 접속해서 데이터를 쿼리하고 이를 리턴하는 방식으로 테스트를 할 것인데요, REST API 서버는 echo 프레임워크를 사용했습니다. 이에 따라 handler.go를 아래와 같이 만들 수 있습니다.

package handler

import (
	"gormTest/db"

	"github.com/labstack/echo/v4"
)

type HandlerInterface interface {
	GetUser(c echo.Context) error
}

type Handler struct {
	db db.DB
}

func NewHandler() (HandlerInterface, error) {
	dbms := "mysql"
	dbConf := "root:1111@tcp(0.0.0.0:3306)/eiffel"
	return NewHandlerWithParams(dbms, dbConf)
}

func NewHandlerWithParams(dbtype, conn string) (HandlerInterface, error) {
	dbInstance, err := db.NewORM(dbtype, conn)
	if err != nil {
		panic(err)
	}

	return &Handler{
		db: dbInstance,
	}, nil
}

그럼 이제, echo 프레임워크로 router만 만들어서 구동하면 정상적으로 결과값을 확인 할 수 있겠죠? router 구현과 테스트 그리고 gorm을 사용하면서 트러블 슈팅 결과는 이어서 다음 글에서 설명드리도록 하겠습니다.

반응형
댓글