Hibernate Service

Hibernate는 객체 모델링(Object Oriented Modeling)과 관계형 데이터 모델링(Relational Data Modeling) 사이의 불일치를 해결해 주는 ORM 도구로, EJB의 Entity Bean과 같이 특정 플랫폼에 의존적인 제약을 정의하고 있지 않기 때문에 POJO 기반 개발이 가능하다. 또한 Java에서 지원하는 다양한 Collection 유형을 지원함으로써 객체 모델링을 관계형 모델링으로 매핑하는데 따르는 제약을 최소화하고 있다.
Hibernate의 특징을 살펴보면, 다음과 같다.
  • Hibernate 기반 개발시 특정 DBMS에 영향을 받지 않으므로 DBMS가 변경되더라도 데이터 액세스 처리 코드에 대한 변경없이 설정 정보의 변경만으로도 올바르게 동작 가능하다.
  • SQL을 작성하고 SQL 실행 결과로부터 전달하고자 하는 객체로 변경하는 코드를 작성하는 시간이 줄어들기 때문에 개발자는 비즈니스 로직에 집중할 수 있게 되고, 개발 시간이 단축될 수 있다.
  • JDBC Api를 사용한 코드의 양이 줄어들고, 매핑 파일을 별도로 관리하게 되면서 DB 정보 변경으로 인해 영향을 받는 부분 또한 감소한다.
  • 다음과 같은 접근 방법을 취함으로써, DBMS에 대한 접근 횟수를 줄여나가 궁극적으로 어플리케이션의 성능 향상을 도모한다.
    • 기본적으로 필요 시점에만 DBMS에 접근하는 Lazy Loading 전략 채택
    • Session 종료 시에 변경 사항에 대해 일괄 batch 처리
    • 1st Level Cache, 2nd Level Cache를 활용하여 DBMS에 대한 재접근없이 Caching된 객체 사용
  • 대부분의 개발자가 어플리케이션의 데이터 액세스 로직을 개발하기 위해 DTO(Data Transfer Object), DAO 패턴을 사용하는데 익숙하기 때문에 데이터와 로직을 가진 객체를 설계하는데 익숙하지 못하다는 단점을 가지고있다.
본 페이지에서는 Hibernate 3.2.6.ga 버전을 이용하여 Hibernate 기본 개념에 대해서 살펴볼 것이다. 먼저 어플리케이션 실행 여부와 상관없이 물리적으로 존재하는 데이터들을 정의하고 있는 Persistent Class와 Persistent Class의 Lifecycle에 대해 알아보고 이러한 객체들의 영속성을 관리하는 Hibernate Session에 대해 정리해 보고자 한다.

Conceptual Architecture

Hibernate 기본 구성은 다음 그림과 같다.

위 그림에서와 같이 Hibernate이 DBMS 기반의 어플리케이션 수행을 하기 위해 필요한 주요 구성 요소는 Persistent Objects, Hibernate Properties, XML Mapping 이며, 각각은 다음과 같은 역할을 수행한다.
  • Persistent Objects : Persistent Object는 어플리케이션 실행 여부와 상관없이 물리적으로 존재하는 데이터들을 다룬다. 일반적으로 DBMS 데이터를 이용하는 어플리케이션을 개발할 경우 어플리케이션의 비즈니스 레이어에서 특정 DBMS에 맞는 SQL을 통해 어플리케이션의 데이터를 처리하게 된다. 그러나 Hibernate 기반의 어플리케이션에서는 Persistent Object를 중심으로 하여 어플리케이션의 데이터와 DBMS 연동이 가능해진다.
  • Hibernate Properties : Hibernate 실행에 관련된 속성 정보를 포함하고 있는 파일로, hibernate.cfg.xml 또는 hibernate.properties 형태로 정의할 수 있다. 주로 DBMS, Logging, Cache, Mapping File 정보 등을 다루고 있다.
  • XML Mapping : Persistent Object과 특정 테이블 사이의 다양한 매핑 정보를 명시하기 위한 XML 파일이다. Hibernate는 Hibernate Mapping XML을 기반으로 하여 실행할 SQL을 생성하고 있다.

Persistent Classes

