잊지 않겠습니다.

10. Hibernate

Java 2013. 9. 9. 11:13

* 사내 강의용으로 사용한 자료를 Blog에 공유합니다. Spring을 이용한 Web 개발에 대한 전반적인 내용에 대해서 다루고 있습니다.


 Hibernate는 지금까지 SQL을 이용한 DB query 방법이 아닌, 객체를 이용한 SQL auto generate를 통한 DB 접속 방법을 제공합니다. 이런 객체를 이용한 방법을 ORM(object relation model)이라고 합니다. 먼저, ORM에 대해서 간단한 소개를 해보도록 하겠습니다. 

ORM

java 언어의 발전은 sw 개발에 있어서 객체지향의 개발 방법으로 오는 혜택에 매료가 되었습니다. 그렇지만, 모든 웹 및 기업의 데이터가 저장되어 있는 RDBMS와 OOP간의 괴리의 차이에 의한 비용의 증가가 계속해서 발생하게 되었습니다. OOP적 개발 방법과 RDBMS의 관계형 개발 방법론이 끊임없이 충돌하게 되는 것이지요. 서로간에 전쟁(war) 라는 표현을 사용할 정도로 논란이 매우 큰 문제입니다. java 측에서는 relation 기술의 탓으로 돌리고, data 전문가들은 OOP 기술의 문제라고 약간은 소모적인 논쟁으로 계속해서 가게 되었지요.

이때, ORM(Object Relation Model)은 이러한 불일치 기술에 대한 solution을 지칭할 때 사용됩니다. 

ORM은 4가지로 구성이 되어 있습니다.

1. Persistence class에 대한 기본적인 CRUD를 수행하는 API
2. class 자체나 class의 property를 참조하는 query를 작성할 수 있는 API
3. mapping meta data 작성을 위한 기반 API
4. ORM 구성이 Transaction과 상호 동작하며 최적화를 수행하도록 돕는 API


왜 ORM을 사용해야지 되는가?

1. 생산성 : SQL관련 코드를 제거하고 객체간의 관계를 명확하게 그릴수 있습니다. 그리고 Domain에 집중할 수 있는 설계 구조를 가지고 옵니다.
2. 유지보수성 : Domain을 구현했기 때문에 Domain에 대한 명확한 정의가 나타납니다. Domain의 BL이 변경, 추가 되었을 때 그에 대한 수정이 쉽습니다.
3. 성능 : 가장 큰 이슈입니다. VM을 설명할 때, VM보다 일반 assembly로 compile되는 언어가 더 빠르지만 이제 사용되지 않는 이유랑 동일합니다. ORM 자체에 이미 많은 최적화 방법들이 구현되어 있고, 그 현인들의 지식을 사용할 수 있다는 점에서 더욱더 큰 장점을 가지고 올 수 있습니다.
4. 밴더 독립성 : ORM은 DB와 DB의 방언(각 DB만의 함수들)로부터 추상화 되어 있습니다. DB의 독립성은 매우 큰 장점을 가지고 오는데, 개발에 있어서 오히려 더 나은 장점을 가지고 옵니다. 개발자들은 자신의 개발 PC에 가벼운 DB(mysql)를 설치해서 개발을 하고, 실 서버에서는 Data에 최적화된 DB를 이용해서 서비스를 할 수 있는 장점을 가지고 있습니다. 이는 생산성과도 직결되는 문제입니다.
5. Java 표준 : ORM은 JSR 220에 의해서 java에 표준적인 기술로 인정을 받았습니다.


Domain Driven Design

에릭 에반스 (Eric Evans) 는 과거의 지혜와 경험들을 종합하여 도메인-드리븐 디자인 (Domain Driven Design) 이라는 방법론을 제시 했습니다.
단순 객체지향 세계에서 살던 개발자들은  이 굉장하지만 새로운 개념에 어려움을 느껴 발표된지 몇년이 지난 후에야 관심을 가지게 되었죠.
DDD가 도대체 뭔데? 어떻게 해서든지 돌아가기만 하면 되는거 아냐! 하면서 무심히 지나쳤던 것들에 대해 체계적으로 설명하는 방법론이죠. 하지만 여전히 DDD는 어려운 것, 그저 한때 유행하는 버즈 워드로 인식되고 있는 경향이 있습니다. DDD가 무엇인지 처음 듣는 개발자도 많을 것입니다. DDD의 전체적인 철학을 쉽게 요약하고 있는 블로그 포스트 DDD: How to tackle complexity  번역으로 DDD 카테고리를 시작합니다. 


DDD (Domain Driven Design) 에서는 어플리케이션 도메인을 표현하기 위한 오브젝트 모델을 만듭니다.
이 모델은 도메인의 모든 관계와 로직을 담고 있습니다. 이렇게 하는 목적은 도메인의 복잡성을 관리하기 위함 입니다. DDD 에는 매우 많은 개념과 패턴이 투입되어 있으나, 정제된 두개의 큰 그림으로 그 복잡성에 태클을 걸 수 있습니다.

1. 도메인의 개념을 명확하게 표현합니다.
2. 더욱 심도 있는 통찰을 위해 지속적인 리팩토링을 수행합니다.

복잡성이란 자체의 복잡한 정도를 의미합니다. 복잡한 것은 이해하기 어렵습니다. 이해하기 어려우면 금방 알아 들을 수 없습니다.

이것이 실제 이슈 입니다 : 복잡한 소프트웨어는 이해하기 어렵습니다. 이것이 바로 모든 사람이 업데이트 하기를 두려워 하여 아예 처음부터 다시 만드는 이유입니다. 아마 첫번째나 두번째는 해킹 하듯이 코드를 추가해서 원하는 바를 이룰 수 있을지 모르지만, 각각의 해킹은 더 이상 시도하는 것이 의미가 없을 때까지 복잡성과 추잡함을 증가시킵니다. 이것을 다른 말로 실패 라고 합니다.

그래서 우리는 이 복잡성을 극복해야 합니다. DDD의 첫번째 방법은 객체지향, 모델 과 추상화의 장점을 얻는 것 입니다. 하지만 이건 매우 광범위 하죠. 우리는 이 오브젝트와 모델들을 어떻게 구조화 해야 하는지 알아내야 합니다. 이것이 DDD가 도메인의 개념을 명시적으로 표현하자는 아이디어의 입니다.

아이디어는 간단합니다. 여러분의 도메인에 새로이 적용되는 개념이 있다면 모델에서 확인할 수 있어야 합니다. 중요한 개념을 확인하기 위해서 코드를 뒤져서는 안됩니다. 그 개념은 모델에서 오브젝트로써 표현되어야 합니다. 특정 조건에서만 발생하는 액션이 있다고 합시다, 이 조건들이 중요하지 않다면 그 액션을 수행하도록 그저 IF Statement 메소드로 처리하면 됩니다. 하지만, 그 조건들이 도메인에서 중요하다면 코드로 부터 감추는 것만으로는 부족합니다. 그 조건들을 수행하도록 Policy Object가 조건들을 표현해야 합니다. 이제 조건들은 당신의 도메인에서 명시적으로 표현됩니다.

