주요글: 도커 시작하기
반응형
Hibernate의 OR 매핑에서 사용될 자바 객체에 대해서 살펴본다.

퍼시스턴트 클래스(Persistent Class) 작성 규칙

도메인 영역의 개체(entity)를 표현하는 데 가장 많이 사용되는 자바 클래스는 자바빈의 형태를 취한다. 대부분의 도메인 영역의 개체는 테이터베이스의 테이블에 지속적으로 저장되며 이렇게 지속적으로 저장되는 데이터를 표현하기 위해 사용되는 클래스를 퍼시스턴트 클래스(Persistent Class)라고 부른다. (영속적 클래스, 지속성 클래스 등의 해석한 용어가 어색해서 그냥 퍼시스턴트 클래스라고 부르겠다.)

퍼시스턴트 클래스를 Hibernate에서 사용가능하게 하려면 다음과 같은 규칙을 따라야 한다.

  • 퍼시스턴트 필드에 대한 접근/수정 메소드를 제공해야 한다.
  • 기본 생성자를 제공해야 한다.
  • ID 프로퍼티를 제공해야 한다. (선택사항)
  • non-final 클래스로 작성하는 것이 좋다. (선택사항)
퍼시스턴트 필드에 대한 접근/수정 메소드

퍼시스턴트 클래스는 DB 테이블에서 읽어온 값을 저장하기 위한 멤버 필드가 정의되어 있다. Hibernate는 퍼시스턴트 클래스의 멤버 필드에 직접 접근하기 보다는, 자바빈 스타일의 메소드를 통해서 멤버 필드에 간접적으로 접근한다. 즉, 자바빈 프로퍼티를 사용해서 DB 테이블의 값을 퍼시스턴트 클래스에 저장해준다.

자바빈 프로퍼티의 명명규칙을 따르기 때문에 set/get/is를 메소드 이름에 붙여서 사용하면 된다. 예를 들어, getId, setId, getName, setName 등의 이름을 사용한다.

기본 생성자를 제공해야 한다.

Hibernate는 Constructor.newInstance()를 사용해서 퍼시스턴트 클래스의 인스턴스를 생성하기 때문에, 인자가 없는 기본 생성자를 제공해야 한다. 예를 들어, 다음과 같이 기본 생성자를 제공하지 않는 클래스는 Hibernate에서 사용할 수 없다.

public class InvalidPersistentClass {

    private String name;
    ...
    