Persistent Class는 Hibernate를 이용하여 DB의 특정 테이블과 매핑되는 객체로 Hibernate를 제대로 사용하기 위해서는 Persistence Class작성이 중요하다. Java Class를 Hibernate의 Persistent Class로 사용하기 위한 기본 요건은 다음과 같다.
  • [필수] Default Constructor 제공 : Hibernate에서는 Default Constructor를 이용하여 해당 클래스의 인스턴스를 생성하므로 Persistence Object로 사용하기 위해서는 해당 클래스 내에 입력 인자를 갖지 않은 Default Constructor를 생성해야 함에 유의하도록 한다.
  • public class Category implements java.io.Serializable {
    <!-- 중략  -->
    	public Category() {
    	}
    }
  • [권장] 테이블의 Primary Key 칼럼과 매핑 되는 identifier field 정의 : 일반적인 경우 Hibernate Persistent Class에 DB 테이블의 primary key와 매핑되는 identifier field가 반드시 존재해야 할 필요는 없지만 몇 가지 경우에 반드시 필요하다. (예 : Session.saveOrUpdate 메소드 이용 시) 하지만 일반적인 Domain Object에서 identifier를 갖는 것이 일반적이므로 Persistent Class에 identifier field 정의하는 것을 추천한다.
  • [권장] final 클래스로 정의하지 않음 : Hibernate의 lazy loading은 proxy를 사용해야 하는데 final로 Persistent class를 선언할 경우 proxy를 사용 할 수 없다.
  • [권장] 속성 정보 접근을 위한 getter, setter 정의 : Hibernate는 getter/setter로 구성된 method가 존재할 경우 매핑 처리를 할 수 있다.
  • public class Category implements java.io.Serializable {
    
    	private String categoryId;
    	private String categoryName;
    	
    	<!-- 중략 -->
    	public String getCategoryId() {
    		return this.categoryId;
    	}
    
    	public void setCategoryId(String categoryId) {
    		this.categoryId = categoryId;
    	}
    
    	public String getCategoryName() {
    		return this.categoryName;
    	}
    
    	public void setCategoryName(String categoryName) {
    		this.categoryName = categoryName;
    	}
    	<!-- 중략 -->
    }
    만약 Mapping File에 DB컬럼에 대한 매핑 정보를 아래와 같이 설정 한다면 gettter/setter메소드가 필요없다.
    <property name="name" column="NAME" access="field"/>
  • [선택] equals(), hashCode() 메소드 구현 : 다음에서 동일한 테이블의 동일한 행의 데이터를 읽어온 category1과 category2는 다른 Session을 통해 얻어졌으므로 동일한 객체가 아닌 것으로 처리된다. 이처럼 다른 Session을 통해 얻어 온 객체의 동일함을 비교할 필요가 있을 경우에는 equals() 메소드를 적절히 구현해 주도록 한다.
  • Session session1 = SessionFactory.openSession();
    Category category1 = (Category)session1.get(Category.class, “test”);
    session1.close();
    
    Session session2 = SessionFactory.openSession();
    Category category2 = (Category)session2.get(Category.class, “test”);
    session2.close();
  • [선택] Serializable 인터페이스 구현 : Hibernate에서 persistent class들이 java.io.Serializable 인터페이스를 반드시 implement 해야 하는 것은 아니지만, persistent object가 HttpSession에 저장되거나 RMI를 이용해서 전달할 때는 반드시 필요한다.

Persistent Object States

Lifecycle

