저자: 닉 휴데커(Nick Heudecker), 역 김진회
데이터베이스로부터 객체를 얻어오거나, 저장하는 퍼시스턴스 계층을 만들거나, 유지 보수하는 작업에 엔터프라이즈 애플리케이션 개발의 상당 부분이 소요된다. 만약 데이터베이스의 스키마가 바뀌는 경우에는 애플리케이션 나머지 부분도 크게 변경해야 한다. 하이버네이트(Hibernate)는 자바 애플리케이션에 적합하기 때문에 사용하기 쉽고, 강력한 객체 관련 퍼시스턴스(Object-Relational Persistence) 프레임워크를 제공한다. 따라서 퍼시스턴스 계층과 나머지 계층 간의 독립성을 유지하도록 해준다.
하이버네이트는 복합 타입은 물론, 컬렉션(Collection)과 객체(Object) 간의 관계에 대한 지원을 한다. 객체에 대한 퍼시스턴스 제공과 동시에, 효율적인 캐싱 계층과 JMX(Java Manage Extensions)를 지원하고, 데이터베이스로부터 객체를 얻어올 수 있는 퀴리 언어까지 제공한다. 사용자 정의 데이터 타입과 동적인 빈(Bean) 객체 역시 지원한다.
하이버네이트는 Lesser GNU Public License 하에 출시되고 있다. 따라서 상업용 애플리케이션은 물론이고 오픈 소스 애플리케이션에도 충분히 사용할 수 있다. 하이버네이트는 오라클, DB2, PostgreSQL, MySQL 등을 비롯한 수많은 데이터베이스를 지원한다. 또한, 하이버네이트를 확장하기 위한 지원 및 툴을 제공, 좀더 쉽게 사용할 수 있는 방법 등은 활발하게 운영되고 있는 사용자 커뮤니티를 통해 도움을 받을 수 있다.
본 기사가 작성된 시점인 2003.6.17 현재 Hibernate 2.0.1가 출시되었다.
(
†역자 주: 2003.11.5부로 Hibernate 2.1 beta 6 released가 출시되었다.)
작동 방법(How Hibernate Works)
바이트 코드 프로세싱이나 코드 생성이 아니라, 하이버네이트는 클래스의 퍼시스턴스 프로퍼티를 얻어오기 위해서 런타임 리플렉션을 이용한다. 저장되는 객체는 퍼시스턴스 필드와 필드 간의 관계에 대한 정보를 제공하는 매핑(mapping) 문서에 정의가 된다. 매핑 문서는 애플리케이션이 동작하면 컴파일되고, 클래스에 필요한 정보를 가진 프레임워크를 제공한다. 뿐만 아니라 매핑 문서는 데이터베이스 스키마 또는 Stub 자바 소스 파일생성 등의 기능을 이용할 때 사용된다.
매핑 문서의 컴파일된 컬렉션으로부터
SessionFactory 객체가 생성된다.
SessionFactory는
Session 인터페이스로 퍼시스턴스 클래스를 관리하기 위한 매커니즘을 제공한다.
Session 클래스는 퍼시스턴스 데이터 저장과 애플리케이션 간의 인터페이스를 제공한다.
Session 인터페이스는 JDBC 연결을 래핑(wrapping)하고 있다. 이 JDBC 연결은 사용자에 혹은, 하이버네이트로부터 관리될 수 있다. 그리고 오직 하나의 애플리케이션 쓰레드에 의해서만 사용되도록 관리된다.
매핑 문서(Mapping Documents)
아래 매핑 문서는
Team,
Player라는 클래스에 관한 내용을 담고 있다.
[SourceCode 1] example.Team 매핑 문서
[SourceCode 2] example.Player 매핑 문서
상당히 이해하기 쉬운 편이지만 설명이 필요한 부분도 있다.
id 요소는 퍼시스턴스 클래스에 의해서 사용되는 주요키를 뜻하는 것으로
id 요소의 속성은 다음과 같다.
- name: 퍼시스턴스 클래스에 의해서 사용되는 프로퍼티(† 역자 주: 자바 클래스에 정의된 프로퍼티의 이름)
- column: 주요 키의 컬럼명
- type: 자바 데이터 타입
- unsaved-value: 이 값은 클래스가 데이터베이스에 저장이 되었는지 알기 위해서 사용된다. 예를 들어 id 속성이 널(null)이라면 하이버네이트는 이 객체가 저장이 안된 것으로 인식한다. 이 속성은 나중에 saveOrUpdate()라는 메소드를 호출할 때 중요하며, 자세한 설명은 나중에 하겠다.
generator는 주요 키를 생성하는데 사용되는 메소드를 명시한다. 여기서는
hilo generator가 사용되었지만, 하이버네이트는 10여 가지 key generation 메소드를 제공한다. 그뿐만 아니라, Composite Primary Key를 포함하여 개발자 자신만의 매커니즘을 이용하는 것도 가능하다.
property는 표준 자바 속성을 정의하고, 스키마에서 이것들이 컬럼으로 매핑되는 방법을 정의하게 된다. 속성은 컬럼의 길이, SQL 타입, 널 값 사용 가능 유무를 명시할 수 있다.
property에는
column이라는 child element를 제공한다. 이 element는 컬럼에 대한 인덱스 명이나 특정 컬럼 타입 같은 추가적인 특징을 명시하기 위해 사용된다.
br>
Team 클래스는 다음과 같은 element 블록을 가지고 있다. 이것은
Team에 속한
Players의 컬렉션을 나타낸다.
[SourceCode 3] example.Team 컬렉션 정의
[SourceCode 3]은
Player 클래스에 정의된 양방향 매핑을 사용하여
Players가
Team에 매핑된 것을 정의한다.
key element로는 foreign key가 정의가 된다.
one-to-many element는 정의된 집합 안에 어떤 객체가 담겨 있는지 정의한다. 예를 들어,
Players라는 컬렉션은
Team의
eam_id라는 컬럼을 foreign key로 참조하며, 컬렉션 안에는
example.Player 객체를 담게 된다.
set element에 있는
lazy와
inverse라는 속성에 주목해야 한다. 컬렉션을
lazy="true"라 표기하는 것은 컬렉션을 포함하는 객체를 데이터베이스로부터 얻어올 때, 컬렉션이 자동으로 생성되지 않는다는 것을 의미한다. 예를 들어, 데이터베이스로부터
Team 객체를 얻어온다면
Players 객체는 애플리케이션이 접근할 때까지 생성하지 않을 것이다.
이렇듯, inverse element는 양방향성을 가진다는 것을 의미한다.
하이버네이트 프로퍼티(Hibernate Properties)
하이버네이트가 데이터베이스와 접속하거나 스키마를 생성하기 위해서 사용하는 프로퍼티는
hibernate.properties에 작성된다. (
†역자 주: 원문에서는 Postgresql 데이터베이스에 접속하는 예제를 보여줬지만 본 기사에서는 역자가 테스트한 환경을 제시하겠다.)
hibernate.connection.username=webfx (데이터베이스에 접속할 수 있는 계정)
hibernate.connection.password=webfx (데이터베이스에 접속하기 위해서 필요한 패스워드)
hibernate.connection.url=jdbc:oracle:thin:@localhost:1521:SHINHWA
hibernate.connection.driver_class=oracle.jdbc.driver.OracleDriver
hibernate.dialect=net.sf.hibernate.dialect.Oracle9Dialect
(사용하고자 하는 데이터베이스 종류에 따라서 하이버네이트가 제공한다.
여기서는 Oracle 9i를 사용했으므로, Oracle9Dialect를 사용했다.)
[SourceCode 4]
위의 4가지 속성은 JDBC 연결에서 많이 접해봤을 것이라 생각하므로 자세한 설명은 생략하겠다.
hibernate.dialect 속성은 하이버네이트에서 제공하는 Hibernate Query Language(HQL)을 SQL로 변환하기 위해 필요한 SQL dialect를 지정한다. 이 속성 덕분에 애플리케이션의 Data Access Layer는 특정 데이터베이스에 종속되지 않게 할 수 있다.
SessionFactory 생성하기
SessionFactory는 팩토리가 생성될 때 지정한 컴파일된 매핑문서을 저장하고 있다.
SessionFactory의 구성 변경 작업은 아주 직관적이다. 모든 매핑 정보는
net.sf.hibernate.cfg.Configuration의 인스턴스에 추가된다. 이것은 나중에
SessionFactory 객체를 생성할 때 사용하게 된다.
Configuration configuration = new Configuration().configure();
Configuration configuration = new Configuration().configure("매핑 파일의 위치");
[SourceCode 5]
위와 같이 매핑 정보를 가지고 있는 Configuration을 생성하는 방법에는 두 가지가 있다. 첫번째 방식처럼 매핑 파일의 위치를 지정하지 않으면 기본적으로 지정된 클래스 패스를 찾게 된다(이 경우는 개인적으로 추천하지 않는다). 외부에서 제공되는 모듈을 사용하는 경우, 각 모듈마다 설정 파일을 가지고 있을 경우가 있다. 이런 경우에는 설정파일을 한 곳에 모아놓고 관리하는 것이 유지 보수하기도 편리할 것이므로, 매핑 파일의 위치를 지정할 수 있는 두 번째 방식을 추천한다.
SessionFactory factory = congiruation.buildSessionFactory();
[SourceCode 6]
Configuration 클래스는
SessionFactory의 생성시에만 필요하다. 그리고 팩토리가 생성된 후에는 버려진다.
Session 인스턴스는
SessionFactory.openSession()을 호출함으로써 얻어지며
Session 인스턴스의 논리적인 lifecycle은 데이터베이스 트랜잭션의 lifecycle과 일치한다.
SessionFactory는 또한 XML 매핑 파일을 이용하여 설정할 수 있다. 매핑 파일은 지정한 클래스 패스의 루트 안에 위치시키면 자동적으로 인식되며, 직접 지정할 수도 있다. (개인적으로는 직접 지정하는 방식을 추천하지만, 패스를 직접 하드 코딩하는 얘기는 아니다. 구체적인 방법은 여기서는 다루지 않겠다.)
※주의사항: 하이버네이트의 프로퍼티를 설정하는 방법은 Properties 파일을 이용하는 방법과 XML 파일을 이용하는 방법이 있다. 이때 둘 중, 하나만 이용하도록 하자. 둘 다 지정된 클래스 패스에 있는 경우, 에러가 발생할 위험이 있다.
Persistent 클래스 생성 및 업데이트
하이버네이트를 이용하는 경우, 객체는 Transient 혹은 Persistent와 같이 두 가지 부류로 나누어진다. Transient 객체는 데이터베이스에 저장되지 않은 객체를 말하는 것으로 Transient 객체를 Persistent하게 만들기 위해서는 Session 클래스를 이용해서 데이터베이스에 저장해야 한다.
Player player = new Player();
// ... player 객체에 대한 처리
-> 이때 까지는 player 객체는 Transient 한 상태이다.
Session session = SessionFactory.openSession();
Session.saveOrUpdate(play);
-> 비로서 player 객체는 Persistent하게 된다.
[SourceCode 7]
saveOrUpdate라는 메소드는
id 프로퍼티가
null인 경우에는 객체를 저장(데이터베이스에 SQL
INSERT)하고,
null이 아닌 경우에는 업데이트 작업을 실행(데이터베이스에 SQL
UPDATE)하게 된다. 위에서 언급된
unsaved-value 속성을 기억하는가? 이와 관련된 내용이다.
할당된
Players를 포함하고 있는
Team 객체를 생성하고 저장하고 싶다면, 다음과 같이 작업하면 된다.
Team team = new Team();
team.setCity ("Detroit");
team.setName ("Pistons");
// add a player to the team.
Player player = new Player();
player.setFirstName ("Chauncey");
player.setLastName ("Billups");
player.setJerseyNumber (1);
player.setAnnualSalary (4000000f);
Set players = new HashSet();
players.add(player);
team.setPlayers(players);
// open a session and save the team
Session session = SessionFactory.openSession();
Session.saveOrUpdate(team);
[SourceCode 8] Persisting 객체
Team 객체와
Set 컬렉션에 저장된
Player 객체가 저장된다.
Persistent 클래스 검색하기
데이터베이스로부터 얻어오고자 하는 객체의 주요 키 값을 알고 있다면, 아래와 같이
Session.load()를 이용하여 그 객체를 얻어올 수 있다.
// method 1: loading a persistent instance
Session session = SessionFactory.createSession();
Player player = session.load(Player.class, playerId);
// method 2: loading the Player"s state
Player player = new Player();
Session.load(player, playerId);
[SourceCode 9] Loading Persistent 인스턴스
주요 키 값을 알지 못한 상태에서 Persistent 객체를 얻어오기 위해서는 Session.find() 메소드를 사용해야 한다. find() 메소드는 HQL(Hibernate Query Language) 구문을 이용해서 java.util.List 형으로 객체들을 얻어올 수 있도록 한다. HQL에 대한 내용은 참고문헌을 찾아보도록 하자.
†역자 주: 혹시, HQL을 이용해야 한다고 듣자마자 "뭐야? SQL을 여기서도 쓰잖아?"라고 대꾸하는 사람이 있을 것으로 생각한다. 역자도 처음에는 그렇게 생각했다. 그렇지만 HQL은 하이버네이트 내에서 표준으로 쓰이는 쿼리 언어이므로 데이터베이스와 독립적으로 쓰일 수 있다. 그리고 상당히 객체 중심이므로 익히기도 편하다.
Persistent 클래스 삭제하기
Persistent 객체를 Transient 객체로 만드는 것은
Session.delete() 메소드를 이용하면 된다. 이 메소드는 특정 객체에 대해서 직접
delete() 메소드를 호출하거나, HQL을 이용하여 특정 조건에 만족하는 객체를 삭제할 수 있다.
// method 1 - deleting the Player loaded in SourceCode 8
session.delete(player);
// method 2 - deleting all of the Players with a
// salary greater than 4 million
session.delete("from player in class example.Player where player.annualSalary
> 4000000");
[SourceCode 10] Persistent 객체 삭제
여기서 데이터베이스에서는 객체가 삭제되지만, 애플리케이션 상에서는 아직 참조하고 있다는 사실에 주목해야 한다. 객체의 컬렉션을 가지고 있는 객체를 삭제할 경우에는 매핑 파일에 cascade="delete"라고 지정해야 컬렉션에 있는 자식 객체까지 모두 삭제가 된다.
Persistence 클래스 대체안
마무리하면서
본 기사에서는 하이버네이트를 아주 간단하게 소개만 했다. 하지만 하이버네이트는 높은 성능을 제공할 뿐만 아니라 다른 오픈 소스나 상용 제품들과 비교해보았을 때에도 결코 뒤지지 않은 오픈 소스 퍼시스턴스 프레임워크를 제공한다. 하이버네이트에 대해 더 자세한 내용을 알고 싶다면 우선
http://www.hibernate.org를 방문하길 바란다.