본문으로 바로가기

이 포스팅을 쓰는 이유

현재 Python의 Micro Framework인 Flask를 사용한 Studi 프로젝트를 진행하고 있다. Studi에서는 DBMS로 관계형 데이터베이스인 SQLite(에스큐엘라이트, 시퀄라이트)를 사용한다. 나는 Front만 맡아서 했었고 초기에는 간단한 기능만을 넣은 사이드 프로젝트였기 때문에 raw SQL Query를 사용하여 DB에 접근하고 있었다. 이 프로젝트를 좀더 키워서 백엔드의 영역도 광범위하게 건드려볼 생각에 리팩토링을 진행하면서 Python ORM인 Flask-SQLAlchemy(SQLAlchemy의 확장 라이브러리)를 적용하는 것이 좋겠다고 생각하게 되었다. Node.js로 프로젝트를 진행했을 때 ORM으로 Sequelize.js를 사용해보았는데 그때는 이미 프로젝트에 ORM이 적용되어 있던 상태였고 ORM이 무엇인지 깊이있게 생각해본 적이 없었다. 이번 프로젝트에 ORM을 적용하면서 ORM에 대해, 특히나 Flask-SQLAlchemy 에 대해서 정리해볼까 한다. (studi를 하면서 얼마나 많은 조각 포스팅을 하게될지 가늠은 안되지만..!)

ORM이란?

데이터베이스와 객체 지향 프로그래밍 언어간의 호환되지 않는 데이터를 변환하는 프로그램이 기법이다. 객체 관계 매핑이라고 한다. 객체 지향 언어에서 사용할 수 있는 '가상' 객체 데이터베이스를 구축하는 방법이다. 

SQLAlchemy는 Python을 위한 ORM(Object-relational mapping) 중에 하나이다. 프로젝트에 ORM을 적용하는 이유는 DBMS의 테이블을 하나의 객체로 보고 객체간의 관계를 프로그래밍 언어로 풀어난다는 점이다.

 

ORM의 장점과 단점(Pros and cons)

장점(pros) 

ORM을 사용하게 되면 SQL 질의어를 쓰지 않고, 프로그래밍 언어(python, Java, JS)로 객체(테이블)간의 관계를 풀어낼 수 있다. 개발자는 좀더 직관적으로 객체간의 관계를 파악할 수 있으며 비즈니스 로직에 집중할 수 있다. 만약 다른 DBMS를 이전해야 한다면, Raw SQL Query를 사용했을때는 굉장히 많은 부분을 변경해야 하지만, ORM을 사용해 개발하였다면 개발자는 비교적 적은 리스크만을 감당하면 된다. 개인적으로 DB 테이블간의 구조가 Python 코드로 명시되어 있다보니 필요할 때 구조를 빠르게 파악할 수 있어서 좋았다. (객체 지향 패러다임에 익숙한 개발자라면 바로 이해할 수 있도록, 아래 실제 Studi 의 코드 조각을 추가해두었으니 보면 무슨 의미인지 이해가 갈 것이다.)

- 개발자가 객체 지향적인 코드로 인해 더 직관적이고 비지니스 로직에 집중할 수 있다. 

  •  SQL 선언문, 할당, 종료와 같은 부수적인 코드가 없거나 급격히 줄어든다.

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

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

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

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

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

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

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

  • 종속적이지 않다는 것은 구현 방법 뿐만 아니라 많은 솔루션에서 자료형 타입까지 유효하다

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

단점 (cons)

Node.js 프로젝트에서 Sequelize를 사용해 프로젝트를 진행했던 때에 여러 테이블 간 JOIN을 해야만 했다. ORM을 사용한 코드보다 Raw SQL Query문이 더 빨리 요청을 처리한 경우가 있었다. (그때는 내가 ORM을 제대로 활용 못했던 가능성이 크다. 그렇게 크게 와닿는 속도 차이라면 실무 프로젝트에서 ORM을 절대 적용하진 않을테니) 이 포스팅을 하기 위해 ORM에 관련해서 쓴 다른 분들의 글을 읽어보았는데, 나도 아직 깊이 있게 ORM을 사용해보지 않다보니 단점은 와닿지는 않았다. 그래서 나중을 위해 기록만 해두도록 하겠다.