Hibernate에서 Persistent Object는 Transient, Persistent, Detached의 3가지 상태를 가질 수 있으며, 상태 변이를 발생시킨다. 각 상태에 관한 내용은 다음과 같다.
  • Transient : 객체는 생성되었으나 아직 Hibernate에 의해 관리되지 않는 경우
  • <!-- 중략 -->
    Category category = new Category();
    
    category.setCategoryId("CTGR-0001");
    category.setCategoryName("Romantic");
    category.setCategoryDesc("Romantic genre");
    <!-- 중략 -->
    
    위의 소스에서 처럼 persistent class의 인스턴스는 생성되었지만 Hibernate에 의해서 아직 관리되지 않은 상태를 Transient 상태라고 한다.
  • Persistent : 테이블의 특정 행과 매핑되며 변경 값 자동 반영, Lazy Loading 등 Hibernate에서 제공하는 기능이 적용되는 경우
  • session.save(category);
    Transient상태의 persistent class를 Hibernate의 Session의 save(),persist()와 같은 method를 이용하면 Hibernate에서 제공하는 기능을 사용할 수 있는 상태인 Persistent 상태가 된다.

    아래는 Persistent 상태인 Object의 값을 변경 했을 때 자동으로 DB에 변경된 내용이 반영 되는 것을 테스트 하기 위한 HibernatePersistentObjcetStatesTest.java 의 일부분이다.
    public void testPersistentState() throws Exception {
        newSession();
           
        Category category = new Category();
        category.setCategoryId("CTGR-0001");
        category.setCategoryName("Romantic");
        category.setCategoryDesc("Romantic genre");
           
        session.save(category);
         
        category.setCategoryName("Comedy");
        category.setCategoryDesc("Comedy consists...");
           
        closeSession();
    
    }
    
    다음은 위 테스트 코드를 실행 시켰을 때 query log의 일부분이다.
    1:  insert into PUBLIC.CATEGORY (CATEGORY_NAME, CATEGORY_DESC, CATEGORY_ID) values ('Romantic', 
    'Romantic genre', 'CTGR-0001') 
    1:  update PUBLIC.CATEGORY set CATEGORY_NAME='Comedy', CATEGORY_DESC='Comedy consists' 
    where CATEGORY_ID='CTGR-0001'
    
    위 로그에서 알 수 있듯이 category의 값을 변경한 후 save나 update명령을 실행하지 않았음에도 불구하고 transaction이 종료 됐을 때 update query가 실행된다. 이처럼 persistent 상태가 되면 DB 테이블의 특정 행과 매핑되어 값이 자동으로 반영된다.
  • Detached : 변경 값이 있더라도 DB에 반영되지 않으며 Lazy Loading 등의 Hibernate 제공 기능 적용 대상에서 제외되는 경우

    아래는 Detached 상태가 된 persistent object의 값을 변경시키고 Session을 닫았을 때 DB에 반영이 안되는 경우를 테스트 하는 HibernatePersistentObjcetStatesTest.java 의 일부분이다.
  • public void testDetachedState() throws Exception {
    	newSession();
    	
    	Category category = new Category();
    	category.setCategoryId("CTGR-0001");
    	category.setCategoryName("Romantic");
    	category.setCategoryDesc("Romantic genre");
    	
    	session.save(category);
    	
    	closeSession();
    	
    	newSession();
    	
    	category.setCategoryName("Comedy");
    	category.setCategoryDesc("Comedy consists");
    	closeSession();
    
    위 테스트 케이스에서 보면 closeSession()을 해서 persistent object의 state를 detached 상태로 만든 후에 값을 변경하고 다음 Session을 close시키면 persistent state때와는 달리 update가 되지 않는다. 실행되는 query 로그는 다음과 같다.
    insert into PUBLIC.CATEGORY (CATEGORY_NAME, CATEGORY_DESC, CATEGORY_ID) values ('Romantic', 
    'Romantic genre', 'CTGR-0001') 
    
    위 query 로그에서 보듯이 persistent state상태일 때와는 달리 persistent object가 변경 되었음에도 update query가 실행 되지 않는다.

    참고 : 한 Session 내에서 Initialize되지 않은 객체는 해당 Session 종료로 인해 Detached 상태로 변경되었을 때에는 객체 정보를 읽을 수 없다. 아래는 Session이 종료 되서 Detached상태로 된 객체에서 initialize되지 않은 연관 객체의 정보를 읽을 때 LazyInitializationException발생하는 것을 확인 하는 테스트 코드 HibernateLazyInitializationExceptionTest.java 의 일부분이다.
    public void testFindMovie() throws Exception {
    <!-- 중략 -->
    	Movie movie = (Movie) session.get(Movie.class, "MV-00001");
    <!-- 중략 -->
    	session.close();
    			
    try {
    	movie.getCategories().iterator();
    	fail("expected LazyInitializationException");
    } catch (Exception e) {
    	assertTrue("fail to throw LazyInitializationException.",
    	e instanceof LazyInitializationException);
    }
    Movie와 Category는 1:n 관계를 갖고 있다. MOVIE 테이블을 대상으로 단건 조회 작업을 수행한 후, Session을 종료하여 Movie Object를 Detached 상태로 만든다. 그리고 initialize되지 않은 Category 목록 정보를 얻으려 할 때 LazyInitializationException이 발생한다.

Hibernate Session

Hibernate Session은 하나의 DB Connection과 연결되어 있다. 그러나 Session 생성시 바로 Connection이 해당 Session과 연결되는 것은 아니며 필요시에 Connection을 얻어낸다. 위 Persistent Object State에서 본 테스트 코드HibernateLazyInitializationExceptionTest.java 에서 보면,
session = initialSessionFactory.openSession(); -- 1
session.beginTransaction();                    -- 2 
				
1은 SessionFactory에서 Session을 생성하는 코드고 2번은 Sesstion에서 Transaction이 시작되는 코드이다. 이 같은 코드를 실행해 보면 Connection이 2번에서 연결되는 것을 확인 할 수 있다.

또한, JDBC에서 동일 트랜잭션 범위에서 실행되는 SQL은 하나로 묶음 처리된다면 Hibernate에서는 동일 Session에서의 작업을 하나의 묶음으로 처리한다. 즉, Hibernate에서 Session.save(), Session.update()와 같은 메소드가 호출 될 때 Query가 실행되는 것이 아니라 Transaction이 commit됐을 때 실행된다. 위 Persistent Object State에서 본 테스트 코드HibernatePersistentObjcetStatesTest.java 에서 보면,
newSession();
   
Category category = new Category();
category.setCategoryId("CTGR-0001");
category.setCategoryName("Romantic");
category.setCategoryDesc("Romantic genre");

session.save(category);

closeSession();
session.save()가 호출됐을 때 query 로그를 보면 어떤 query도 실행되지 않는 것을 확인 할 수 있다. closeSession()의 호출로 Transaction이 종료됐을 때 query가 실행된다.

따라서 Hibernate를 이용하여 데이터 접근 처리를 수행하기 위해서는 Session이 필요하다. Hibernate의 SessionFactory는 다음 두가지 메소드를 이용하여, Session을 제공한다.
  • openSession
  • SessionFactory.openSession()을 호출하면 호출할 때마다 새로운 Session을 생성한다.
    Session session1 = SessionFactory.openSession();
    Session session2 = SessionFactory.openSession();
    위 소스에서 생성된 session1과 session2는 서로 다른 session이다.
  • getCurrentSession
  • SessionFactory.getCurrentSession()은 같은 Transaction 범위에 있을 경우 매번 같은 session을 리턴한다.
    Session session1 = SessionFactory.getCurrentSession();
    session1.beginTransaction();
    Session session2 = SessionFactory.getCurrentSession();
    <!-- 중략 -->	
    session1과 session2는 같은 Transaction으로 묶여 있기 때문에 같은 Session이다.
openSession(), getCurrentSession()d을 호출해서 얻어온 Session에는 차이가 있다. openSession()으로 생성된 session은 트랜잭션이 종료되더라도 종료되지 않는 반면, getCurrentSession()으로 얻어진 Session은 트랜잭션 종료시 Session도 함께 종료된다. 따라서 openSession()으로 얻어진 Session에 대해서는 session.close()를 호출해서 Session을 종료해야 하며, getCurrentSession()으로 얻어진 Session에 대해서는 따로 Session.close() 메소드를 호출하지 않도록 한다.

Resources

  • 다운로드
  • 샘플 테스트 코드를 포함하고 있는 anyframe-hibernatetest-src.zip 파일을 다운받은 후, 테스트 환경 설정 을 참조하여 위에서 제시한 예제 코드 (src/test/java 폴더의 anyframe.core.hibernate.basic 패키지와 anyframe.core.hibernate.fetch 패키지에 속한 일부 *Test.java)를 실행해 볼 수 있다.
    Name
    Download
    anyframe-hibernatetest-src.zip
    Download