요즘 새 프로젝트는 대부분 어노테이션으로 Hibernate를 설정하지만, 오래된 코드베이스를 만지다 보면 여전히 XML 설정을 만난다. 설정을 코드에서 떼어 두고 싶을 때도 XML 방식이 나쁘지 않다. 이 글에서는 XML 기반 설정을 한 번 훑고 간다.

기본 설정 (hibernate.cfg.xml)

Hibernate의 핵심 설정 파일입니다. 데이터베이스 연결 정보와 매핑 파일을 지정합니다.

<?xml version='1.0' encoding='UTF-8'?>
<!DOCTYPE hibernate-configuration PUBLIC
    "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
    "http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">

<hibernate-configuration>
    <session-factory>
        <!-- 데이터베이스 연결 설정 -->
        <property name="connection.driver_class">oracle.jdbc.driver.OracleDriver</property>
        <property name="connection.url">jdbc:oracle:thin:@localhost:1521:xe</property>
        <property name="connection.username">system</property>
        <property name="connection.password">oracle</property>

        <!-- Hibernate 설정 -->
        <property name="dialect">org.hibernate.dialect.Oracle9Dialect</property>
        <property name="hbm2ddl.auto">update</property>
        <property name="show_sql">true</property>
        <property name="format_sql">true</property>

        <!-- 매핑 파일 -->
        <mapping resource="question.hbm.xml"/>

        <!-- 또는 어노테이션 기반 Entity 클래스 -->
        <mapping class="com.example.Employee"/>
    </session-factory>
</hibernate-configuration>

주요 프로퍼티 설명

프로퍼티 설명
hbm2ddl.auto 테이블 자동 생성 전략
dialect 데이터베이스별 SQL 방언
show_sql 실행되는 SQL 콘솔 출력
format_sql SQL 포맷팅

hbm2ddl.auto 옵션

옵션 설명 사용 환경
create 기존 테이블 삭제 후 새로 생성 테스트
create-drop 세션 종료 시 테이블 삭제 테스트
update 변경사항만 반영 개발
validate 매핑 검증만 수행 운영
none 아무것도 하지 않음 운영

운영 환경에서는 validate 또는 none을 사용하세요. update도 위험할 수 있습니다.

컬렉션 매핑

Hibernate는 <list>, <set>, <bag>, <map> 등의 컬렉션 매핑을 지원합니다.

기본 타입 컬렉션 (List of String)

<class name="com.example.Question" table="q100">
    <id name="id">
        <generator class="increment"/>
    </id>
    <property name="qname"/>

    <list name="answers" table="ans100">
        <key column="qid"/>
        <index column="type"/>
        <element column="answer" type="string"/>
    </list>
</class>

객체 컬렉션 (List of Entity)

<class name="com.example.Question" table="q100">
    <id name="id">
        <generator class="increment"/>
    </id>
    <property name="qname"/>

    <list name="answers">
        <key column="qid" not-null="true"/>
        <index column="type"/>
        <one-to-many class="com.example.Answer"/>
    </list>
</class>

컬렉션 타입 비교

타입 순서 보장 중복 허용 특징
<list> O O 인덱스 컬럼 필요
<set> X X 유일한 요소만
<bag> X O 인덱스 없이 순서 무관
<map> X X 키-값 쌍 저장

Key 태그 속성

<key
    column="컬럼명"
    on-delete="noaction|cascade"
    not-null="true|false"
    property-ref="속성명"
    update="true|false"
    unique="true|false"
/>

Component Mapping

별도 테이블 없이 객체를 컬럼으로 포함합니다.

<class name="com.example.Employee" table="emp177">
    <id name="id">
        <generator class="increment"/>
    </id>
    <property name="name"/>

    <component name="address" class="com.example.Address">
        <property name="city"/>
        <property name="country"/>
        <property name="pincode"/>
    </component>
</class>

결과 테이블 구조:

id name city country pincode
1 홍길동 Seoul Korea 12345

Component는 별도 테이블이 아닌 부모 테이블의 컬럼으로 저장됩니다.

객체 참조 매핑

Many-to-One

여러 Employee가 하나의 Address를 참조합니다.

<class name="com.example.Employee" table="emp211">
    <id name="employeeId">
        <generator class="increment"/>
    </id>
    <property name="name"/>
    <property name="email"/>

    <many-to-one name="address" unique="true" cascade="all"/>
</class>

One-to-One

Employee와 Address가 1:1 관계입니다.

<class name="com.example.Employee" table="emp212">
    <id name="employeeId">
        <generator class="increment"/>
    </id>
    <property name="name"/>
    <property name="email"/>

    <one-to-one name="address" cascade="all"/>
</class>

Cascade 옵션

옵션 설명
none 기본값, 연관 객체에 영향 없음
save-update 저장/수정 시 연관 객체도 함께
delete 삭제 시 연관 객체도 삭제
all 모든 작업 전파
all-delete-orphan all + 부모에서 제거된 자식 삭제