이 아이디어 들은 Factories, Repositories, Services, Knowledge Levels 등등으로 표현될 수 있습니다. 이 것은 여러분의 시스템을 이해 가능하도록 만드는 중요한 부분입니다.

DDD가 작동하도록 만드는 두번째 아이디어는 "Deeper Insight"를 위한 지속적인 리팩토링 입니다. Deeper Insight 란 이미 가지고 있는 도메인 모델에서 새로운 어떤 것을 발견하게 된다면 대충 끼워넣지 말고 도메인에서 중요한 요소인지 반드시 알아내라는 것을 의미합니다. 만약 중요하다면 새로 이해한 것이 명확하게 표현 되도록 모델을 리팩토링 해야 합니다. 이 리팩토링은 사소할 때도 있고, 매우 중요 할 때도 있습니다.

도메인 모델이 표현성을 잃게 되면 점점 더 부서지고, 점점더 복잡해 지며 점점 더 어려워 집니다. 여러분의 모델이 단순하며  표현력과 정확성을 유지할 수 있도록 항상 싸워야 합니다. 당신이 운이 좋다면 에릭 에반스가 말하는 Break Through [전에는 불가능 했던 것이 새로운 가능성과 통찰력이 갑자기 나타나는 일] 것을 경험할  수도 있습니다. 그렇게 된다면 진짜 운이 좋은 것입니다. 당신이 운이 좋지 않더라도 리팩토링은 적어도 모델이 유연성을 요구할때 유연함을 만족 시킬 수는 있습니다. 이것은 미래에 나타날 통찰력과 리팩토링 요소를 더 쉽게 핸들링 할 수 있음을 의미합니다.


위 내용과 같이, DDD는 우리가 표현하고자 하는 실세계의 데이터 프로세스 자체를 컴퓨터 언어로 옮겨가는 과정입니다. 

예를 들어, SQL로 작업을 하게 되면 다음과 같은 대화가 나오게 됩니다. 

"TB_USER"에서 SELECT를 할때, POINT Column으로 Order By DESC로 얻어오고, 그 POINT값이 100점 이상인 경우에는 LEVEL column값을 2로 업데이트를 시키면 됩니다.

자, 방금 말한 내용을 실제 BL을 설계한 기획자에게 이야기를 해보도록 합시다. 과연 어떤 말인지 알아들을수 있을까요? SQL 개발자들이라면 가능하겠지요. DDD로 모델링을 거치면 다음과 같은 대화를 할 수 있습니다.

User를 표로 보여줄 때, Book Point값이 높은 순서대로 보여주고, Book Point값이 100점이상인 경우에는 Reader로 사용자 Level을 높여줘서 보여주면 됩니다.


자, 이제 좀더 말이 쉬워졌습니다. 이 말을 우리가 객체 지향 언어로 표현하면 어떻게 될까요? 

List<User> users = userDao.getAll();
users.sort("Book Point");
for(User user : users) {
    if(user.getBookPoint() >= 100) {
        user.setStatus("Reader");
    }
}
return users;

SQL query로 표현하면 다음과 같이 표현할 수 있습니다. 

UPDATE TB_USER SET LEVEL = 2 WHERE POINT >= 100;
SELECT * FROM TB_USER ORDER BY POINT DESC;


데이터 중심으로 보는 것과 모델링을 중심으로 보는것. 이 두가지의 차이는 그 데이터의 구조를 모르는 사람이 로직을 읽을수 있느냐, 없느냐에 따라 갈리게 됩니다. 그리고 우리가 문장으로 설명할 수 있는 BL을 code에 어떻게 녹여내느냐를 고민을 해야지 됩니다. 

최종적으로 DDD는 다음 목표들을 갖습니다.

Ubiquitous Language
Domain 중심의 SW팀에서는 모든 참가자들(사용자, 도메인전문가, 설계자, 프로그래머, 분석가)간에 동일한 의미를 갖는 공통된 언어를 갖는다. 심지어 공통된 언어는 Code의 Object로 구현이 가능해야지 된다.

Layered Architecture pattern
UI와 Model이 결합되어 있으면, UI가 바뀌는데에 따라 Model이 변경되어야지 됩니다. 따라서 UI, BL, Modeling간에 분리가 가능한 Layer들이 만들어져야지 됩니다. 기준에 따라 Layer를 나누고, 역할을 부여하는 작업이 필요합니다. 이에 대한 장점은 다음과 같습니다.

1. Layer들이 재사용될 수 있어야지 됩니다.
2. 표준을 지원한다.
3. 종속성을 국지적으로 최소화한다.
4. 교환 가능성이 확보된다.
5. 동작이 변경된 경우, 단계별 재작업이 필요하다.

기본적으로 Domain Model은 다음 4개의 Layer로 구분시켜서 사용하게 됩니다. (어디서 많이 본 내용입니다. ㅋ)




# Infrastructure Layer : 상위 계층을 지원하는 일반화된 기술적 기능을 제공. 공통 Library, Engine, Framework 영역
# Domain Layer : 업무 개념과 업무 상황에 대한 정보. 업무 규칙을 표현하는 Layer.
# Application Layer : 작업을 정의하고 조정하는 영역. Domain 객체로 작업을 위임하는 역활을 담당.
# UI Layer : 정보를 노출하고 입력을 받아들이는 영역

이러한 구조는 결국은 Domain의 격리, 즉 분리가 이루어지게 됩니다.  이러한 Layer architecture중 가장 대표적인 방식이 MVC 입니다. 



Model 은 기본 기능을, View는 User Interface를 의미하게됩니다. Controller는 M과 V간의 직접적인 연결을 막음으로서 독립성을 유지하는 역활을 하게 됩니다. 추후에 Spring @MVC에서 보다 더 설명이 될 영역이기도합니다.  

Smart UI anti pattern
모든 업무로직을 사용자 인터페이스에 넣는 설계 방법을 Smart UI pattern이라고 합니다. 

특징은 
- application을 작은 기능으로 잘게 나누고,
- 나뉜 기능을 분리된 UI로 구현. 업무 규칙이 분리된 UI에 들어가게 합니다.
- 분리된 업무 규칙은 RDBMS를 이용해서 데이터를 공유하고 실행합니다. 주로 SP가 이 용도로 사용됩니다.
- 주로 자동화된 UI 구축 도구와 시각적인 프로그래밍 도구를 이용합니다. 

