초기 JAVA JDBC에서 DB CRUD 작업이나 기타 DB CRUD 작업을 해본 개발자라는 그 번 거로 웁과 지루한 작업에 대한 경험이 한 번씩은 있을 것이다. 쿼리에 대입할 값을 한 칸씩 밀어 써서 전혀 다른 값이 들어갔다던가, 쿼리 결과를 객체에 잘못 넣어 다른 결과가 출력된다던지 등등 단순 실수로 디버깅 시간이 길어지는 경우가 많았다. 이런 번거로움과 실수를 해결을 위해서 나온 것이 ORM이다. ORM에 대해서 간단히 알아보자.

ORM이란?

ORM(Object-relational mapping)은 말 그대로 객체와 관계의 맵핑이다. 객체지향 프로그래밍의 객체(Object)와 RDB의 관계(relation)를 매핑해주는 툴이나 프레임워크를 말한다.

직관적이지만 한없이 번거로운 JDBC 코드.

현시대에 가장 많이 사용하는 언어 패러다임은 객체지향이다. 그리고 비즈니스 프로그램을 개발하다 보면 프로그램에서 생산된 데이터를 영구적으로 저장해하는 일이 허다한데, 이때 저장소로 가장 많이 사용하는 것이 RDB이다.
위 두 가지 환경으로 개발을 하다 보면 불편한 부분이 생긴다. 객체지향으로 언어로 개발한 프로그램에서 만들어지는 데이터는 객체에 저장되어 있다. 이것을 RDB에 저장하려고 하니 변환작업을 해야 한다.
아래 예제는 JAVA JDBC로 간단하게 12개 칼럼이 있는 테이블에 하나의 Row를 추가하고 테이블을 조회해서 반환하는 메쏘드이다.

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
public class ColsBean {
private String col1;
...
private String col12;

public String getCol1() {
return col1;
}

public void setCol1(String col1) {
this.col1 = col1;
}
...
}

public List<ColsBean> insertAndSelectQuery(ColsBean newColsBean) {
Connection connection = null;
String driver = "com.mysql.jdbc.Driver";

final String url = "jdbc:mysql://localhost:3306/dbName";
final String user = "root";
final String pw = "";

final String selectQuery = " SELECT * FROM testTable ";
final String insertQuery = " INSERT INTO testTable(col1, col2, col3, col4, " +
" col5, col6, col7, col8, " +
" col9, col10, col11, col12 ) " +
" VALUES (?, ?, ?, ?, " +
" ?, ?, ?, ?, " +
" ?, ?, ?, ?) ";

// ColsBean 은 String col1 ... col12 멥버를 가진 클래스.
List<ColsBean> list = new ArrayList<ColsBean>();

try {
Class.forName(driver);

connection = DriverManager.getConnection(url, user, pw);

PreparedStatement pstmt = null;
try {
// 값을 추가하기 위해서 추가할 객체의 값을 하나 하나 쿼리에 맵핑시켜야 한다.
pstmt = con.prepareStatement(SQL);
pstmt.setString(1, newColsBean.getCol1());
pstmt.setString(2, newColsBean.getCol2());
pstmt.setString(3, newColsBean.getCol3());
pstmt.setString(4, newColsBean.getCol4());
pstmt.setString(5, newColsBean.getCol5());
pstmt.setString(6, newColsBean.getCol6());
pstmt.setString(7, newColsBean.getCol7());
pstmt.setString(8, newColsBean.getCol8());
pstmt.setString(9, newColsBean.getCol9());
pstmt.setString(10, newColsBean.getCol10());
pstmt.setString(11, newColsBean.getCol11());
pstmt.setString(12, newColsBean.getCol12());

int resultCount = pstmt.executeUpdate();
System.out.println("insert row : " + resultCount);

} catch (Exception qex) {
ex.printStackTrace();
} finally {
if (pstmt != null) try { pstmt.close(); pstmt = null; } catch (SQLException cex) { cex.printStackTrace(); }
}

Statement stmt = null;
ResultSet rs = null;
try {
stmt = connection.createStatement(selectQuery)
rs = pstmt.executeUpdate();

// 쿼리 결과를 객체에 하나 하나 맵핑시켜야 한다.
while(rs.next()) {
ColsBean bean = new ColsBean();
bean.setCol1(rs.getString("col1"));
bean.setCol2(rs.getString("col2"));
bean.setCol3(rs.getString("col3"));
bean.setCol4(rs.getString("col4"));
bean.setCol5(rs.getString("col5"));
bean.setCol6(rs.getString("col6"));
bean.setCol7(rs.getString("col7"));
bean.setCol8(rs.getString("col8"));
bean.setCol9(rs.getString("col9"));
bean.setCol10(rs.getString("col10"));
bean.setCol11(rs.getString("col11"));
bean.setCol12(rs.getString("col12"));
list.add(bean);
}

System.out.println("insert row : " + resultCount);

pstmt.excuteQuery(selectQuery);
} catch (Exception qex) {
ex.printStackTrace();
} finally {
if (stmt != null) try { stmt.close(); stmt = null; } catch (Exception cex) { cex.printStackTrace(); }
if (rs != null) try { rs.close(); rs = null; } catch (Exception cex) { cex.printStackTrace(); }
}
} catch (Exception ex) {
ex.printStackTrace();
} finally {
if (connection != null) try { connection.close(); connection = null; } catch (Exception e) { cex.printStackTrace(); }
}
return list;
}