Lazy Loading

연관된 객체를 실제로 사용할 때까지 로딩을 지연합니다.

<list name="answers" lazy="true">
    <key column="qid"/>
    <index column="type"/>
    <one-to-many class="com.example.Answer"/>
</list>

Lazy Loading 옵션

옵션 설명
true 지연 로딩 (기본값, Hibernate 3.0+)
false 즉시 로딩
extra 컬렉션 크기 조회 시 COUNT 쿼리만 실행

Lazy Loading은 성능 최적화에 중요합니다. 하지만 세션이 닫힌 후 접근하면 LazyInitializationException이 발생합니다.

캐시 (Cache)

Hibernate는 두 가지 레벨의 캐시를 제공합니다.

First Level Cache (1차 캐시)

  • 범위: Session 객체 단위
  • 기본값: 항상 활성화
  • 특징: 같은 세션 내에서 동일 엔티티 재조회 시 DB 접근 없음
Session session = sessionFactory.openSession();
// 첫 번째 조회 - DB에서 가져옴
Employee emp1 = session.get(Employee.class, 1);
// 두 번째 조회 - 1차 캐시에서 가져옴 (DB 접근 없음)
Employee emp2 = session.get(Employee.class, 1);
session.close();

Second Level Cache (2차 캐시)

  • 범위: SessionFactory 단위 (전체 애플리케이션)
  • 기본값: 비활성화
  • 특징: 여러 세션에서 공유, 별도 캐시 구현체 필요

설정 방법:

<!-- hibernate.cfg.xml -->
<property name="hibernate.cache.use_second_level_cache">true</property>
<property name="hibernate.cache.region.factory_class">
    org.hibernate.cache.ehcache.EhCacheRegionFactory
</property>

엔티티에 캐시 적용:

<class name="com.example.Employee" table="emp">
    <cache usage="read-write"/>
    <!-- ... -->
</class>

캐시 전략

전략 설명 사용 시점
read-only 읽기 전용, 수정 불가 코드 테이블 등 변경 없는 데이터
read-write 읽기/쓰기 가능 일반적인 경우
nonstrict-read-write 약한 일관성 보장 동시 수정이 거의 없는 경우
transactional JTA 트랜잭션과 연동 분산 환경

트랜잭션 관리

데이터 일관성을 위해 트랜잭션을 적절히 관리해야 합니다.

기본 트랜잭션 패턴

Session session = null;
Transaction tx = null;

try {
    session = sessionFactory.openSession();
    tx = session.beginTransaction();

    // 비즈니스 로직 수행
    Employee emp = new Employee();
    emp.setName("홍길동");
    session.save(emp);

    tx.commit();
} catch (Exception ex) {
    if (tx != null) {
        tx.rollback();
    }
    ex.printStackTrace();
} finally {
    if (session != null) {
        session.close();
    }
}

Spring에서의 트랜잭션 관리

Spring을 사용하면 @Transactional 어노테이션으로 간편하게 관리할 수 있습니다.

@Service
@Transactional
public class EmployeeService {

    @Autowired
    private EmployeeRepository employeeRepository;

    public void createEmployee(Employee emp) {
        employeeRepository.save(emp);
        // 예외 발생 시 자동 롤백
    }

    @Transactional(readOnly = true)
    public Employee getEmployee(Long id) {
        return employeeRepository.findById(id).orElse(null);
    }
}

트랜잭션 전파 옵션

옵션 설명
REQUIRED 기존 트랜잭션 있으면 참여, 없으면 새로 생성 (기본값)
REQUIRES_NEW 항상 새 트랜잭션 생성
SUPPORTS 트랜잭션 있으면 참여, 없으면 없이 실행
NOT_SUPPORTED 트랜잭션 없이 실행, 기존 있으면 보류
MANDATORY 반드시 기존 트랜잭션 필요
NEVER 트랜잭션 있으면 예외 발생
NESTED 중첩 트랜잭션 생성

어노테이션 vs XML 비교

특징 어노테이션 XML
가독성 코드와 함께 있어 이해 쉬움 설정이 분리되어 코드 간결
수정 시 재컴파일 필요 재컴파일 불필요
IDE 지원 자동완성, 검증 우수 상대적으로 약함
유지보수 일반적으로 선호됨 레거시 시스템에서 사용

현재는 어노테이션 기반 설정이 주류입니다. 새 프로젝트에서는 어노테이션을 사용하세요. XML은 레거시 시스템 유지보수나 설정을 코드와 분리해야 할 때 고려하세요.

정리하며

XML 설정을 한 번 훑어두면 Hibernate가 내부적으로 무엇을 들고 있는지 감이 잡힌다. 어노테이션을 쓰더라도 결국 동일한 매핑 정보를 다루기 때문에, 컬렉션 매핑이나 캐시·트랜잭션 옵션 같은 개념은 그대로 살아남는다. 레거시 코드를 만지다 막힐 때 다시 와서 들춰볼 만한 레퍼런스로 남겨둔다.