DDD pattern에서 가장 피해야지 되는 것이 바로 UI Pattern입니다.
UI pattern이라는 것은 지금도 매우 자주 쓰이고 있는 패턴입니다. 이 패턴의 가장 큰 문제는 우리가 사용하는 데이터 및 Domain에 대한 모든 로직을 보여지는 View에 맞추게 됩니다. 실질적인 데이터의 흐름을 방해하고, 언제나 바뀔 수 있는 View 영역에 Model이 영속되기 때문에 application의 변경에 취약한 약점을 갖게 됩니다. 

대표적인 Smart UI pattern의 도구가 PowerBuilder입니다. Data Window의 Smart UI를 이용하기 위해, UI에 따른 Model과 로직이 꼬인 상태로 존재하게 됩니다. 이는 필연적으로 코드의 중복을 가지고 오게 되며, 아직까지 UI에 대한 명확한 테스트를 할 수 없는 현 시점에서 유지보수가 거의 불가능한 시스템을 만들게 됩니다. *anti pattern은 쓰지 말라고 있는겁니다.*

Entities pattern
ID를 갖는 unique한 object를 갖는 pattern입니다. 이는 RDBMS의 PK와 연결시켜, 이 객체가 유일한 어떤 값임을 나타내는 방법을 제공합니다.

Service pattern
서비스는 기능을 처리하거나, Entity로 구별할 수 없는 것을 지칭합니다. 가장 주로 사용되는 것은 BL의 Group을 표현하는 것이 가장 많습니다.

Factory pattern
각각의 Object, Service의 생성방법에 대한 통일성을 갖는것을 목표로 합니다. 어떤 객체를 생성할 때, 초기화 해줘야지 되는 값이라던지 실행시켜줘야지 되는 method가 독특하게 존재한다면 개발자 및 참여자들은 Domain에 집중할 수 없습니다. Spring의 ApplicationContext는 이런 경우 가장 좋은 해결 방법이 됩니다.

Repository pattern
생성된 객체나 Model이 외부(주로 DB)와 연동되어 생명주기를 가질때, 그 생명주기에 대한 관리 Focus가 되는 Layer가 존재해야지 됩니다. dao로 구현되는 것이 일반적이며, Spring에서는 @Repository로 지정하는 것이 일반적입니다.

DDD의 사상을 반영하기 위해 주로 사용되는 툴이 ORM이고, java에서 가장 오랜 시간동안 개발이 되고 사랑받고 있는 Hibernate가 그 선두 주자라고 할 수 있습니다.


Hibernate를 이용한 Dao의 개발

Hibernate를 이용한 Dao를 개발해보도록 하겠습니다. 먼저, 새로운 maven 프로젝트를 하나 만들어주세요. 그리고, 다음 jar들을 추가하도록 합니다.

    <dependency>
      <groupId>org.hibernate</groupId>
      <artifactId>hibernate-core</artifactId>
      <version>4.1.9.Final</version>
    </dependency>
    <dependency>
      <groupId>c3p0</groupId>
      <artifactId>c3p0</artifactId>
      <version>0.9.1.2</version>
    </dependency>
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>5.1.22</version>
    </dependency>

그리고 entities와 dao, util package를 구성해주고, 기존의 dao의 interface와 entity를 모두 구성해주세요. file을 copy 한 후, package 명을 변경해도 좋습니다. 최종적으로 모든 객체가 다 완성되면 다음과 같은 형태가 될 것입니다. 


먼저, ORM 을 사용하기 위해서는 Hibernate가 정확히 어떻게 객체들과 DB간에 연결되어있는지 정의하는 방법이 필요합니다. Hibernate는 2개의 방법을 제공하고 있는데, hbm.xml 파일을 이용한 xml mapping과 annotation을 이용한 mapping을 제공하고 있습니다. 


지금까지 우리가 만든 객체들의 관계는 어떻게 될까요?

먼저, Book은 대여한 User와의 관계가 있습니다. 이 관계는 N:0의 관계를 갖습니다.  또한 여러개의 History를 가질 수 있습니다.
그리고 History는 관계된 User와 Book간의 모든 관계를 갖습니다. 
마지막으로 User는 어떤 관계를 가질까요? Table상에서는 관계가 나타나지 않습니다. 그렇지만, 하나의 User는 여러개의 Book을 대여할 수 있고, 여러개의 History를 가질 수 있습니다.  이를 반영할 수 있는 객체로 표현해보면 다음과 같은 객체를 선언할 수 있습니다.

public class Book {
    private int id;
    private String name;
    private String author;
    private BookStatus status;
    private Date publishDate;
    private User rentUser;
    private String comment;
    private List<History> histories;
}

public class History {
    private int id;
    private User user;
    private Book book;
    private ActionType action;
    private Date insertDate;
}

public class User {
    private int id;
    private String name;
    private String password;
    private UserLevel level;
    private int point;
    private List<History> histories;
}

각 객체들로 서로간에 동작을 이해할 수 있는 코드들이 나오게 됩니다. 이제 이 객체를 DB와 연결을 시켜야지 됩니다. DB와 연결을 시키는 방법은 전통적인 xml을 사용하는 방법과 @annotation을 이용한 방법 두가지로 나눌 수 있습니다.  기본적으로 이제 java 표준이 된 @annotation을 이용한 ORM mapping을 중심으로 알아보도록 하겠습니다. xml을 이용한 mapping 방법은 각자 숙제로 남겨두도록 하겠습니다. 

Hibernate를 이용하기 위해서는 먼저 DB의 접근을 제공해야지 됩니다. 이는 SessionFactory를 통해서 이루어지며, SessionFactory에서 얻어지는 Session을 이용해서 DB에 접근하게 됩니다. 그리고 만들어진 Session을 이용해서 DB에 객체의 query를 만들어주고, 그 query를 실행하게 됩니다.  

지금까지 나온 개념을 좀 정리해보도록 하겠습니다. 

1. Entity : Domain Model과 Persistence Object간의 연결 객체
2. SessionFactory : Entity를 얻어내기 위한 Session을 관리하는 객체
3. Session : Entity를 Persistence Layer에서 얻어내는 객체

가 됩니다. 조금 더 간단히 생각해보면, Session은 JdbcTemplate와 비슷한 개념을 가지고 있고, SessionFactory는 DataSource와 비슷한 개념을 가지게 됩니다. 단 기능면에서는 좀 많은 차이를 가지고 있지요. 

SessionFactory를 구성하기 위해서는 hibernate.cfg.xml 파일을 통해서 이루어지며, 파일의 구성은 다음과 같습니다.