위 소스 코드를 보면 알 수 있듯이 객체를 쿼리로, 쿼리 결과를 객체로 맵핑하는 코드가 장왕 하게 작성되어 있는 부분을 확인할 수 있다. 단순한 코드가 반복되고, 객체나 DB 스키마가 수정이 하나하나 변경해야 한다. 별로 대단한 코드가 아닌데 코드 재활성이 없고 유지보수가 어려운 부분이다. 이에 “객체를 바로 RDB에 바로 쓰고 바로 읽어 올 수는 없을까?”. 하고 천재 개발자들은 생각하게 되었다. 그래서 ORM을 만들게 되었다.

ORM. 내가 알아서 해줄께 무엇을해야 되는지만 알려죠!

그럼 ORM에서는 어떻게 처리하는지 알아보자.

아래 코드는 JAVA 진영 ORM으로 가장 많이 사용되었던 Hibernate의 코드이다.
\ 코드의 세부적인 부분은 최대한 생각했다. 기존 JDBC 코드와 차이점을 알 수 있을 수준으로 작성되었다..
\
ibatis/mybatis는 ORM이 아니다.

테이블 맵핑 파일

테이블과 객체를 맵핑한다.

testTable.hbm.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-2.0.dtd">

<hibernate-mapping>
<class name="kr.co.signal9.test.TestTable" table="testTable">
<id name="id" type="string" unsaved-value="null">
<column name="col1" sql-type="varchar(32)" not-null="true" />
</id>

<property name="col1">
<column name="col1" sql-type="varchar(32)" not-null="true" />
</property>
...
</class>
</hibernate-mapping>

DB 연결 설정 및 테이블 매핑 파일 지정

DB를 연결하고 테이블 매핑 설정 파일을 지정한다.

hiberante.cfg.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!DOCTYPE hibernate-configuration
PUBLIC "-//Hibernate/Hibernate Configuration DTD//EN"
"http://hibernate.sourceforge.net/hibernate-configuration-2.0.dtd">

<hibernate-configuration>
<session-factory>
<property name="hibernate.connection.driver_class">com.mysql.jdbc.Driver</property>
<property name="hibernate.connection.url">jdbc:mysql://127.0.0.1:3306/DATABASE_NAME</property>
<property name="hibernate.connection.username">root</property>
<property name="hibernate.connection.password">xxxxx</property>

<property name="dialect">org.shh.db.MySQL57Dialect</property>

<mapping resource="testTable.hbm.xml" />
</session-factory>
</hibernate-configuration>

실제 호출 코드

실제 테이블에 데이터를 추가하는 코드.

insert.java

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
...
try {
hbSession = HibernateUtil.currentSession();
tx = hbSession.beginTransaction();

ColsBean cols = new ColsBean();
cols.setCol1("col1");
cols.setCol2("col2");
cols.setCol3("col3");
cols.setCol4("col4");
cols.setCol5("col5");
cols.setCol6("col6");
cols.setCol7("col7");
cols.setCol8("col8");
cols.setCol9("col9");
cols.setCol10("col10");
cols.setCol11("col11");
cols.setCol12("col12");

hbSession.save(cols);
tx.commit();
} catch(Exception ex) {
if (tx != null) tx.rollback();
} finally {
HibernateUtil.closeSession();
}
...

더 많이 정보를 제공하지만 어쩐지 심플한(?)

실제로 Hibernate를 사용함으로 있어 기술 내용이 증가되었다. 그리고 코드가 설정으로 변경되어 명료하지 않은 부분도 있다. 하지만 개발자가 신경 써야 하는 부분이 확실히 줄어들었다. DB 연결에 신경 쓰지 않아도 된다. 그리고 쿼리가 한 줄도 없다. 단순이 객체를 save()메쏘드로 저장하고 있는 것을 볼 수 있다.

마무리

시작 컨셉을 객체지향 언어 개발자에게는 솔깃하다. 하지만 빛이 있으면 어둠이 있고, 모든 것이 처음 생각했던 것처럼 돌아가지는 않는다. ORM에 장단점을 알아보자.

ORM에 장점

  • 생산성
    SQL 직접 사용하지 않아 SQL 반복 작업이 없어진다.
  • 유지보수 양호
    스키마 수정 시 기존은 쿼리도 코치고 객체도 고치고 매핑 코드도 수정해야 하지만 ORM에서 처리가 간단하고 추가적으로 스키마 검증 작업해준다.
  • 이식성 우수
    특정 DB에 족속적이지 않아서 이식성이 우수하다.

ORM에 단점

  • 성능
    직접 쿼리 하는 것보다 성능이 떨어진다.

-> 개선되고 있음.

  • 복잡한 쿼리 구현 불가.
    직접 쿼리로 구현했던 복잡한 조인 및 기타 구현이 힘들다.

-> 구현할 수 있는 대안이 나오고 있음.

  • 러닝 커브
    먼가 새로 공부를 해야 한다. 그것도 난이도가 높다.

결론

실제로 ORM을 사용하면 처음에는 생산성이 높다. 하지만 쿼리가 복잡해지고 데이터가 늘어나면 하나둘 문제가 발생한다. 그래서 발생한 문제를 해결하기 위해서 추가적인 기술도 사용해고, ORM에 대한 높은 이해도가 필요하다. 그래도 앞으로 계속적으로 발전하는 기술이고 최종적으로 앞으로 주력이 되는 기술이니 CRUD 작업을 해야 하는 개발자라면 도전해보자.