주요글: 도커 시작하기
반응형
Hibernate에서 퍼스시턴트 객체를 이용한 CRUD 처리 및 퍼시스턴트 생명주기, 객체로딩전략, 영속성 전이에 대해서 살펴본다.

퍼시스턴트 객체의 영속성 생명주기

Hibernate의 퍼시스턴트 객체는 영속성과 관련해서 아래 그림과 같은 세 가지 상태를 갖는다.


위 그림에서 모서리가 둥근 건 퍼시스턴트 객체의 상태를 나타내며, 메소드는 Hibernate Session이 제공하는 메소드를 의미한다. 각 상태는 퍼시스턴트가 객체가 데이터베이스 테이블과 연결되었느냐에 따라 달라진다.

  • 비영속 상태 - 퍼시스턴트 객체를 처음 만들었을 때의 상태. 데이터베이스 테이블에 관련 데이터가 없으며, 연관된 Session이 없다.
  • 영속 상태 - 현재 활성화된 Session과 연결된 퍼시스턴트 객체. 이 상태의 퍼시스턴트 객체는 고유성을 가지며, 프로퍼티 값의 변경이 Session을 통해 자동으로 데이터베이스에 반영된다.
  • 준영속 상태 - 영속 상태의 퍼시스턴트 객체가 Session과 연결이 끊기면 준영속 상태가 된다. Hibernate의 관리를 받지는 않지만, 영속 데이터를 갖고 있다.
위 상태를 코드에서 표시하면 아래와 같다.


퍼시스턴트 객체가 고유성이 없는 경우(즉, 데이터베이스에 1대 1로 매핑되는 데이터가 존재하지 않는 경우) 비영속 상태에 해당한다. 예를 들어, 위 코드처럼 퍼시스턴트 객체를 새로 생성한 경우에 비영속 상태가 된다.

비영속 상태의 객체를 Session을 통해 저장하면(save() 또는 saveOrUpdate() 등을 사용) 퍼시스턴트 객체는 영속 상태가 된다. 영속 상태의 퍼시스턴트 객체는 활성화된 Session과 관련을 맺는다. 보통 영속 상태가 된다 하더라도 Session의 트랜잭션이 커밋되지 않으면 데이터베이스에 실제로 저장되지는 않는다.

일단, 퍼시스턴트 객체가 영속 상태가 되면 퍼시스턴트 객체의 값 변화는 자동으로 데이터베이스에서 반영된다. (이를 automatic dirty checking)이라 한다. Hibernate Session은 트랜잭션이 끝날 때 Session과 관련 있는 영속 상태 퍼시스턴트 객체의 프로퍼티가 변경되었는 지 확인한 후, 변경된 내용을 데이터베이스와 동기화한다.

Session이 종료되면 Session과 관련된 모든 영속 상태의 퍼스시턴트 객체는 준영속 상태가 된다. 준영속 상태의 퍼시스턴트 객체는 데이터베이스와 연결되어 있지 않기 때문에 프로퍼티 값을 변경하더라도 그 내역이 데이터베이스에 반영되지는 않는다. 준영속 상태의 퍼시스턴트 객체는 나중에 다시 다른 Session과 연결될 수 있으며, Session과 연결되면 다시 영속 상태로 바뀌게 된다. 다음의 코드는 준영속 상태로 된 객체가 다시 영속 상태로 변경되는 예를 보여주고 있다.

    Session session1 = sessions.openSession();
    ...
    Book book = (Book)session1.get(Book.class, new Integer(1)); // 영속 상태
    ...
    session1.close(); // 이 시점에서 book은 준영속 상태로 변경
    
    ...
    
    Session session2 = sessions.openSession();
    ...
    session2.update(book); // 이 시점에서 다시 영속 상태로 변경
    ...
    session2.close();

준영속 객체의 활용

준영속 객체는 Session과 연결되어 있지는 않지만 테이블의 값을 저장하고 있기 때문에, 데이터를 보여줄 때 유용하게 사용할 수 있다. 보통 MVC 패턴에 기반한 프로젝트인 경우 다음과 같은 방식으로 준영속 객체를 사용한다.

  • 컨트롤러는 Hibernate를 사용하여 데이터베이스로부터 데이터를 읽어온다.
  • 컨트롤러는 읽어온 퍼시스턴트 객체를 준영속 객체로 변환한다. (보통, Session을 종료)
  • 컨트롤러는 준영속 객체를 뷰에 전달한다.
  • 뷰는 준영속 객체를 사용해서 데이터를 출력한다.