<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE hibernate-configuration PUBLIC
        "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
        "http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd"><hibernate-configuration>
  <session-factory>
    <property name="hibernate.connection.driver_class">com.mysql.jdbc.Driver</property>
    <property name="hibernate.connection.password">qwer12#$</property>
    <property name="hibernate.connection.url">jdbc:mysql://localhost/bookstore3</property>
    <property name="hibernate.connection.username">root</property>
    <property name="hibernate.dialect">org.hibernate.dialect.MySQL5InnoDBDialect</property>
    <property name="hibernate.show_sql">true</property>
    <property name="hibernate.c3p0.min_size">5</property>
    <property name="hibernate.c3p0.max_size">20</property>
    <property name="hibernate.c3p0.timeout">300</property>
    <property name="hibernate.c3p0.max_statements">50</property>
    <property name="hibernate.c3p0.idle_test_period">3000</property>
    <mapping class="com.xyzlast.hibernate.bookstore.entities.User" />
    <mapping class="com.xyzlast.hibernate.bookstore.entities.Book" />
    <mapping class="com.xyzlast.hibernate.bookstore.entities.History" />
  </session-factory>
</hibernate-configuration>


파일이 매우 깁니다. 그리고 내용이 복잡해보이지만, 기존의 DataSource를 이용했을 때와 큰 차이가 없습니다. driver_class, password, username, url의 경우에는 기존의 DataSource와 완전히 동일합니다. 
dialect의 경우, hibernate와 db간의 연결을 해주기위한 설정입니다. 우리가 사용한 MySql5의 경우 MySQL5InnoDBDialect를 사용을 하며 이것은 Oracle을 사용하는 경우에는 Oracle10Dialect를 사용하면 됩니다. JDBC가 DB를 연결할 때 사용하는 것이 driver_class라면, 그 driver class와 Hibernate간에 연결하는 중간다리 역활을 하는 것이 dialect입니다. 

Hibernate는 기본적으로 c3p0 connection pool을 사용하도록 구성되어 있습니다. 앞으로는 사용하지는 않을것이지만, c3p0에 대해서 알아두시는것도 괜찮습니다. 
그리고, 나머지 c3p0로 시작되는 설정은 모두 connection pool에 대한 설정들입니다. 지금까지 우리가 만든 application은 한개의 DB connection만을 가지고, 처리를 했습니다. 만약에 web system에 올렸다면 이는 큰 문제가 되는 것으로, db에 query를 날릴때 딱 1개만의 연결을 갖게 됩니다. 여러 사용자가 동시에 사용을 할때, 먼저 사용자가 connection을 갖게 되면 그 다음 처리를 못해주게 됩니다. c3p0에 대한 설정들은 다음과 같습니다. 

1. min_size : connection pool의 최소 갯수를 지정합니다. connection을 application이 시작한 다음 최소 5개는 만들도록 설정한 상태입니다.
2. max_size : connection pool의 최대 갯수를 지정합니다. connection을 application이 시작한 후, 최대 20개까지 만들도록 설정했습니다.
3. timeout : connection의 생명 시간을 지정합니다. 처음 만들어진지 300sec가 지나면 connection을 소멸하도록 합니다.
4. max_statement : 1개의 connection당 최대 query문을 지정합니다. 최대 50개의 query문이 쌓이게 되면 DB측에 flush로 한번에 보내버립니다. 이는 Transaction의 성능에 영향을 미치고 db에 대한 bulk 작업에 도움을 줍니다.
5. idle_test_period : connection을 유지하고 있지만, db측에서 connection을 끊어버린 것을 확인하는 시간입니다. 이는 db server가 connection을 같이 관리할 때, 이 값을 통해서 자신의 connection을 계속해서 유효하도록 만드는 시간주기입니다. 

나머지 mapping class의 경우, mapping된 class를 추가해주는 방법입니다. 

자, 이제 객체 선언 코드들입니다. 너무 길어서 get/set code는 제거되었습니다. 한번 확인해보도록 하겠습니다. 

@Entity
@Table(name="books")
public class Book {
    @Id
    @Column(name="id")
    @GeneratedValue(strategy = GenerationType.AUTO)
    private int id;
    @Column(name="name")
    private String name;
    @Column(name="author")
    private String author;
    @Column(name="status")
    @Enumerated(EnumType.ORDINAL)
    private BookStatus status;
    @Column(name="publishDate")
    private Date publishDate;
    @JoinColumn(name="rentUserId", nullable=true)
    @ManyToOne
    private User rentUser;
    @Column(name="comment", nullable=true)
    private String comment;
    @OneToMany(mappedBy="book")
    private Set<History> bookHistories = new ArrayList<>();
}


@Entity
@Table(name="histories")
public class History {
    @Id
    @Column(name="id")
    @GeneratedValue(strategy=GenerationType.AUTO)
    private int id;
    @ManyToOne
    @JoinColumn(name="userId")
    private User user;
    @ManyToOne
    @JoinColumn(name="bookId")
    private Book book;
    @Enumerated(EnumType.ORDINAL)
    @Column(name="actionType")
    private ActionType action;
    @Column(name="insertDate")
    private Date insertDate;
}

@Entity
@Table(name="users")
public class User {
    @Id
    @Column(name="id")
    @GeneratedValue(strategy=GenerationType.AUTO)
    private int id;
    @Column(name="name")
    private String name;
    @Column(name="password")
    private String password;
    @Enumerated(EnumType.ORDINAL)
    @Column(name="level")
    private UserLevel level;
    @Column(name="point")
    private int point;
    @OneToMany(mappedBy="user")
    private List<History> histories = new ArrayList<>();
    @OneToMany(mappedBy="rentUser")
    private List<Book> rentBooks = new ArrayList<>();
}

@annotation이 많이 생겼습니다. 각각의 @annotation에 대해서 간단히 알아보도록 하겠습니다. 

@Entityentity 객체임을 지정하는 annotation입니다.
@Tableentitty 객체와 연결되는 table을 지정하는 annotation입니다.
@IdPK를 지정하는 annotation입니다.
@ColumnDB의 Column과 1:1로 mapping되는 Property를 지정하는 annotation입니다.
@GeneratedValuePK의 값이 지정되는 방법을 결정할 수 있습니다. AUTO의 경우 mysql의 AUTO_INCREMENT에 해당됩니다. 각각의 DB에 따라 다른 값을 넣어주는 것도 가능합니다.(ex : oracle의 sequence)
@Enumeratedenum 값과 1:1로 mapping이 되는 것을 지정합니다.
@ManyToOne자신과 같은 객체들이 다른 한개의 객체에 연관이 있음을 지정합니다. 이는 N:1의 속성을 지정하게 됩니다.
@JoinColumn@ManyToOne과 같이 사용됩니다. Join 되는 Column을 지정합니다. (FK column)
@OneToMany자신이 다른 객체들에 연관이 있음을 지정합니다. 이는 1:N 또는 N:N의 속성을 지정합니다.

