하이버네이트는 객체 관계 매핑 라이브러리(ORM)다.
ORM 라이브러리의 주된 목적은 관계형 데이터베이스 관리 시스템의 관계형 데이터 구조와 자바의 객체지향 모델 사이의 차이를 줄여서 개발자가 객체 모델을 사용해 프로그래밍을 하는 것에 집중하게 하면서 데이터 저장 관련 작업을 쉽게 수행할 수 있게하는 것이다.
하이버네이트 핵심 개념은 Session
인터페이스를 기반으로 하며, 해당 인터페이스는 SessionFactory
에서 얻을 수 있다.
스프링은 하이버네이트의 SessionFactory
구성에 사용하는 클래스를 원하는 프로퍼티가 포함된 빈으로 제공한다.
하이버네이트를 사용하려면 프로젝트에 하이버네이트 의존성을 등록해야 한다.
그리고 자바 구성 클래스를 통해 하이버네이트르 설정해주어야 한다.
@Configuration
@EnableTransactionManagement
public class AppConfig {
@Bean
public DataSource dataSource () {
try {
EmbeddedDatabaseBuilder dbBuilder = new EmbeddedDatabaseBuilder();
return dbBuilder.setType(EmbeddedDatabaseType.H2)
.addScripts("classpath:...").build();
} catch (Exception e) {
...
return null;
}
}
private Properties hibernateProperties() {
Properties hibernateProp = new Properties();
hibernateProp.put("hibernate.dialect", "org.hibernate.dialect.H2Dialect");
hibernateProp.put("hibernate.format_sql", true);
hibernateProp.put("hibernate.use_sql_comments", true);
hibernateProp.put("hibernate.max_fetch_depth", 3);
hibernateProp.put("hibernate.jdbc.batch_size", 10);
hibernateProp.put("hibernate.jdbc.fetch_size", 50);
return hibernateProp;
}
@Bean
public SessionFactory sessionFactory() throws IOException {
LocalSessionFactoryBean sessionFactoryBean = new LocalSessionFactoryBean();
sessionFactoryBean.setDataSource(dataSource());
sessionFactoryBean.setPackagesToScan("...");
sessionFactoryBean.setHibernateProperties(hibernateProperties());
sessionFactoryBean.afterPropertiesSet();
return sessionFactoryBean.getObject();
}
@Bean
public PlatformTransactionManager trnasactionManager() throws IOException {
return new HibernateTransactionManager(sessionFactory);
}
}
dataSource
BeantransactionManager
Bean :componet-scan
Tag : @Repository 에너테이션이 붙은 컴포넌트를 스캔한다.SessionFactory
Bean :매핑 방법은 두 가지가 있다.
@Entity
애너테이션은 해당 엔티티가 매핑된 엔티티 클래스임을 나타낸다.
@Table
애너테이션은 엔티티 클래스가 매핑돼야 할 데이터베이스 테이블 이름을 정의한다.
@Column
애너테이션은 컬럼 이름을 지정한다.
@Temporal
애너테이션은 자바 Date 타입을 SQL Date 타입으로 매핑하고 싶다는 뜻이다.
@Id
id 애트리뷰트가 객체의 기본 키임을 뜻한다.
@GenerateValue
애너테이션은 id 값 생성 방법을 하이버네이트에 알려준다.
@Version
애너테이션은 version 애트리뷰트를 제어 수단으로 사용하는 낙관적 잠금 메커니즘을 사용한다.
하이버네이트가 레코드를 수정할 때마다 인스턴스의 version과 데이터베이스 레코드의 version을 비교한다.
만약 버전이 같으면 이전에 아무도 수정하지 않았다는 뜻이므로 하이버네이트가 데이터를 수정하면 version 값을 증가시킨다.
@OneToMany
애너터이션은 일다대 관계를 나타낸다.
mappedBy
애트리뷰트는 해당 테이블에 외래 키로 연결된 애트리뷰트를 말한다.
cascade
애트리뷰트는 수정 작업이 수정할 테이블부터 관련 있는 자식 테이블의 레코드까지 "모두 전이돼야 함"을 나타낸다.
orphanRemoval
애트리뷰트는 해당 값이 수정되어 연관관계가 끊어졌을 때 자식 엔티티를 자동으로 삭제해준다.
@ManyToOne 애너테이션 역시 일대다 관계를 나타내며 @JoinColum
애너테이션으로 외래키 컬럼 이름을 지정한다.
@ManyToMany
애너테이션은 다대다 관계를 나타낸다.
@JoinTable
애너테이션을 적용해 하이버네이트가 검색해야 할 조인 테이블을 지정한다.
@JoinTable
에 지정한 name
애트리뷰트는 조인 테이블 이름을 정의한다.
joinColumns
애트리뷰트는 외래 키가 설정된 컬럼을 정의하며 inverseJoinColumns
애트리뷰트는 연관 관계의 반대 편과 외래 키가 설정된 컬럼을 정의한다.
하이버네이트로 데이터베이스를 조작할 때 사용하는 주요 인터페이스는 SessionFactory에서 얻을 수 있는 Session 인터페이스이다.
@Transactional
@Repository("singerDao")
public class SingerDaoImpl implements SingerDao {
private SessionFactory sessionFactory;
public SessionFactory getSessionFactory() {
return sessionFactory;
}
@Resource(name = "sessionFactory")
public void setSessionFactory(SessionFactory sessionFactory) {
this.sessionFactory = sessionFactory;
}
}
@Repository
애너테이션을 적용해 스프링 빈으로 선언하였다.
@Transactional
애너테이션은 트랜잭션 요구사항을 정의할 때 사용한다.
sessionFactory 애트리뷰트는 @Resource
애너테이션으로 주입된다.
하이버네이트에서는 하이버네이트 쿼리 언어로 쿼리를 정의할 수 있다.
데이터베이스르 조작할 때 하이버네이트는 HQL로 작성한 쿼리를 개발자 대신 SQL 문으로 변환한다.
@Transactional
@Repository("singerDao")
public class SingerDaoImpl implements SingerDao {
private SessionFactory sessionFactory;
@Transactional(readOnly = true)
public List<Singer> findAll() {
return sessionFactory.getCurrentSession().createQuery("from Singer s").list();
}
}
위는 지연로딩을 하는 간단한 예제 코드이다.
SessionFactory.getCurrentSession()
메서드를 사용해 하이버네이트의 Session 인터페이스를 가져온다.
그 다음 HQL 문을 인자로 전달하여 Session.createQuery()
메서드를 호출한다.
하지만 이때 연관 관계에 있는 레코드에 접근하려 하면 하이버네이트가 LazyInitializationException
을 던진다.
이는 하이버네이트가 기본으로 연관 관계를 지연 로딩하며, 레코드에 연관된 테이블을 조인하지 않기 때문이다.
지연 로딩을 하는 근본적인 이유는 성능 때문이다.
하이버네이트가 연과 관계 데이터를 조회하는 데는 두 가지 방법이 있다.
하나는 연관 관계를 정의할 때 @ManyToMany(fecth=FecthType.EAGER)
처럼 로딩 방법을 즉시 로딩으로 지정하는 것이다.
즉시 로딩을 설정하면 하이버네이트는 객체를 쿼리할 때마다 연관 레코드도 모두 조회한다.
그렇기에 이는 데이터 조회 성능에 영향을 준다.
다른 방법은 필요시 하이버네이트가 연관된 레코드를 조회하도록 쿼리 내에 강제하는 것이다.
크라이티리어 쿼리를 사용할 때는 Criteria.setFetchMode()
함수를 호출하면 하이버네이트가 즉시 연관된 레코드를 조회한다.
또한 NamedQuery
를 사용할 때 fetch 연산자를 사용하면 마찬가지로 하이버네이트가 즉시 연관된 데이터를 조회한다.
@Entity
@Table(name = "singer")
@NamedQueries( {
@NamedQuery(name = "Singer.findAllWithAlbum"
query = "select distinct s from Singer s"
+ "left join fetch s.albums a"
+ "left join fetch s.instruments i")
})
public class Singer implements Serializable {
...
}
위 코드에서는 Singer.findAllWithAlbum
이라는 이름으로 NamedQuery 인스턴스를 정의한다.
그 다음에 HQL로 쿼리를 정의한다.
left join fetch
절을 사용해 하이버네이트가 연관 관계를 즉시 조회하도록 지정하였다.
select distinct
를 사용해 하이버네이트가 중복 객체를 반환하는 것을 방지하였다.
@Transactional
@Repository("singerDao")
public class SingerDaoImpl implements SingerDap {
@Transactional(readOnly = true)
public List<Singer> findAllWithAlbum() {
return sessionFacotry.getCurrentSession.getNamedQuery("Singer.findAllWithAlbum").list();
}
}
Session.getNamedQuery()
메서드를 사용하며 여기에 NamedQeury
인스턴스를 이름을 인자로 전달하였다.
NamedQuery
에 파라미터를 사용하는 다른 예제로 findById()
메서드를 구현하며 연관 관계도 함께 조회할 수 있다.
@Entity
@Table(name = "singer")
@NamedQueries( {
@NamedQuery(name = "Singer.findById"
query = "select distinct s from Singer s"
+ "left join fetch s.albums a"
+ "left join fetch s.instruments i"
+ "where s.id = :id")
})
public class Singer implements Serializable {
...
}
Singer.findById
NamedQuery
에서는 id
라는 네임드 파라미터를 선언하였다.
@Transactional
@Repository("singerDao")
public class SingerDaoImpl implements SingerDap {
@Transactional(readOnly = true)
public List<Singer> findById(Long id) {
return (Singer) sessionFactory.getCurrentSession()
.getNamedQuery("Singer.findById")
.setParameter("id", id).uniqueResult();
}
}
이전 예제 구현과 다르게 setParameter()
메서드를 호출하며 여기에 네임드 파라미터와 값을 전달하였다.
여러 파라미터를 설정할 때는 Query
인터페이스의 setParameterList()
나 setParameters()
메서드를 사용할 수 있다.
하이버네이트로 데이터를 등록하는 것은 간단하다.
또 다른 매력적인 기능은 데이터베이스가 생성한 기본 키를 조회할 수 있다는 것이다.
하이버네이트에선는 생성된 키를 조회하고 등록 후에 도메인 객체를 조회한다.
아래는 save()
메서드를 구현하는 코드이다.
@Transactional
@Repository("singerDao")
public class SingerDaoImpl implements SingerDap {
public Singer save(Singer singer) {
sessionFactory.getCurrentSession().saveOrUpdate(singer);
return singer;
}
}
데이터를 등록하려면 단순히 레코드 등록 및 수정 작업을 담당하는 Session.saveOrUpdate()
메서드를 호출하면 된다.
데이터 수정은 데이터 등록만큼이나 간단하다.
수정할 레코드를 조회하고 수정한다.
그리고 save()
메서드를 통해 수정한 내용을 저장하면 된다.
Singer singer = singerDao.findById(1L);
singer.setFirstName("John Clayton");
singer.removeAlbum(album);
singerDao.save(singer);
데이터 삭제 역시 간단하다.
Session.delete()
메서드를 호출하며 삭제하려는 객체를 넘기면 된다.
@Transactional
@Repository("singerDao")
public class SingerDaoImpl implements SingerDap {
public void delete(Singer singer) {
sessionFactory.getCurrentSession().delete(singer);
}
}
하이버네이트로 애플리케이션을 개발 시작할 때는 앤티티 클래스를 먼저 작성하고 앤티티 클래스 내용을 바타으로 데이터베이스 테이블을 생성하는 것이 일반적이다.
hibernate.hbm2ddl.auto
라는 하이버네이틀 프로퍼티를 지정하면 엔티티를 사용해 데이터베이스 테이블을 생성할 수 있다.
애플리케이션을 처음으로 실행할 때는 프로퍼티 값을 create
로 설정한다.
이렇게 하면 하이버네이트가 엔티티를 스캔해 테이블을 만들고 JPA와 하이버네이트 애너테이션으로 엔터티 사이에 정의한 관계에 맞춰 키를 생성한다.
엔터티를 제대로 구성했으며, 그 결과 생성된 데이터베이스 객체가 원하는 대로 생성됐다면 hibernate.hbm2ddl.auto
프로퍼티를 update
로 변경해야 한다.
이렇게 하면 하이버네이트는 이후 엔티티에 발생한 수정 사항을 기존 데이터 베이스에 적용하며, 원래의 데이터베이스와 여기에 등록돼 있던 데이터는 다시 생성하지 않고 유지한다.
아래 코드느 자바 구성 클래스인 AdvancedConfig
코드이다.
Hiberanate.hbm2ddl.auto
를 도입했으며 데이터 소스로 DBCP
풀드 데이터 소스를 사용한다.
@Configuration
@EnableTransactionManagement
public class AdvancedConfig {
@Value("${driverClassName}")
private String driverClassName;
@Value("${url}")
private String url;
@Value("${username}")
private String username;
@Value("${password}")
private String password;
@Bean
public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
return new PropertySourcesPlaceholderConfigurer();
}
@Bean(destroyMethod = "close")
public DataSource dataSource () {
try {
BasicDataSource dataSource = new BasicDataSource();
dataSource.setDriverClassName(driverClassName);
dataSource.setUrl(url);
dataSource.setUsername(username);
dataSource.setPassword(password);
return dataSource
} catch (Exception e) {
...
return null;
}
}
private Properties hibernateProperties() {
Properties hibernateProp = new Properties();
hibernateProp.put("hibernate.dialect", "org.hibernate.dialect.H2Dialect");
hibernateProp.put("hibernate.hbm2ddl.auto", "create-drop");
hibernateProp.put("hibernate.format_sql", true);
hibernateProp.put("hibernate.use_sql_comments", true);
hibernateProp.put("hibernate.max_fetch_depth", 3);
hibernateProp.put("hibernate.jdbc.batch_size", 10);
hibernateProp.put("hibernate.jdbc.fetch_size", 50);
return hibernateProp;
}
@Bean
public SessionFactory sessionFactory() {
return new LocalSessionFactoryBuilder(dataSource())
.scanPackages("...")
.addProperties(hibernateProperties)
.buildSessionFactory();
}
@Bean
public PlatformTransactionManager trnasactionManager() throws IOException {
return new HibernateTransactionManager(sessionFactory);
}
@Bean(destroyMethod = "destory")
public CleanUp cleanUp() {
return new CleanUp(new JdbcTemplate(dataSource()));
}
}
JPA 애너테이션은 필드에도 직접 적용할 수 있는데, 해당 방법은 몇 가지 장점이 있다.
데이터베이스에는 실제로 객체의 상태가 저장되며, 저장되는 객체의 상태는 접근자가 반환하는 값이 아니라 객체의 필드 값으로 정의되는 것을 유념해야 한다.
이는 객체를 데이터베이스에 저장한 방법을 사용해 데이베이스에서 객체를 정확하게 다시 생성할 수 있음을 의미한다.
그러므로 어떤 면에서는 접근자에 애너테이션을 다는 것은 캡슐화를 깨는 일인 셈이다.
하이버네이트가 생성하는 SQL을 직접 제어할 수 없으므로 매핑을 정의할 때, 특히 연관 관계 및 로딩 전략을 정의할 때 주의해야 한다.
다음으로 하이버네이트가 생성하는 SQL 문장을 살펴보며 모든 SQL 문장이 예상대로 동작하는지 검증해야 한다.
하이버네이트가 어떻게 하이버네이트 세션을 관리하는지 내부 메커니즘을 이해하는 것도 중요한데, 특히 배치 잡을 조작할 때는 내부 메커니즘을 잘 이해해야 한다.
하이버네이트는 세션에 관리 대상 객체를 가지고 있다가 주기적으로 객체 내용을 DB에 기록(flush)한 뒤 해당 객체를 정리한다.
잘못 설계된 데이터 액세스 로직을 하이버네이트가 세션에 들어있는 데이터를 너무 자주 데이터베이스에 기록하면 성능에 큰 지장을 주게 될 것이다.