뷰에서는 일반 자바빈 객체를 사용해서 데이터를 출력하듯이 준영속 객체를 사용해서 데이터를 출력하면 된다.

또한 준영속 객체는 다시 영속 객체로 전환될 수 있기 때문에, 여러 단계에 걸쳐서 데이터를 수정한 후 DB에 반영하는 방식에서도 잘 적용될 수 있다. 예를 들어, 웹 어플리케이션인 경우 세션에 준영속 객체를 저장한 뒤 여러번에 걸쳐서 객체의 데이터를 수정한다. 그리고 마지막 과정에서 세션에 저장했던 준영속 객체를 읽어와 영속 객체로 전환한 뒤 변경 결과를 반영하면 된다.

Session이 제공하는 CRUD 처리 메소드

Hibernate를 사용하는 가장 큰 이유는 SQL 쿼리를 작성하지 않고도 Hibernate가 제공하는 Session의 메소드를 사용해서 간단한 코드로 CRUD 작업을 처리할 수 있기 때문이다. Session은 CRUD 작업을 처리할 수 있는 메소드를 제공하는데, 이들 메소드는 다음과 같다.

  • save(Object obj) - 새로운 객체를 (테이블에) 저장한다.
  • save(Object obj, Serializable id) - 새로운 객체를 (테이블에) 저장한다. 이때 식별값으로 두번째 인자로 전달받은 객체를 사용한다.
  • Object get(Class type, Serializable id) - 지정한 타입의 객체를 읽어온다. 두번째 인자는 객체를 읽어올 때 사용할 식별자이다.
  • Object load(Class type, Serializable id) - 지정한 타입의 객체를 읽어온다. 두번째 인자는 객체를 읽어올 때 사용할 식별자이다.
  • load(Object obj, Serializable id) - obj의 타입과 관련된 데이터를 읽어와 obj에 저장한다. 두번째 인자는 객체를 읽어올 때 사용할 식별자이다.
  • update(Object obj) - 객체의 값을 변경한다.
  • update(Object obj, Serializable id) - 지정한 식별자에 해당하는 테이블 행을 객체가 저장한 값으로 변경한다.
  • saveOrUpdate(Object obj) - 새로운 객체인 경우 (테이블에) 저장하고, 이미 존재하는 객체인 경우 (테이블의) 값을 새로운 값으로 변경한다.
  • delete(Object obj) - 객체를 (테이블에서) 삭제한다.
객체를 읽어오는 메소드가 get과 load 두가지가 있는데, 이 두 메소드는 다음과 같은 차이가 있다.

  • get : 읽어올 객체가 없는 경우 null을 리턴하며, 프록시 객체를 리턴하지 않는다.
  • load : 읽어올 객체가 없는 경우 예외를 발생하며, 프록시 객체를 리턴할 수 있다.
load() 메소드는 읽어올 객체가 존재하지 않는 경우 예외를 발생하기 때문에, 존재해야 하는 데이터가 없는 경우를 예외상황으로 처리하고 싶을 때 load() 메소드를 사용하면 된다. 그렇지 않고 단지 객체가 존재하는지의 여부를 판단할 필요만 있는 거라면 get() 메소드를 사용해서 리턴된 값이 null인지의 여부를 검사하는 편이 낫다.

객체를 변경하는 방법은 크게 두가지가 있는데 하나는 Session의 트랜잭션 범위에서 영속 상태의 객체의 프로퍼티를 변경하는 것이며, 두번째 방법은 준영속 상태의 개체를 변경한 뒤 Session과 연관시키는 것이다. 다음은 두 방식의 차이점을 코드로 보여준다.

    Session session1 = sessions.openSession();
    Transaction tx1 = session1.beginTransaction();
    
    Item item = (Item)session1.get(Item.class, new Integer(6));
    item.setTitle(newTitle); // 영속 상태의 객체 프로퍼티 값 변경    
    tx1.commit(); // update() 메소드를 호출하지 않아도 영속 상태 객체의 변경 내용이 적용
    session1.close();
    
    item.setLowPrice(1000); // 준영속 상태의 객체 프로퍼티 값을 변경
    
    Session session2 = sessions.openSession();
    Transaction tx2 = session1.beginTransaction();
    
    session2.update(item); // 준영속 상태를 영속 상태로 변경
    
    tx2.commit();
    session2.close();