여기에서 개발자들에게 개념적으로 힘든 부분이 @ManyToOne과 @JoinColumn, @OneToMany입니다. 먼저 지금까지 만든 entity에 대한 개념을 한번 더 정립해보도록 하겠습니다. 


하나의 엔티티 클래스는 다음의 요구조건을 충족해야 합니다:

# 클래스 선언부에 javax.persistence.Entity 어노테이션을 반드시 명시하여야 합니다.
# 기본 생성자를 반드시 포함해야 합니다. 기본생성자는 인수가 없는 생성자를 의미합니다. 만일, 인수를 포함한 생성자를 사용한다면 명시적으로 기본 생성자를 만들어야만 합니다.
# 클래스를 fianl로 선언해서는 안됩니다. 영속화 대상이 아닌 필드나 메소드는 반드시 final로 선언합니다.
# 엔티티 인스턴스가 세션빈의 리모트 비지니스 인터페이스와 같이 detached object형태로 전달되는 경우, 클래스는 반드시 Serializble 인터페이스를 구현해야 합니다.
# 엔티티는 엔티티 또는 non-엔티티 클래스 모두 확장(extend)이 가능하며, non-엔티티 클래스는 엔티티 클래스를 확장할 수 있습니다.
# 영속화 인스턴스 변수는 반드시 private, protected, package-private중 하나로 선언되어야 하며, 엔티티 클래스의 메소드에 의해 직접 참조될 수 있습니다. 클라이언트는 엔티티의 상태를 접근자(accessor)또는 비지니스 메소드를 통해 접근이 가능합니다.

또한, 영속상태의 엔티티는 엔티티의 인스턴스 변수 또는 자바빈 스타일의 속성에 의해 접근할 수 있습니다. 필드 또는 속성은 반드시 다음의 자바 언어 타입에 따릅니다.

# 자바 원시 타입
# java.lang.String
# 그외 직렬화 가능(Serializable) 타입들
# 자바 원시타입의 Wrappers
# java.math.BigInteger
# java.math.BigDecimal
# java.util.Date
# java.util.Calendar
# java.sql.Date
# java.sql.Time
# java.sql.Timestamp
# 사용자 정의 직렬화 타입들
# byte[]
# Byte[]
# char[]
# Character[]
# 열거형(Enumerated) 타입들
# 다른 엔티티 또는 엔티티 컬렉션
# 내장형(Embeddable) 클래스

각 엔티티들은 다음과 같은 관계를 가질 수 있습니다. 

# One-to-one : 각 엔티티 인스턴스는 하나의 인스턴스가 다른 엔티티와 연관됩니다. One-to-one 관계는 javax.persistence.OneToOne 어노테이션으로 해당 필드에 정의합니다.
# One-to-many : 하나의 엔티티 인스턴스가 다수의 다른 엔티티 인스턴스와 연관됩니다. 영업주문의 경우 다수의 라인 아이템을 가집니다. 즉, Order 엔티티는 여러개의 LineItem 을 가지므로 이들 사이에는 One-to-many 관계가 선언되어야 하므로 javax.persistence.OneToMany 어노테이션을 사용합니다.
# Many-to-one : 다수의 인스턴스가 하나의 다른 엔티티와 연관됩니다. 이것은 One-to-many 와 반대입니다. 영업주문의 경우 LineItem은 Order에 대해 Many-to-one 관계가 성립되므로 javax.persistence.ManyToOne 어노테이션을 지정합니다.
# Many-to-many : 이 인스턴스는 다수의 인스턴스가 각기 다른 엔티티들과 연관됩니다. 예를들어, 학교에서 각 수업들은 다수의 학생들과 연관이 있으며, 학생 역시 다수의 수업을 듣고 있습니다. 이경우 수업과 학생은 Many-to-Many 관계가 성립되며 javax.persistence.ManyToMany 어노테이션을 사용합니다.

엔티티 연관의 방향 : 엔티티간의 연관 관계는 단방향 또는 양방향이 될 수 있습니다. 양방향 관계에서 한쪽은 소유자측이 되며 그 반대는 피소유자가 됩니다. 단방향 연관은 소유자측만 대변합니다. 연관관계에서 소유자측은 영속성 런타임(Persistence Runtime)이 연관관계에 있는 데이터를 어떻게 갱신할지 결정합니다.

양방향 관계

양방향 관계에서, 각 엔티티는 서로 상대필드나 속성에 대한 참조를 가집니다. 예를들어, User는 어떤 History 인스턴스들이 있는지 알고 있으며 History는 자신이 어떤 User에 속해있는지 알고 있습니다. 이 경우 이들은 양방향 관계를 가지고 있다고 볼 수 있습니다.
양방향 관계는 반드시 다음의 규칙을 따릅니다:

# 양방향 관계의 반대편(피소유자)은 반드시 소유자측을 mappedBy를 사용해 참조해야 합니다. mappdBy는 @OneToOne, @OneToMany 또는 @ManyToMany 어노테이션에서 사용할 수 있습니다. mappedBy 는 소유자 엔티티의 필드나 속성과 대응됩니다.
# Many-to-one 양방향 관계에서 Many측 관계는 mappedBy요소를 사용할 수 없습니다. Many측은 항상 관계에서 소유자측이어야 합니다.
# one-to-one 양방향 관계에서, 소유자측은 다른측에 대한 FK를 가지는쪽입니다.
# Many-to-many 양방향 관계는 양쪽중 둘 중 아무나 소유자가 될 수 있습니다.


entity와 entity간의 관계에 대한 정리가 조금 되셨나요? 이 부분이 이해가 되지 않는다면 Dao부분을 구성할 수 없습니다. 꼭 코드와 같이 이해를 해주시길 바랍니다. 

자, 이제까지 만들어진 객체를 기반으로 Dao를 작성해보도록 하겠습니다. 

먼저, SessionFactory를 생성해주는 객체를 구성해보도록 하겠습니다. SessionFactory는 Configuration 객체를 통해서 구현이 되며, 위에 소개된 hibernate.cfg.xml 파일을 기반으로 구성이 되게 됩니다. 코드는 다음과 같습니다. 

public class HibernateSessionFactoryBuilder {
    public static SessionFactory build(String filename) {
        Configuration cfg = new Configuration();
        cfg.configure(filename);
        ServiceRegistryBuilder serviceRegistryBuilder = new ServiceRegistryBuilder()
                                                            .applySettings(cfg.getProperties());
        SessionFactory sessionFactory = cfg.configure().buildSessionFactory(
                serviceRegistryBuilder.buildServiceRegistry());
        return sessionFactory;
    }
}

그리고, Hibernate는 기본적으로 Session을 통해서 DB와 연결합니다. 그리고 Session의 모든 Action은 Transaction을 기반으로 동작하게 됩니다. 따라서, Template-callback pattern을 이용해서 Hibernate의 Transaction을 처리하는 구문을 만들어주는 것이 구성이 용의합니다. HibernateSqlExecutor를 작성하고, 그 코드는 다음과 같이 동작하게 됩니다. 

