스프링에서 하이버네이트 사용하기

김종준·2023년 3월 5일
0

스프링에서 하이버네이트 사용하기

하이버네이트는 객체 관계 매핑 라이브러리(ORM)다.

ORM 라이브러리의 주된 목적은 관계형 데이터베이스 관리 시스템의 관계형 데이터 구조와 자바의 객체지향 모델 사이의 차이를 줄여서 개발자가 객체 모델을 사용해 프로그래밍을 하는 것에 집중하게 하면서 데이터 저장 관련 작업을 쉽게 수행할 수 있게하는 것이다.

하이버네이트 SessionFactory 구성

하이버네이트 핵심 개념은 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 Bean
  • transactionManager Bean :
    하이버네이트 SessionFactory는 트랜잭션이 필요한 데이터 액세스에 사용할 트랜잭션 매니저가 필요하다.
    스프링이 트랜잭션 매니저를 제공하는데 이 트랜잭션 매니저가 HibernateTransactionManager에 선언되어 있다.
  • componet-scan Tag : @Repository 에너테이션이 붙은 컴포넌트를 스캔한다.
  • 하이버네이트 SessionFactory Bean :
    sessionFactory 빈에 dataSource 빈을 주입하였다.
    그리고 도메인 객체를 스캔하도록 구성하였고 하이버네이트 세부 구성을 hibernateProperties 프로퍼티로 전달하였다.

하이버네이트 애너테이션으로 ORM 매핑하기

매핑 방법은 두 가지가 있다.

  1. 객체 모델을 설계하고 설계된 객체 모델을 기반으로 데이터베이스 스크립트를 생성하는 것이다.
  2. 기존 데이터 모델을 기바능로 해당 모델에 맞는 매핑을 가진 POJO를 설계하는 것이다.

단순 매핑

@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 애트리뷰트는 연관 관계의 반대 편과 외래 키가 설정된 컬럼을 정의한다.

하이버네이트 Session 인터페이스

하이버네이트로 데이터베이스를 조작할 때 사용하는 주요 인터페이스는 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)한 뒤 해당 객체를 정리한다.

잘못 설계된 데이터 액세스 로직을 하이버네이트가 세션에 들어있는 데이터를 너무 자주 데이터베이스에 기록하면 성능에 큰 지장을 주게 될 것이다.

0개의 댓글