- 완벽한 ORM 으로만 서비스를 구현하기가 어렵다.

  • 사용하기는 편하지만 설게는 매우 신중하게 해야 한다.
  • 프로젝트의 복잡성이 커질 경우 난이도 또한 올라갈 수 있다.
  • 잘못 구현된 경우에 속도가 저하되고 심각할 경우에는 일관성이 무너지는 문제점이 생길 수 있다.
  • 일부 자주 사용되는 대형 쿼리는 속도를 위해 SP를 쓰는 등 별도의 튜닝이 필요한 경우가 있다.
  • DBMS의 고유 기능을 이용하기 어렵다

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

 

실제 코드로 보는 ORM (Flask-SQLAlchemy)

기존 코드는 작성된 SQL 파일을 읽어와서 스크립트를 실행(executescript) 시키는 방법이었다. ORM을 적용한 방식에서는 각각의 테이블을 객체로 생성하여 활용한다. 

<Raw SQL Query 방식>

Notes, Clauses, ClausePoints 테이블을 생성하기 위한 SQL 파일이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
CREATE TABLE Notes (
    note_id INTEGER PRIMARY KEY AUTOINCREMENT,
    note_name TEXT NOT NULL
);
 
 
CREATE TABLE Clauses (
    clause_id INTEGER PRIMARY KEY AUTOINCREMENT,
    note_id INTEGER NOT NULL,
    title TEXT NOT NULL,
    contents TEXT NOT NULL,
    FOREIGN KEY(note_id) REFERENCES Notes(note_id)
);
 
 
CREATE TABLE ClausePoints (
    clause_id INTEGER PRIMARY KEY,
    note_id INTEGER NOT NULL,
    imp INTEGER NOT NULL DEFAULT 0,
    und INTEGER NOT NULL DEFAULT 0,
    FOREIGN KEY(clause_id) REFERENCES Clauses(clause_id),
    FOREIGN KEY(note_id) REFERENCES Notes(note_id)
);
cs

SQL 파일을 읽어들여서 DB를 생성할 수 있도록 하는 파이썬 코드이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def execute_sql_file(file_name, cur):
    try:
        fd = open(file_name, 'r')
        sql_file = fd.read()
        fd.close()
    except Exception:
        # Read file error.
        app.logger.warning("Cannot open(read) .sql file.")
        return False
    try:
        cur.executescript(sql_file)
    except sqlite3.Error:
        # Execute error.
        app.logger.warning("Fail to execute {0}".format(file_name))
        return False
    return True
cs

 

<ORM을 적용한 방식>

class로 데이터 모델 객체를 구현한다. 아래에선 Note, Clauses, ClausePoints 라는 3개의 모델이 구현되어 있다. 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
from studi import app
from flask_sqlalchemy import SQLAlchemy
 
db = SQLAlchemy(app)
 
 
class Note(db.Model):
    column = ['note_id''note_name']
    note_id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    note_name = db.Column(db.Text, nullable=False)
 
    def __init__(self, notename):
        self.note_name = notename
 
    # 외래키의 표현
    clauses = db.relationship("Clauses", cascade="all, delete")
    clausePoints = db.relationship('ClausePoints', cascade="all, delete")
 
 
class Clauses(db.Model):
    column = ['clause_id''note_id''title''contents']
    clause_id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    note_id = db.Column(db.Integer, db.ForeignKey('note.note_id'), nullable=False)
    title = db.Column(db.Text, nullable=False)
    contents = db.Column(db.Text, nullable=False)
 
    clausePoints = db.relationship('ClausePoints', cascade="all, delete")
 
    def __init__(self, noteid=None, title=None, contents=None):
        self.note_id = noteid
        self.title = title
        self.contents = contents
 
 
class ClausePoints(db.Model):
    column = ['clause_id''note_id''imp''und']
    clause_id = db.Column(db.Integer, db.ForeignKey('clauses.clause_id'), primary_key=True)
    note_id = db.Column(db.Integer, db.ForeignKey('note.note_id'), nullable=False)
    imp = db.Column(db.Integer, nullable=False, default=0)
    und = db.Column(db.Integer, nullable=False, default=0)
 
    def __init__(self, clauseid=None, noteid=None, imp=0, und=0):
        self.clause_id = clauseid
        self.note_id = noteid
        self.imp = imp
        self.und = und
cs

구현된 모델 객체를 아래의 코드로 간결하게 실행할 수 있다. (코드에 넣지는 않은) app.config에 명시한 위치에 해당 DB 파일이 생성된다. 

1
db.create_all()
cs

 

참고한 곳

wiki : https://en.wikipedia.org/wiki/Object-relational_mapping

ORM  : http://www.incodom.kr/ORM  (장점과 단점 부분 참고)