public class HibernateSqlExecutor {
    private final SessionFactory sessionFactory;

    public HibernateSqlExecutor(SessionFactory sessionFactory) {
        this.sessionFactory = sessionFactory;
    }

    public Object execute(HibernateAction action) {
        Session session = sessionFactory.openSession();
        Transaction transaction = session.beginTransaction();
        try {
            Object ret = action.doProcess(session);
            transaction.commit();
            return ret;
        } catch(Exception ex) {
            transaction.rollback();
            throw ex;
        } finally {
            session.close();
        }
    }
}

public interface HibernateAction {
    Object doProcess(Session session);
}

이 HibernateSqlExecutor를 기반으로 구성되는 Hibernate의 Dao는 다음과 같이 꾸며지게 됩니다. 

public class BookDaoImpl implements BookDao {
    @Autowired
    private SessionFactory sessionFactory;
    
    private HibernateSqlExecutor executor;
    
    public SessionFactory getSessionFactory() {
        return sessionFactory;
    }

    public void setSessionFactory(SessionFactory sessionFactory) {
        this.sessionFactory = sessionFactory;
    }

    public HibernateSqlExecutor getExecutor() {
        return executor;
    }

    public void setExecutor(HibernateSqlExecutor executor) {
        this.executor = executor;
    }

    @Override
    public void add(final Book book) {
        executor.execute(new HibernateAction() {
            @Override
            public Object doProcess(Session session) {
                session.save(book);
                return null;
            }
        });
    }

    @Override
    public Book get(final int id) {
        return (Book) executor.execute(new HibernateAction() {
            @Override
            public Object doProcess(Session session) {
                Book book = (Book) session.get(Book.class, id);
                return book;
            }
        });
    }

    @Override
    public void update(final Book book) {
       executor.execute(new HibernateAction() {
            @Override
            public Object doProcess(Session session) {
                session.refresh(book);
                session.update(book);
                return null;
            }
        });
    }

    @SuppressWarnings("unchecked")
    @Override
    public void deleteAll() {
       executor.execute(new HibernateAction() {
            @Override
            public Object doProcess(Session session) {
                List<Book> books = session.createCriteria(Book.class).list();
                for(Book book : books) {
                    session.delete(book);
                }
                return null;
            }
        });
    }

    @Override
    public int countAll() {
       return (int) executor.execute(new HibernateAction() {
            @Override
            public Object doProcess(Session session) {
                Long count = (Long) session.createCriteria(Book.class)
                        .setProjection(Projections.rowCount())
                        .uniqueResult();
                if(count == null) {
                    return 0;
                }
                return count.intValue();
            }
        });
    }

    @Override
    public void delete(final Book book) {
       executor.execute(new HibernateAction() {
            @Override
            public Object doProcess(Session session) {
                session.delete(book);
                return null;
            }
        });
    }

    @SuppressWarnings("unchecked")
    @Override
    public List<Book> getAll() {
        return (List<Book>) executor.execute(new HibernateAction() {
                @Override
                public Object doProcess(Session session) {
                    return session.createCriteria(Book.class).list();
                }
            });
    }

    @SuppressWarnings("unchecked")
    @Override
    public List<Book> search(final String name) {
        return (List<Book>) executor.execute(new HibernateAction() {
            @Override
            public Object doProcess(Session session) {
                return session.createCriteria(Book.class)
                        .add(Restrictions.like("name", name, MatchMode.ANYWHERE))
                        .list();
            }
        });
    }
}


public class UserDaoImpl implements UserDao {
    @Autowired
    private SessionFactory sessionFactory;
    @Autowired
    private HibernateSqlExecutor executor;

    public SessionFactory getSessionFactory() {
        return sessionFactory;
    }

    public void setSessionFactory(SessionFactory sessionFactory) {
        this.sessionFactory = sessionFactory;
    }

    public HibernateSqlExecutor getExecutor() {
        return executor;
    }

    public void setExecutor(HibernateSqlExecutor executor) {
        this.executor = executor;
    }


    @Override
    public int countAll() {
        return (int) executor.execute(new HibernateAction() {
            @Override
            public Object doProcess(Session session) {
                Long count = (Long) session.createCriteria(User.class).setProjection(Projections.rowCount())
                        .uniqueResult();
                if (count == null) {
                    return 0;
                }
                return count.intValue();
            }
        });
    }

    @Override
    public User get(final int id) {
        return (User) executor.execute(new HibernateAction() {
            @Override
            public Object doProcess(Session session) {
                return session.get(User.class, id);
            }
        });
    }

    @Override
    public void update(final User user) {
        executor.execute(new HibernateAction() {
            @Override
            public Object doProcess(Session session) {
                session.refresh(user);
                session.update(user);
                return null;
            }
        });
    }

    @Override
    public void delete(final User user) {
        executor.execute(new HibernateAction() {
            @Override
            public Object doProcess(Session session) {
                session.refresh(user);
                session.delete(user);
                return null;
            }
        });
    }

    @Override
    public void add(final User user) {
        executor.execute(new HibernateAction() {
            @Override
            public Object doProcess(Session session) {
                session.save(user);
                return null;
            }
        });
    }

    @SuppressWarnings("unchecked")
    @Override
    public List<User> getAll() {
        return (List<User>) executor.execute(new HibernateAction() {
            @Override
            public Object doProcess(Session session) {
                return session.createCriteria(User.class).list();
            }
        });
    }

    @Override
    public void deleteAll() {
        executor.execute(new HibernateAction() {
            @Override
            public Object doProcess(Session session) {
                @SuppressWarnings("unchecked")
                List<User> users = session.createCriteria(User.class).list();
                for (User user : users) {
                    session.delete(user);
                }
                return null;
            }
        });
    }
}

public class HistoryDaoImpl implements HistoryDao {
    @Autowired
    private SessionFactory sessionFactory;
    @Getter
    @Setter
    private HibernateSqlExecutor executor;
    
    @Override
    public void add(final History userHistory) {
        executor.execute(new HibernateAction() {
            @Override
            public Object doProcess(Session session) {
                session.save(userHistory);
                return null;
            }
        });
    }
    
    @SuppressWarnings("unchecked")
    @Override
    public List<History> getByUser(final User user) {
       return (List<History>) executor.execute(new HibernateAction() {
            @Override
            public Object doProcess(Session session) {
                return session.createCriteria(History.class)
                        .add(Restrictions.eq("user", user))
                        .addOrder(Order.desc("insertDate")).list();
            }
        });
    }
    