    /* 기본 생성자가 없음!! 아래와 같은 기본 생성자를 추가해주어야 Hibernate에서 사용가능 public InvalidPersistentClass() { } /*    
    public InvalidPersistentClass(String name) {
        ...
    }
    
    public String getName() {
        return name;
    }
    ...
}

자바에서는 어떤 생성자도 제공하지 않으면 기본 생성자를 자동으로 추가해준다. 따라서, 위 코드와 같이 생성자를 포함한 클래스는 기본 생성자를 추가해주어야 Hibernate에 적용할 수 있지만, 아래와 같이 어떤 생성자도 포함하지 않으면 기본 생성자가 자동으로 추가되므로 Hibernate에서 사용할 수 있다.

public class Product {
    private String id;
    private String name;
    
    public String getId() {
        return id;
    }
    public void setId(String id) {
        this.id = id;
    }
    ...
}

식별자 프로퍼티를 제공해야 한다. (선택사항)

퍼시스턴트 클래스는 DB 테이블의 Primary Key 필드와 매핑되는 프로퍼티를 제공하는 것이 좋다. 이 프로퍼티의 타입은 기본 데이터 타입일 수도 있고, Integer나 Long과 같은 래퍼 타입일 수도 있고, String이나 Date 같은 일반 자바 객체일 수도 있다. (필드가 여러개로 구성된 복합 키이거나 사용자 정의 타입을 사용할 수도 있는데 이에 대한 대한 내용은 본 시리즈의 4회 글인 OR 매핑 부분에서 살펴볼 것이다.)

식별자 프로퍼티는 선택사항이다. 식별자 프로퍼티를 정의하지 않고, Hibernate가 내부적으로 객체의 ID를 추적하도록 할 수 있다. 하지만, 가급적이면 식별자 프로퍼티를 제공하는 것이 퍼시스턴트 클래스의 좋은 설계 방식이다.

식별자 프로퍼티의 경우 기본 데이터 타입보다는 클래스 타입을 사용하는 것이 좋다. 예를 들어, PK가 정수타입인 경우 다음과 같이 두 가지 형태로 식별자 프로퍼티를 정의할 수 있을 것이다.

public class Some {
    private int id;
    
    public int getId() {
        return id;
    }
    ...
}

public class Any {
    private Integer id;
    
    public Integer getId() {
        return id;
    }
    ...
}

여기서 Some은 값을 DB 테이블에 삽입하기 위해 새로운 객체를 생성하면 자동으로 id 프로퍼티에 0이라는 값이 할당된다. (int 타입의 기본값은 0이다.) 따라서, id 프로퍼티의 값을 올바르게 할당하지 않으면 0 값이 DB 테이블에 삽입될 수도 있다. 하지만, Any의 경우는 새로운 객체를 생성하더라도 id 프로퍼티의 값은 null이다. PK 필드에는 null값이 들어갈 수 없으므로, 실수로 id 프로퍼티의 값을 할당하지 않을 경우 잘못된 값이 DB 테이블에 삽입되는 것이 아니라 에러가 발생하게 된다. 따라서, 식별자 프로퍼티의 경우는 오류 방지를 위해 null 값을 가질 수 있는 타입으로 지정하는 것이 좋다.

non-final 클래스로 작성하는 것이 좋다. (선택사항)

Hibernate는 프록시 기능을 제공하는데, 이 기능을 사용하려면 퍼시스턴트 클래스가 final 클래스가 아니거나, public 메소드를 선언한 인터페이스를 구현해야 한다. final 클래스를 Hibernate에 적용할 수는 있지만, public 메소드를 선언한 인터페이스를 구현하지 않은 final 클래스의 경우 프록시 기능을 사용할 수 없게 된다.

equals()/hashCode() 메소드 오버라이딩 & 라이프사이클 콜백

equals() 메소드와 hashCode() 메소드의 구현

만약 퍼시스턴트 클래스들을 객체들이 섞인다면 equals() 메소드와 hashCode() 메소드를 알맞게 오버라이딩 해 주는 것이 좋다. (예를 들어, 퍼시스턴트 클래스 객체를 Set이나 Map 같은 곳에 저장할 경우)

테이블의 같은 레코드에 매핑된 두 객체가 서로 다른 Session에서 로딩된 경우를 생각해보자. 비록 두 객체가 갖는 값은 같더라도, 기본 equals() 메소드는 == 연산자를 사용해서 두 객체를 비교하기 때문에 두 객체는 서로 다른 객체로 판단된다. 따라서, 값이 같을 때 equals() 메소드가 true를 리턴하도록 하려면 equals() 메소드를 알맞게 구현해주어야 한다.

equals() 메소드를 구현하는 가장 확실한 방법은 객체의 각각의 프로퍼티가 동일한지 비교하는 것이다. 하지만, DB 테이블이 Primary Key 필드(또는 Unique 필드)로 각각의 레코드를 구분하므로, Primary Key 필드(또는 Unique 필드)에 해당하는 프로퍼티만 비교하면 효율적으로 비교를 할 수 있다.

라이프사이클 콜백: Lifecycle 인터페이스

새로운 객체를 DB 테이블에 저장할 때, DB 테이블로부터 데이터를 읽어와 객체에 저장할 때, 값을 변경할 때, DB 테이블에서 데이터를 삭제할 때 별도의 처리가 필요할 때가 있을 것이다. (예를 들어, 객체를 삭제하기 전에 객체에 의존하고 있는 또 다른 객체가 존재할 경우 객체 삭제가 안 되도록 하고 싶은 경우가 있다.)

이렇게 CRUD 작업이 처리될 때 값을 검증한다거나 별도의 처리를 하고 싶을 경우, 퍼시스턴트 클래스는 net.sf.hibernate.Lifecycle 인터페이스를 구현해주어야 한다. Lifecycle 인터페이스는 아래와 같이 네 개의 메소드를 선언하고 있다.

public interface Lifycycle {
    public boolean onSave(Session s) throws CallbackException;
    public boolean onUpdate(Session s) throws CallbackException;
    public boolean onDelete(Session s) throws CallbackException;
    public void onLoad(Session s, Serializable id) throws CallbackException;
}

각 메소드는 아래와 같은 기능을 제공한다.

  • onSave - 객체가 저장되기(삽입되기) 전에 호출된다.
  • onUpdate - 객체의 수정 결과가 테이블에 반영되기 전에(즉, Session.update() 메소드에 객체가 전달될 때) 호출된다.
  • onDelete - 객체가 삭제되기 전에 호출된다.
  • onLoad - 객체를 로딩한 이후에 호출된다.
onSave, onUpdate, onDelete 메소드는 boolean 타입을 리턴하는데, 리턴값에 따라서 다음과 같이 처리된다.

  • true - 작업을 거부한다. 예를 들어, onSave() 메소드가 true를 리턴하면 값이 저장되지 않는다.
  • false - 작업을 수행한다. 예를 들어, onSave() 메소드가 true를 리턴하면 값이 저장된다.
예를 들어, 아래와 같이 퍼시스턴트 클래스를 작성할 수 있을 것이다.

public class Product implements Lifecycle {
    ...
    private int stock; // 재고 개수
    
    ...
    
    public boolean onSave(Session s) throws CallbackException {
        if (stock < 0) return true;
        return false;
    }
    public boolean onUpdate(Session s) throws CallbackException {
        return false;
    }
    public boolean onDelete(Session s) throws CallbackException {
        // 재고가 남아 있는 경우 삭제 불가.
        if (stock > 0) throw CallbackException("stock > 0:" + stock);
    }
    public void onLoad(Session s, Serializable id) throws CallbackException {
    }
}

onSave, onUpdate, onDelete 메소드가 true를 리턴할 경우, 작업을 거부하기 때문에 DB 테이블에 객체값이 반영되지 않는다. 하지만, Hibernate를 사용하는 어플리케이션 코드에서는 DB 테이블에 반영되지 않은 사실을 알지 못한다.

    // Cat이 Lifecycle 인터페이스를 구현했다고 가정
    Product product = ...;
    
    tx = hbSession.beginTransaction();
    hbSession.save(product); // Product.onSave() 메소드가 true를 리턴하더라도 예외는 발생하지 않음
    tx.commit();

만약 Lifecycle 메소드 처리시에 CRUD 작업이 처리되지 않았다는 사실을 어플리케이션 코드가 알아야 한다면 true를 리턴하지 말고 CallbackException을 발생시켜야 한다. 앞서 예로 보여준 Product 클래스의 onDelete() 메소드는 stock 필드 값이 0보다 큰 경우 CallbackException 예외를 발생시키며, 이 경우 아래와 같이 어플리케이션에서 try-catch 블럭을 사용해서 CRUD 작업이 수행되지 않았음을 확인할 수 있게 된다.

    Product product = ...;
    tx = hbSession.beginTransaction();
    try {
        hbSession.delete(product); 
    } catch(CallbackException ex) {
        // onDelete()에서 예외 발생
    }
    tx.commit();

다음 글에서는

본 장에서는 퍼시스턴트 클래스를 작성하는 방법을 살펴봤는데, 퍼시스턴트 클래스의 목적은 DB 테이블과 매핑되어 테이블의 각 레코드를 메모리상에서 표현하는 것이다. 따라서, 퍼시스턴트 클래스와 DB 테이블이 어떻게 서로 연결되는 지에 대한 정보를 표시할 방법이 필요하며 이 방법에 대해서는 다음 글에서 살펴보도록 하겠다.

+ Recent posts