객체를 삭제할 때에는 다음과 같이 get()이나 load()로 읽어온 뒤 delete() 메소드를 호출하면 된다.

    Session session = sessions.openSession();
    Transaction tx = session1.beginTransaction();
    
    Item item = (Item)session.get(Item.class, new Integer(6));
    session.delete(item);
    
    tx.commit();
    session.close();

이외에 CRUD를 처리하는 방법으로는 iterate(), update(), delete() 메소드에 쿼리를 전달하는 방식도 있고, Criteria를 사용해서 조건에 따라 조회하는 방법도 있는데 이에 대한 내용은 본 글에서는 다루지 않으며, 시리즈가 진행되는 동안에 살펴볼 것이다.

객체 로딩 전략, 프록시와 외부조인

도메인 영역의 객체는 서로 연관되어 있으며, Hibernate는 이런 연관 관계를 알맞게 처리해준다. 예를 들어, 지난 번 글에서 예제로 사용했었던 Item과 Bid의 관계를 다시 살펴보자.

      <class name="Item" table="ITEM">
            ...
            <set name="bids" inverse="true" cascade="all">
                  <key column="ITEM_ID" />
                  <one-to-many class="Bid" />
            </set>
      </class>

      <class name="Bid" table="BID">
            ...
            
            <many-to-one
                         name="item"
                         column="ITEM_ID"
                         class="Item"
                         not-null="true" />
            ...
      </class>

위 매핑 설정에서 알 수 있듯이 하나의 Item은 여러개의 Bid를 갖고 있으며 별도의 설정을 하지 않는 이상 Item을 읽어오면 Item과 관련된 모든 Bid 객체를 읽어오게 된다. 하지만, 경우에 따라서는 Item 객체만 필요하고 Item과 관련된 Bid 객체는 필요하지 않은 경우가 있으며, 이 경우 아래와 같은 코드를 실행할 때 Bid는 읽어오지 않길 원할 것이다.

    Session session = ...;
    ..
    Item item = (Item)session.get(Item.class, id); // 관련 Bid는 읽어오지 않길 원할 때가 있다
    ..
    session.close();

반대로 Bid 객체만 필요하고 Bid와 관련된 Item 객체는 필요하지 않은 경우가 있을 수 있다. 이렇게 연관된 객체를 읽어오지 않고 싶은 경우가 있을 수 있고, 반대로 늘 관련 객체를 함께 읽어오길 원하는 경우도 있을 수 있다. 이렇게 상황에 따라 데이터를 조회하는 네 가지 방식을 사용할 수 있는데, 이 네 가지 방식은 다음과 같다.

  • 즉시 읽어오기 방식 - 연관된 객체를 모두 읽어온다. 만약 one-to-one으로 연관되었을 경우, 연관된 객체를 읽어오기 위해 데이터베이스 조회가 발생한다.
  • 실제 사용할 때 읽어오기(lazy) 방식 - 실제로 연관된 객체가 사용될 때, 데이터베이스 조회가 발생한다. 예를 들어, Bid 객체를 읽어왔을 때, Bid.getItem() 메소드가 호출될 때 Item 객체를 읽어오기 위해 데이터베이스 조회가 발생한다.
  • 조인 읽어오기(outer-join) 방식 - 외부 조인을 수행하는 SQL 쿼리를 사용하여 연관된 객체를 모두 읽어온다. 즉시 읽어오기 방식이 연관 객체를 읽어오기 위해 여러번의 데이터베이스 조회가 발생하는 것과 달리 한번의 조회만 발생한다.
Hibernate는 위의 접근 방식을 매핑 파일에 속성을 사용해서 설정할 수 있도록 하고 있는데, 본 글에서는 실제 사용할 때 읽어오는 lazy 방식과 조인해서 읽어오는 outer-join 방식을 살펴보겠다. (참고로, lazy 방식과 outer-join 방식 중 아무것도 사용하지 않으면 즉시 읽어오는 방식을 사용한다.)