    @SuppressWarnings("unchecked")
    @Override
    public void deleteAll() {
       executor.execute(new HibernateAction() {
            @Override
            public Object doProcess(Session session) {
                List<History> userHistories = session.createCriteria(History.class).list();
                for(History userHistory : userHistories) {
                    session.delete(userHistory);
                }
                return null;
            }
        });
    }
    
    @SuppressWarnings("unchecked")
    @Override
    public List<History> getByBook(final Book book) {
        return (List<History>) executor.execute(new HibernateAction() {
            @Override
            public Object doProcess(Session session) {
                return session.createCriteria(History.class)
                        .add(Restrictions.eq("book", book))
                        .addOrder(Order.desc("insertDate")).list();
            }
        });
    }

    @Override
    public int countAll() {
        return (int) executor.execute(new HibernateAction() {
            @Override
            public Object doProcess(Session session) {
                Long count = (Long) session.createCriteria(History.class).setProjection(Projections.rowCount())
                        .uniqueResult();
                if (count == null) {
                    return 0;
                }
                return count.intValue();
            }
        });
    }
}

코드가 많이 바뀌었습니다. 한번 코드에 대해서 논해보도록 하겠습니다. 
Hibernate는 Session을 통해서 DB에 대한 접근을 행하게 됩니다. 그리고 Session은 Criteria를 통해, 각 객체에 대한 query를 생성하게 됩니다. 그리고 Criteria는 Restriction과 Projection을 통해서 각각의 DB에 query를 행하고, CUD의 경우에는 save, update, delete라는 action을 통해서 구성하게 됩니다. 

먼저, countAll()을 하는 code를 간단하게 살펴보도록 하겠습니다. 

    @Override
    public int countAll() {
       return (int) executor.execute(new HibernateAction() {
            @Override
            public Object doProcess(Session session) {
                Long count = (Long) session.createCriteria(Book.class)
                        .setProjection(Projections.rowCount())
                        .uniqueResult();
                if(count == null) {
                    return 0;
                }
                return count.intValue();
            }
        });
    }

위 코드에서 setProjection을 통해서, rowCount를 하는 것을 지정합니다. 그리고 그 값을 uniqueResult를 통해서 얻어옵니다. rowCount라는 값 자체는 저 query가 수행되면 단일 값으로 나오기 때문에 uniqueResult를 통해서 얻어오게 됩니다. 

그럼 다음은 어떤 코드를 한번봐볼까요? HistoryDao의 getByUser를 한번 봐보도록 하겠습니다. 

    @SuppressWarnings("unchecked")
    @Override
    public List<History> getByUser(final User user) {
       return (List<History>) executor.execute(new HibernateAction() {
            @Override
            public Object doProcess(Session session) {
                return session.createCriteria(History.class)
                        .add(Restrictions.eq("user", user))
                        .addOrder(Order.desc("insertDate")).list();
            }
        });
    }

보시면, History객체의 user property와 동일한 user를 list로 얻어오는 것을 직관적으로 지정하는 것을 알 수 있습니다. 


마지막으로, 테스트 코드를 통해서 Hibernate의 강력함을 한번 느껴보도록 하겠습니다. 지금 객체가 가장 복잡하게 꼬여있다고 생각되는 History에 대한 테스트 코드입니다. 

public class HistoryDaoImplTest {
    private HibernateSqlExecutor executor;
    private BookDaoImpl bookDao;
    private UserDaoImpl userDao ;
    private HistoryDaoImpl historyDao;
    
    @Before
    public void setUp() {
        SessionFactory sessionFactory = HibernateSessionFactoryBuilder.build("hibernate.cfg.xml");
        executor = new HibernateSqlExecutor();
        executor.setSessionFactory(sessionFactory);
        
        bookDao = new BookDaoImpl();
        bookDao.setExecutor(executor);
        
        userDao = new UserDaoImpl();
        userDao.setExecutor(executor);
        
        historyDao = new HistoryDaoImpl();
        historyDao.setExecutor(executor);
        
        historyDao.deleteAll();
        bookDao.deleteAll();
        userDao.deleteAll();
        
        assertThat(bookDao.countAll(), is(0));
        assertThat(userDao.countAll(), is(0));
        assertThat(historyDao.countAll(), is(0));
        
        for(Book book : getBooks()) {
            bookDao.add(book);
        }
        
        for(User user : getUsers()) {
            userDao.add(user);
        }
    }
    
    @Test
    public void addAndCount() {
        List<Book> books = bookDao.getAll();
        List<User> users = userDao.getAll();
        int count = 0;
        for(Book book : books) {
            for(User user : users) {
                History history = new History();
                history.setAction(HistoryActionType.RENT);
                history.setBook(book);
                history.setUser(user);
                history.setInsertDate(new Date());
                historyDao.add(history);
                count++;
                assertThat(historyDao.countAll(), is(count));
            }
        }
        
        List<User> users2 = userDao.getAll();
        for(User user : users2) {
            System.out.println("==========================================");
            System.out.println("User : " + user.getName());
            for(History history : user.getHistories()) {
                System.out.println("History : BOOK NAME >" + history.getBook().getName());
            }
        }
    }
    
    @Test
    public void getByUser() {
        List<Book> books = bookDao.getAll();
        List<User> users = userDao.getAll();
        int count = 0;
        for(Book book : books) {
            for(User user : users) {
                History history = new History();
                history.setAction(HistoryActionType.RENT);
                history.setBook(book);
                history.setUser(user);
                history.setInsertDate(new Date());
                historyDao.add(history);
                count++;
                assertThat(historyDao.countAll(), is(count));
            }
        }
        
        for(User user : users) {
            List<History> histories = historyDao.getByUser(user);
            assertThat(histories.size(), is(books.size()));
        }
    }
    
    @Test
    public void getByBook() {
        List<Book> books = bookDao.getAll();
        List<User> users = userDao.getAll();
        int count = 0;
        for(Book book : books) {
            for(User user : users) {
                History history = new History();
                history.setAction(HistoryActionType.RENT);
                history.setBook(book);
                history.setUser(user);
                history.setInsertDate(new Date());
                historyDao.add(history);
                count++;
                assertThat(historyDao.countAll(), is(count));
            }
        }
        
        for(Book book : books) {
            List<History> histories = historyDao.getByBook(book);
            assertThat(histories.size(), is(users.size()));
        }
    }
    
    private List<User> getUsers() {
        User user1 = new User();
        user1.setName("name01");
        user1.setPassword("password01");
        user1.setPoint(99);
        user1.setLevel(UserLevel.NORMAL);
        
        User user2 = new User();
        user2.setName("name02");
        user2.setPassword("password02");
        user2.setPoint(101);
        user2.setLevel(UserLevel.READER);
        
        User user3 = new User();
        user3.setName("name03");
        user3.setPassword("password03");
        user3.setPoint(301);
        user3.setLevel(UserLevel.MVP);
        
        return Arrays.asList(user1, user2, user3);
    }
    
    private List<Book> getBooks() {
        Book book1 = new Book();
        book1.setName("book name01");
        book1.setAuthor("autor name 01");
        book1.setComment("comment01");
        book1.setPublishDate(new Date());
        book1.setStatus(BookStatus.NORMAL);
        
        Book book2 = new Book();
        book2.setName("book name02");
        book2.setAuthor("autor name 02");
        book2.setComment("comment02");
        book2.setPublishDate(new Date());
        book2.setStatus(BookStatus.NOWRENT);
        
        Book book3 = new Book();
        book3.setName("book name03");
        book3.setAuthor("autor name 03");
        book3.setComment("comment03");
        book3.setPublishDate(new Date());
        book3.setStatus(BookStatus.MISSING);
        
        List<Book> books = Arrays.asList(book1, book2, book3);
        return books;
    }
}

대부분의 테스트코드와 비슷하게 데이터를 지워버리고, 신규 데이터를 넣도록 구성이 되어 있습니다. addAndCount의 실행결과는 다음과 같습니다. 

==========================================
User : name01
Hibernate: select histories0_.userId as userId0_3_, histories0_.id as id2_3_, histories0_.id as id2_2_, histories0_.actionType as actionType2_2_, histories0_.bookId as bookId2_2_, histories0_.insertDate as insertDate2_2_, histories0_.userId as userId2_2_, book1_.id as id1_0_, book1_.author as author1_0_, book1_.comment as comment1_0_, book1_.name as name1_0_, book1_.publishDate as publishD5_1_0_, book1_.rentUserId as rentUserId1_0_, book1_.status as status1_0_, user2_.id as id0_1_, user2_.level as level0_1_, user2_.name as name0_1_, user2_.password as password0_1_, user2_.point as point0_1_ from userHistories histories0_ left outer join books book1_ on histories0_.bookId=book1_.id left outer join users user2_ on book1_.rentUserId=user2_.id where histories0_.userId=?
History : BOOK NAME >book name02
History : BOOK NAME >book name01
History : BOOK NAME >book name03
==========================================
User : name02
Hibernate: select histories0_.userId as userId0_3_, histories0_.id as id2_3_, histories0_.id as id2_2_, histories0_.actionType as actionType2_2_, histories0_.bookId as bookId2_2_, histories0_.insertDate as insertDate2_2_, histories0_.userId as userId2_2_, book1_.id as id1_0_, book1_.author as author1_0_, book1_.comment as comment1_0_, book1_.name as name1_0_, book1_.publishDate as publishD5_1_0_, book1_.rentUserId as rentUserId1_0_, book1_.status as status1_0_, user2_.id as id0_1_, user2_.level as level0_1_, user2_.name as name0_1_, user2_.password as password0_1_, user2_.point as point0_1_ from userHistories histories0_ left outer join books book1_ on histories0_.bookId=book1_.id left outer join users user2_ on book1_.rentUserId=user2_.id where histories0_.userId=?
History : BOOK NAME >book name03
History : BOOK NAME >book name02
History : BOOK NAME >book name01
==========================================
User : name03
Hibernate: select histories0_.userId as userId0_3_, histories0_.id as id2_3_, histories0_.id as id2_2_, histories0_.actionType as actionType2_2_, histories0_.bookId as bookId2_2_, histories0_.insertDate as insertDate2_2_, histories0_.userId as userId2_2_, book1_.id as id1_0_, book1_.author as author1_0_, book1_.comment as comment1_0_, book1_.name as name1_0_, book1_.publishDate as publishD5_1_0_, book1_.rentUserId as rentUserId1_0_, book1_.status as status1_0_, user2_.id as id0_1_, user2_.level as level0_1_, user2_.name as name0_1_, user2_.password as password0_1_, user2_.point as point0_1_ from userHistories histories0_ left outer join books book1_ on histories0_.bookId=book1_.id left outer join users user2_ on book1_.rentUserId=user2_.id where histories0_.userId=?
History : BOOK NAME >book name01
History : BOOK NAME >book name03
History : BOOK NAME >book name02

내용이 복잡해보이지만, 신기한 특성이 보이지 않나요? User 객체를 한개 얻어왔을 뿐인데, User에 딸린 History가 모두 얻어집니다. 그리고 그 History는 Book객체를 모두 가지고 있고, 이건 DB에 저장된 결과를 보여주고 있습니다. 이와 같이 Hibernate는 DB와 실제 객체간의 연결을 매우 powerful 하게 지원하고 있습니다. DB에 저장된 결과를 마치 고구마 줄기 뽑듯이 객체에 대한 데이터를 들고올 수 있습니다.  

한번 여기까지 Dao 코드를 모두 구성해보고 동작을 확인해보시길 바랍니다. 그리고 console에 Hibernate의 action이 발생하는 경우, 모두 query가 console에 나오게 되어있기 때문에, action에 따른 query 발생이 어떻게 동작하고 있는지 확인하시면 더 재미있는 코드를 짜보실수 있습니다. 

Summary

Hibernate를 통한 Dao의 구성을 알아봤습니다. 여기서 지금 알아본 Hibernate에 대한 내용은 너무나 미약합니다. Hibernate의 경우 책 한권 이상으로 끝나는 내용이 아닙니다. Hibernate의 경우, 굉장히 방대한 양의 Framework입니다. 구성에 대해서 많은 고민을 해보시고, 코드를 한번 더 보시면서 이게 어떤 의미인지 고민을 해보시길 바랍니다. 

한번 더 정리를 하도록 하겠습니다. Hibernate의 경우에는 SessionFactory를 이용한 Session을 통해 DB와 데이터 교환을 행하게 됩니다. SessionFactory는 DataSource를 한번 감싼 형태로, 기존 Jdbc와 비교를 한다면 DataSource와 동일한 위치에 있습니다. 그리고 Session은 jdbcTemplate과 유사한 기능을 행합니다. 이번 예제에서는 HibernateSqlExecutor를 통해서 Session에 query를 보내는 action을 행했습니다. 
그리고, 오늘 제가 보여드린 Hibernate는 Hibernate라는 빙산의 일각일 뿐입니다. xml을 통한 mapping, Set이 아닌 List를 통한 OneToMany mapping 등, 여러가지 내용들이 너무나도 많습니다. 이 부분에 대해서는 좀더 좀더 고민해보시고, 찾아서 공부를 하시는것이 좋습니다.  Hibernate를 통한 지금 DB의 구성 코드에 대해서 전체를 다 작성을 해보시고, 기존 코드와 비교를 해보세요. 이 부분이 진행이 정상적으로 되지 못하면 뒷부분 부터가 많이 힘들어집니다.



Posted by Y2K
,