lazy 방식(프록시)

실제로 사용할 때 연관 객체를 읽어오는 걸, lazy 방식이라고 한다. Hibernate는 lazy 방식으로 연관 객체를 읽어올 때 다음과 같이 프록시 객체를 사용한다.


lazy 방식으로 지정된 경우, Hibernate는 위 그림에서 볼 수 있듯이 연관된 객체를 곧바로 읽어오지 않는다. 대신 연관된 객체와 연결될 수 있는 프록시 객체를 생성한다. 연관된 객체가 필요할 때 실제 연관될 객체가 생성되서 프록시와 연결된다.

many-to-one 관계에서 lazy 방식을 적용하기 위해서는 다음과 같이 연관될 클래스에 대한 정보를 담고 있는 class 태그의 lazy 속성값을 true로 지정해주어야 한다. (Bid-Item의 관계가 many-to-one)

      <class name="Item" table="ITEM" lazy="true">
            ...
      </class>

      <class name="Bid" table="BID">
            ...
            
            <many-to-one
                         name="item"
                         column="ITEM_ID"
                         class="Item"
                         not-null="true" />
            ...
      </class>

위와 같이 lazy 속성을 true로 지정하게 되면 Hibernate는 Item 클래스를 위한 프록시 클래스를 자동으로 생성해서 프록시로 사용한다.

one-to-one 관계에서도 마찬가지로 lazy 속성을 사용해서 lazy 방식을 적용할 수 있다. 예를 들어, 아래와 같이 one-to-one 관계를 맺을 때, 양쪽 모두 lazy 방식으로 읽어오도록 설정할 수 있다.

      <class name="Item" table="ITEM" lazy="true">
            ...
            <one-to-one name="detail"
                        class="ItemDetail" 
                        cascade="save-update" />            
            ...
            
      </class>

      <class name="ItemDetail" table="ITEM_DETAIL" lazy="true">
            ...
            <one-to-one name="item"
                        class="Item"
                        constrained="true" />
      </class>

콜렉션을 읽어올 때에도 lazy 방식을 사용할 수 있다. 예를 들어, set 태그에 다음과 같이 lazy 속성을 추가함으로써 lazy 방식으로 콜렉션을 읽어오도록 명시할 수 있다.

    <class name="javacan.hibernate.test.Item" table="ITEM" .. >
        ...

        <set name="bids" inverse="true" cascade="all-delete-orphan" lazy="true">
            <key column="ITEM_ID" />
            <one-to-many class="javacan.hibernate.test.Bid" />
        </set>
    </class>

lazy 방식을 사용할 때 주의할 점은 객체가 영속 상태일 때에만, 즉 Session과 연결되어 있는 경우에만 lazy 방식으로 데이터를 읽어올 수 있다는 것이다. 예를 들어, 아래의 코드처럼 lazy 방식으로 읽어온 프로퍼티를 준영속 상태에서 읽어올 수 없으며, 이런 경우 예외가 발생하게 된다.

      Session session = sessions.openSession();
      Transaction tx = session.beginTransaction();
      
      // Item의 bids 프로퍼티와 관련 객체를 lazy 방식으로 로딩
      Item item = (Item)session.get(Item.class, someId); 
      
      tx.commit();
      session.close();
      
      // 준영속 상태에서 lazy 방식의 프로퍼티에 접근 -> 예외 발생
      Iterator iter = item.getBids().iterator();

따라서, lazy 방식으로 읽어온 프로퍼티를 준영속 상태에서 사용해야 하는 경우에는 사전에 미리 lazy 방식으로 읽어온 프로퍼티를 초기화해야 한다. 이럴 때 사용할 수 있는 메소드가 Hibernate.initialize()이다. 위와 같은 경우 다음과 같이 Session을 종료하기 전에 Hibernate.initialize() 메소드를 호출해주면 된다.

      Session session = sessions.openSession();
      Transaction tx = session.beginTransaction();
      
      // Item의 bids 프로퍼티와 관련 객체를 lazy 방식으로 로딩
      Item item = (Item)session.get(Item.class, someId); 
      Hibernate.initialize(item.getBids());      
      tx.commit();
      session.close();
      
      // 사전에 초기화 되었으므로 예외 발생안함
      Iterator iter = item.getBids().iter();

outer-join 속성을 사용한 연관 객체 로딩

lazy 방식은 연관된 객체가 실제로 사용되기 전까지 최대한 늦게 객체를 로딩하는 반면에 outer-join 방식은 최대한 빨리 연관된 객체를 로딩한다. outer-join 방식은 외부 조인을 사용해서 한번의 SQL 쿼리로 연관 객체와 관련된 모든 데이터를 읽어오므로 데이터베이스 조회 회수를 줄이면서 동시에 연관 객체를 읽어오게 된다.

outer-join 속성값을 사용해서 outer-join 방식을 설정할 수 있는데, outer-join 속성에 사용할 수 있는 값을 다음과 같이 세가지가 존재한다.

  • auto - 기본값으로서 프록시가 가능한 경우 lazy 방식으로 읽어오고, 그렇지 않은 경우 outer-join 방식으로 읽어온다.
  • true - 프록시가 가능한 경우에도, 항상 outer-join 방식으로 읽어온다.
  • false - 프로시를 사용하지 않더라도, 항상 outer-join 방식을 사용하지 않는다.
예를 들어, Bid 객체를 읽어올 때 many-to-one의 관계에 있는 Item을 항상 outer-join 방식으로 읽어오고 싶다면 다음과 같이 하면 된다.

      <many-to-one name="item" class="Item" outer-join="true">

영속성 전이

이제 영속성 전이에 대해서 살펴보자. 영속성 전이는 영속 상태의 객체와 연관된 비영속 및 준영속 상태의 객체에 자동으로 영속성이 전파되는 특징을 말한다. 예를 들어, 다음의 코드를 보자.

      Bid bid = new Bid();
      ... // bid 프로퍼티들의 값 설정
      
      Session session = sessions.openSession();
      Transaction tx = session.beginTransaction();
      
      Item item = (Item)session.get(Item.class, id);
      item.addBid(bid); // 영속 객체(item)에 비영속 객체(bid)를 연관
      
      tx.commit();
      session.close();

위 코드는 영속 객체인 item에 비영속 객체인 bid를 연관시키고 있다. 이때 Hibernate는 자동으로 비영속 상태인 bid 객체를 영속 상태로 전환하게 된다. (영속 상태로 전환된다는 것은 데이터베이스에 저장된다는 것을 의미한다.) 이렇게 영속 상태의 객체와 연관된 비영속/준영속 객체들이 자동으로 영속 상태가 되는 것을 영속성 전이라고 표현한다.

영속성 전이는 저장 뿐만 아니라 삭제인 경우에도 발생할 수 있다. 예를 들어, 하나의 경매 항목이 삭제되면 그와 관련된 입찰 정보도 삭제될 것이다. 이렇게 영속 상태의 객체가 비영속 상태로 변경될 때 연관된 영속 객체들도 함께 비영속 상태로 변경되는 것 역시 영속성 전이에 해당한다.

Hibernate는 영속성 전이를 연관 객체에 대해 어느 정도까지 적용할 지를 설정하기 위해 cascade 속성을 사용하며, cascade 속성은 다음과 같은 값들을 가질 수 있다.

  • none - 연관을 무시한다.
  • save-update - 트랜잭션이 커밋될 때, save()나 update() 메소드가 호출될 때, 새롭게 생성한 비영속 객체를 영속 객체에 연관할 때, 준영속 객체를 영속 상태로 변경할 때, 영속성 전이를 수행한다.
  • delete - 객체가 delete()에 전달될 때 영속성 전이를 수행해서 연관되어 있는 객체를 삭제한다.
  • all - save-update와 delete 옵션을 함께 수행한다. 또한, evict() 메소드나 lock() 메소드가 실행될 때에도 영속성 전이를 수행한다.
  • all-delete-orphan - all과 같은 기능을 수행한다. 더불어, 연관에서 제거된 퍼시스턴트 객체를 삭제한다. 콜렉션과 관련된 영속성 전이에 사용된다.
예를 들기 위해 먼저 다음의 설정을 보자.

      <class name="javacan.hibernate.test.Item" table="ITEM" lazy="true">
            ...
            <set name="bids" inverse="true" cascade="save-update" >
                  <key column="ITEM_ID" />
                  <one-to-many class="javacan.hibernate.test.Bid" />
            </set>
      </class>
      
      <class name="javacan.hibernate.test.Bid" table="BID">
            
            <many-to-one name="item"
                         column="ITEM_ID"
                         class="javacan.hibernate.test.Item"
                         not-null="false"  />
      </class>
      

위 설정 코드는 Item-Bid를 one-to-many 관계로 하고 있고, Item->Bid로의 영속성 전이를 save-update로 설정하였다. save-update는 영속 객체와 연관된 객체에 영속성이 전이되므로 아래 코드와 같이 새로운 Bid 객체를 생성해서 연관한 경우 새롭게 생성한 Bid 객체가 DB에 저장된다.

      Session session = ...;
      Transaction tx = session.beginTransaction();
      
      Bid bid = new Bid();
      Item item = (Item)session.get(Item.class, id);
      item.addBid(bid);
      
      tx.commit();
      session.close();

하지만, 설정 파일의 set 태그에서 cascade 속성을 none으로 하게 되면 영속성 전이가 수행되지 않으므로, 위 코드에서 새롭게 생성한 Bid 객체는 데이터베이스에 저장되지 않는다.

cascade 속성값이 delete인 경우는 연관된 객체만 삭제한다. 예를 들어, 아래의 코드를 살펴보자.

      Session session1 = session.openSession();
      ...
      Item item1 = (Item)session1.get(Item.class, id);
      session1.delete(item1); // item1과 연관된 ItemDetail도 삭제
      ...
      session1.close();
      
      Session session2 = session.openSession();
      ...
      Item item2 = (Item)session2.get(Item.class, id);
      item2.setDetail(null); // item1과 연관됐었던 ItemDetail을 연관시키지 않음
      session2.delete(item2); // item1만 삭제되고, 연관됐던 ItemDetail은 삭제되지 않음
      ...
      session2.close();

cascade 속성값이 delete인 경우에는 첫번째와 같이 연관되어 있는 객체는 함께 삭제되나, 두번째와 같이 연관됐었다가 연관되지 않게된 객체의 경우는 삭제되지 않는다.

delete와 달리 all-delete-orphan은 연관됐었던 객체를 연관에서 해제하더라도 삭제된다. 예를 들어, Item과 Bid의 관계를 아래와 같이 설정했다고 해 보자. 아래 코드는 Item-Bid의 one-to-many 관계에서 cascade 속성을 all-delete-orphan으로 설정하고 있다.

      <class name="javacan.hibernate.test.Item" table="ITEM" lazy="true">
            ...
            <set name="bids" inverse="true" cascade="all-delete-orphan" >
                  <key column="ITEM_ID" />
                  <one-to-many class="javacan.hibernate.test.Bid" />
            </set>
      </class>
      
      <class name="javacan.hibernate.test.Bid" table="BID">
            
            <many-to-one name="item"
                         column="ITEM_ID"
                         class="javacan.hibernate.test.Item"
                         not-null="false"  />
      </class>
      

이 상태에서 다음과 같은 코드를 실행했다고 해 보자.

      Session session = HibernateUtil.currentSession();
      tx = session.beginTransaction();
      
      Item item = (Item)session.get(Item.class, new Integer(2));
      item.getBids().clear(); // Set에 있는 모든 객체를 비움
      
      tx.commit();

위 코드는 Item의 bids 프로퍼티인 Set 객체에 저장되어 있던 객체 자체를 삭제하는 게 아니라 단지 Set 객체를 비운 것에 불과하다. 하지만, cascade 속성 값이 all-delete-orphan 이기 때문에, Set에 저장되어 있는 Bid 객체와 매핑된 테이블 데이터도 함께 제거된다.

다음 글에서는

본 글의 예제 코드에서 set 태그를 사용했었는데, Hibernate는 set 태그 이외에 list, map과 같은 다양한 타입의 콜렉션을 지원하고 있다.다음 글에서는 Hibernate가 제공하는 콜렉션에는 어떤 것들이 있으며 어떻게 사용하는 지에 대해서 살펴볼 것이다. 또한 살펴보지 않았던 many-to-many 관계를 Hibernate에서 어떻게 적용할 수 있는 지에 대해서도 살펴볼 것이다.

관련링크:

+ Recent posts