웹 애플리케이션에서는 데이터가 정말 중요합니다.
데이터를 효율적으로 관리하고 저장하는 것은 개발 과정에서 아주 종요한 요소입니다.
Java 애플리케이션에서 이를 더욱 쉽게 구현하기 위해 하이버네이트(Hibernate)가 사용됩니다.
이 글에서는 영속성(Persistence)의 기본 개념부터 하이버네이트의 작동원리와 실전 예제까지 알아보겠습니다.
영속성(Persistence)의 기본 개념
영속성은 데이터를 영구적으로 저장해 어플리케이션이 종료되더라도 해당 데이터를 유지할 수 있게 하는 개념입니다.
따라서 데이터의 지속성을 보장하기 위해 데이터를 데이터베이스, 파일시스템, 클라우드 저장소와 같은 영구 저장소에 저장하는 것이 필요하며, 이를 영속성이라고 합니다.
🔍 자바 애플리케이션에서의 영속성
자바 애플리케이션에서는 객체(Entity)를 관리할 때, JPA(Java Persistence API)나 하이버네이트(Hibernate)와 같은
ORM(Objcet-Relational Mapping) 프레임워크를 사용하여 영속성을 구현합니다.
영속성 계층은 애플리케이션과 데이터베이스를 연결합니다.
이를 통해 개발자는 데이터베이스와 직접 상호작용하는 대신, 객체 지향 프로그래밍 방식으로 데이터를 효율적으로 관리할 수 있습니다.
영속성 상태의 종류
자바 엔티티는 영속성 컨텍스트(Persistence Context)에 의해 관리되며,
데이터베이스와의 관계를 세 가지 주요 상태로 구분합니다.
상태 | 설명 | 특징 |
비영속상태 | 메모리에만 존재하며 데이터 베이스와 연결되지 않은 상태 |
- 데이터베이스에 저장 X - 관리대상이 아님 |
영속상태 | 데이터 베이스와 연결되어 변경 사항이 자동으로 저장되는 상태 |
- 하이버네이트의 Session객체가 관리 - 트랜잭션 종료 시 데이터 저장 |
준영속상태 | 영속 상태에서 분리된 엔티티로, 더이상 데이터베이스와 동기화 되지 않는 상태 |
- Session과 연결이 끊김 - 트랜잭션 종료 시 데이터 저장 |
1️⃣ 비영속 상태 (Transient)
비영속 상태는 객체를 생성했지만 아직 데이터베이스에 저장하거나 영속성 컨텍스트에 등록하지 않은 상태입니다.
즉, 순수한 자바 객체(Plain Old Java Object, POJO)를 비영속 상태라고 할 수 있습니다.
이러한 객체는 메모리에만 존재하며, 영속성 컨텍스트와의 관계가 없기 때문에 데이터베이스와의 동기화도 이루어지지 않습니다.
- 영속성 컨텍스트에 포함 X
EntityManager에 의해 관리 되지 않습니다. - 데이터베이스와 연관 X
해당 객체는 영구 저장소에 존재하지 않아, 자바 메모리 공간에서만 관리된다. - 캐시 및 변경 추적이 불가능
비영속 상태의 엔티티는 변경감지(dirty checking)가 이루어지지 않는다.
해당 객체의 속성 값을 변경해도 데이터베이스에 반영이 되지 않습니다. - 라이프 사이클의 시작점
엔티티 생명 주기에서 가장 첫번째 단계입니다.
비영속 상태의 예시
@Entity public class Member { @Id @GeneratedValue private Long id; private String name; // Getter, Setter, Constructor } // 비영속 상태 예제 public class TransientExample { public static void main(String[] args) { Member member = new Member(); // 비영속 상태 member.setName("Faker"); // 아직 EntityManager를 사용하지 않았으므로 비영속 상태 System.out.println("비영속 상태의 객체: " + member.getName()); } }
2️⃣ 영속 상태 (Persistent State)
JPA에서 영속성 컨텍스트에 의해 관리되는 엔티티의 상태를 의미합니다.
즉, 데이터베이스와 동기화된 상태로 관리되는 객체입니다.
- 엔티티 관리
영속 상태의 엔티티는 영속성 컨텍스트에서 관리됩니다.
엔티티가 변경되면 트랜잭션이 끝날 때 자동으로 변경사항이 반영(Flush)됩니다. - 1차 캐시 사용
영속성 컨텍스트는 1차 캐시 역할을 합니다.
동일한 엔티티를 반복 조회할 때, 영속성 컨텍스트의 1차 캐시에서 가져옵니다. - 지연쓰기(Write-Behind)
엔티티 변경 시 트랜잭션이 커밋될 때 한번에 변경사항을 전송한다.
(1) 비영속 상태 -> 영속 상태로의 전환
User user = new User("이상혁", "faker@email.com"); // 비영속 상태 entityManager.persist(user); // 영속 상태로 전환
엔티티는 entityManager.persist()
메소드를 호출하면 비영속 상태에서 영속상태로 전환됩니다.
(2) 영속상태의 예시
// 비영속 상태: 엔티티 객체 생성 User user = new User("이상혁", "faker@email.com"); // 영속 상태: 엔티티를 영속성 컨텍스트에 저장 entityManager.persist(user); // Dirty Checking: 값을 수정해도 별도 메서드 호출 없이 자동 반영 user.setEmail("new_faker@email.com"); // 트랜잭션 커밋 시점에 변경 사항이 데이터베이스에 반영됩니다. transaction.commit();
3️⃣ 준영속 상태(Detached)
JPA에서 엔티티가 더 이상 영속성 컨텍스트에 의해 관리되지 않는 상태를 의미합니다.
데이터베이스에는 여전히 데이터가 존재하지만, JPA가 더이상 엔티티를 추적 관리 하지 않기 때문에
자동 변경 감지 같은 기능이 동작하지 않습니다.
- 변경 감지(Dirty Checking) X
JPA가 더이상 변경 사항을 추적하지 않아, 데이터베이스에 자동으로 반영되지 않습니다.. - 데이터베이스와 동기화 X
준영속 상테 엔티티를 수정하면 데이터베이스와 동기화하려면 수동으로 처리해야 합니다.
(1) detach()메소드
User user = entityManager.find(User.class, 1L); // 영속 상태 entityManager.detach(user); // 준영속 상태
특정 엔티티를 영속성 컨텍스트에서 분리할 때 사용한다.
엔티티가 영속 상태에서 준영속 상태로 변경되며, 관리대상에서 제외된다.
(2) clear()메소드
entityManager.clear(); // 모든 영속 상태의 엔티티가 준영속 상태로 전환
영속성 컨텍스트의 모든 엔티티를 준영속 상태로 전환합니다.
일반적으로 트랜잭션 내에서 메모리 사용량을 조절하기 위해 사용합니다.
(3) close()메소드
entityManager.close(); // 영속성 컨텍스트가 종료되고 모든 엔티티가 준영속 상태로 전환
영속성 컨텍스트가 닫히면 관리 중이던 엔티티가 준영속 상태로 바뀝니다.
(4) 준영속 상태 -> 영속상태
User detachedUser = entityManager.find(User.class, 1L); // 영속 상태 entityManager.detach(detachedUser); // 준영속 상태 detachedUser.setEmail("new_email@example.com"); // 변경 사항은 추적되지 않음 User mergedUser = entityManager.merge(detachedUser); // 재영속 상태로 전환
준영속 상태의 엔티티를 merge()
메소드를 사용하여 영속상태로 다시 만들 수 있다.
이 메소드는 기존 엔티티의 변경 사항을 복사한 새 영속 엔티티를 반환한다.
하이버네이트(Hibernate)란 무엇인가?
하이버네이트는 대표적인 ORM(Object-Relational Mapping)프레임워크로,
자바 객체와 관계형 데이터베이스 테이블 간의 매핑 과정을 자동화해 줍니다.
객체 지향 언어인 자바와 관계형 DB 간에는 구조적 차이가 크기 때문에,
이를 매끄럽게 연결해주는 ORM 프레임워크가 개발 생산성과 유지보수성을 크게 높여줍니다.
💡 하이버네이트를 사용하는 이유
🔷 데이터베이스 종속성 감소
SQL 문장을 직접 작성하는 대신, HQL(Hibernate Query Language) 또는 JPQL을 통해 비즈니스 로직을 구현하므로
특정 DB 벤더에 종속되는 코드를 크게 줄일 수 있습니다.
🔷 생산성 향상
JDBC API를 사용할 때 필요한 객체 관리나 예외 처리 로직이 단순화 되어 생산성이 향상된다.
🔷 자동화된 캐싱(Caching)
하이버네이트는 1차 캐시(세션 범위)와 2차 캐시(세션 팩토리 범위, Ehcache등 외부 캐시 연동 가능)를 지원해,
반복적인 DB 쿼리 요청을 줄이고 애플리케이션 성능을 높여줍니다.
하이버네이트의 주요 구성요소
1️⃣ SessionFactory
애플리케이션에서 데이터베이스와 상호 작용할 세션(Session)을 생성해 주는 팩토리 역할을 담당합니다.
일반적으로 애플리케이션이 시작될 때 하이버네이트 설정 정보를 바탕으로 한 번만 인스턴스화(싱글톤)하며,
생성 비용이 크기 때문에 여러 번 반복해서 만들지 않는 것이 권장됩니다.
또한 여러 스레드에서 동시에 안전하게 사용할 수 있도록 스레드 세이프(thread-safe)하게 설계되었으며,
캐싱이나 커넥션 풀링 설정 등 애플리케이션 전반의 데이터베이스 연결 관리와 관련된 중요한 설정들이 SessionFactory에 적용됩니다.
2️⃣ Session
세션은 애플리케이션이 데이터베이스와 상호작용하기 위한 핵심 인터페이스로,
엔티티를 조회, 저장, 수정, 삭제하고 쿼리를 실행하는 등 실제 DB와의 작업을 담당합니다.
일반적으로 트랜잭션 단위로 열고 닫으며, 짧은 시간 동안만 유지하는 것을 권장합니다.
예) 웹 요청마다 열고, 요청 처리가 끝난 뒤 세션을 닫는 방식
또한 thread-safe하지 않으므로,
여러 스레드에서 동시에 공유하여 사용하는 것은 안전하지 않습니다.
세션을 통해 하이버네이트가 제공하는 1차 캐시(영속성 컨텍스트)가 활성화되어,
엔티티의 상태 변화가 자동으로 추적·동기화되는 이점을 제공합니다.
Session 객체는 이미 준비된 SessionFactory에서
openSession()
또는 getCurrentSession()
메서드를 통해 손쉽게 생성할 수 있으며,
트랜잭션 처리를 시작하고 쿼리를 실행한 뒤에는
반드시 세션을 명시적으로 닫아 리소스를 효율적으로 관리해야 합니다.
3️⃣ Transaction
트랜잭션(Transaction)은 데이터베이스 작업을 일관되고 원자적으로 처리하기 위한 핵심요소로,
하나의 논리적 작업 단위를 설정하여 해당 범위 내에서 수행되는 모든 데이터 변경 사항을 안전하게 관리해 줍니다.
하이버네이트에서는 보통 Session 객체를 통해 트랜잭션을 시작하고,
모든 작업을 마친 후에는 커밋 하거나 문제가 발생할 때는 롤백하여 이전 상태로 되돌릴 수 있습니다.
이러한 과정을 통해 무결성과 일관성을 보장할 수 있으며,
결과적으로 애플리케이션이 안정적으로 동작하도록 지원합니다.
4️⃣ Configuration(환경설정)
하이버네이트가 동작하는 데 필요한 데이터베이스 연결 정보, 엔티티 매핑 정보, 캐시 및 로깅 정책 등을 중앙에서 관리하고 초기화하기 위한 핵심 구성 요소입니다.
하이버네이트에서는 보통 hibernate.cfg.xml 같은 설정 파일을 활용하거나,
Configuration 클래스를 통해 이 설정들을 로드한 뒤 SessionFactory를 생성함으로써
애플리케이션 전반에서 일관된 환경 설정을 유지합니다.
이러한 과정을 통해 설정 누락이나 충돌을 방지하고,
다양한 옵션(예: SQL 방언, 드라이버, 풀링 전략 등)을 일괄적으로 적용하여 개발 생산성과 유연성을 높일 수 있습니다.
하이버네이트(Hibernate)의 작동 원리
1️⃣ 설정(Configurations) 로딩 & SessionFactory 생성
(1) 설정 파일 읽기
- 하이버네이트는 보통
hibernate.cfg.xml
또는META-INF/persistence.xml
(JPA 표준) 같은 설정 파일을 통해
DB 연결 정보, 엔티티 매핑 설정, 캐시 설정 등을 불러옵니다. - Spring Boot 환경에서는 application.properties나 application.yml에서 관련 프로퍼티를 읽어들여
하이버네이트를 자동 구성하기도 합니다.
(2) SessionFactory 생성
- 하이버네이트는 설정 정보를 바탕으로 SessionFactory 객체를 생성하며,
엔티티 매핑 정보(어떤 클래스가 어느 테이블과 연결되는지)와 Dialect(사용할 DB 방언), 캐시 전략, 쿼리 전략 등을 초기화합니다. - 일반적으로 애플리케이션 전체에서 SessionFactory는 싱글턴으로 관리되며,
부하가 큰 작업이므로 보통 애플리케이션 기동 시 한 번만 실행됩니다.
2️⃣ 세션(Session) 생성과 영속성 컨텍스트
(1) Session 열기
- SessionFactory로부터 Session을 가져온다.
Session session = sessionFactory.openSession();
- 세션은 DB커넥션에 대한 논리적 래퍼(Wrapper)역할을 하며, 하이버네이트의 영속성컨텍스트를 관리합니다.
(2) 영속성 컨텍스트(Persistence Context)
- 세션이 살아 있는 동안, 해당 세션 내에서 조회된 엔티티들은 영속성 컨텍스트(1차 캐시)에 보관됩니다.
- 동일 세션 안에서는 같은 식별자(Primary Key)의 엔티티를 다시 조회할 때
DB를 재조회하지 않고, 이미 로드된 엔티티를 반환해 성능과 일관성을 유지합니다.
3️⃣ 엔티티 상태 관리와 변경 감지(Dirty Checking)
(1) 엔티티 상태 추적
- 하이버네이트는 엔티티가 세션에 의해 영속화된 순간부터, 해당 엔티티의 필드 변경사항을 추적합니다.
- 이를 통해, 별도의 UPDATE 쿼리를 호출하지 않아도 트랜잭션 커밋 시점에 자동으로 변경된 필드만 업데이트해줍니다.
(2) Dirty Checking(변경 감지)
- 엔티티가 영속성 컨텍스트에 들어온 이후, 각 필드가 변경되었는지를 추적합니다(스냅샷 기법).
- 트랜잭션 커밋 또는
flush()
시점에 변경 사항이 있는 엔티티들에 대해INSERT
,UPDATE
,DELETE
쿼리를 자동 생성해 데이터베이스에 반영합니다.
4️⃣ Flush와 Commit
(1) Flush
- Flush는 영속성 컨텍스트 내 엔티티의 변경 내용을 데이터베이스에 동기화하는 과정입니다.
- 일반적으로 트랜잭션이 커밋될 때 자동으로 실행되지만,
필요에 따라 개발자가 수동으로session.flush()
를 호출해 즉시 동기화할 수도 있습니다. - Flush 시점에 쿼리가 생성되며, Dirty Checking으로 추적된 변경 내역이 실제 쿼리로 DB에 전달됩니다.
(2) Commit
- 트랜잭션을 커밋하면, 실제로 DB 레벨에서 변경 사항이 영구 저장됩니다.
- 커밋이 완료되기 전에 예외가 발생하거나 문제가 생기면, 롤백(Rollback)으로 인해 변경 사항이 취소됩니다.
5️⃣ 데이터 조회와 Lazy Loading(지연 로딩)
(1) 즉시 로딩(Eager Loading)
- 엔티티를 조회할 때, 연관된 엔티티(예: 1:N, N:1 관계 등)도 즉시 한 번에 로딩합니다.
- 데이터가 많을 경우, 불필요한 조인과 대량의 데이터 로드로 성능 저하가 발생할 수 있습니다.
(2) 지연 로딩(Lazy Loading)
- 실제로 해당 연관 엔티티가 필요한 시점(프로퍼티 접근 시점)까지 조회를 미루는 방식입니다.
- 이 경우 하이버네이트는 프록시 객체를 반환하고, 프로퍼티 접근 시 DB에서 데이터를 가져옵니다.
- 세션 범위(트랜잭션 범위)를 벗어나면
LazyInitializationException
이 발생할 수 있으므로,
트랜잭션 스코프 설계에 유의해야 합니다.
6️⃣ 캐싱(1차, 2차)과 성능 최적화
(1) 1차 캐시(Session 레벨 캐시)
- 앞서 언급한 영속성 컨텍스트 자체가 1차 캐시로 동작합니다.
- 동일 세션 내에서 동일 엔티티를 여러 번 조회해도,
DB가 아닌 캐시에서 반환하기 때문에 불필요한 쿼리를 줄일 수 있습니다.
(2) 2차 캐시(SessionFactory 레벨 캐시)
- 애플리케이션 전역에서 자주 사용하는 데이터(코드 테이블, 공통 참조 데이터 등)를 캐싱하여,
여러 세션 간에도 데이터를 재활용할 수 있습니다. - Ehcache, Infinispan, Redis 등 외부 캐시 솔루션을 연동해 사용하기도 합니다.
7️⃣ 쿼리 생성과 실행
(1) HQL, JPQL, Criteria API
- 하이버네이트는 HQL(Hibernate Query Language), JPA 환경에서는 JPQL이라는 객체 지향 쿼리 언어를 제공합니다.
- session.createQuery(
"FROM Employee e WHERE e.department = :dept"
) 처럼 엔티티와 속성(필드) 중심으로 쿼리를 작성합니다. - 내부적으로는 SQL 쿼리로 변환되어 실행되며, 결과를 재매핑해 엔티티 객체로 반환합니다.
(2) SQL 직접 사용(Native Query)
- 특정 DB에 최적화된 쿼리를 써야 하거나, 복잡한 JOIN, 스토어드 프로시저 등을 호출해야 할 땐, Native SQL Query도 지원합니다.
- 이때도 하이버네이트를 통해 결과 매핑을 받을 수 있습니다.
하이버네이트 예시
1️⃣ 엔티티 매핑: 예시
하이버네이트를 사용하려면, 먼저 엔티티(자바 클래스)를 테이블과 매핑해줘야 합니다.
예를 들어, Employee라는 엔티티 클래스를 만들고, 이를 데이터베이스 employee 테이블과 연결해본다고 해봅시다.
import javax.persistence.*; @Entity // 이 클래스를 엔티티로 선언 @Table(name = "progamer") // 매핑할 테이블 이름 public class Progamer { @Id // 기본 키(Primary Key)로 설정 @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "emp_name", nullable = false) private String name; @Column(name = "team") private String team; // 기본 생성자 public Progamer() { } // 생성자, Getter/Setter 등 // ... }
2️⃣ CRUD 연산과 트랜잭션
(1) CREATE
Session session = sessionFactory.openSession(); Transaction tx = session.beginTransaction(); Progamer gamer = new Progamer(); gamer.setName("Faker"); gamer.setTeam("SKT"); session.save(emp); tx.commit(); session.close();
sessionFactory.openSession()
으로 세션을 열고,session.beginTransaction()
으로 트랜잭션을 시작합니다.- 새로운 엔티티 Employee를 생성하고
session.save()
를 통해 영속 상태로 전환합니다. -
tx.commit()
을 하면 데이터베이스에 실제 INSERT 쿼리가 수행됩니다.
(2) READ
Session session = sessionFactory.openSession(); Progamer gamer = session.get(Progamer.class, 1L); session.close();
session.get(엔티티_클래스, 기본키값)
을 통해 데이터베이스에서 엔티티를 조회합니다.- 기본 키가 1L인 Employee 엔티티를 반환합니다. (데이터가 없으면 null 반환)
(3) UPDATE
Session session = sessionFactory.openSession(); Transaction tx = session.beginTransaction(); Progamer gamer = session.get(Progamer.class, 1L); gamer.setTeam("T1"); // 엔티티 필드 변경 tx.commit(); // UPDATE 쿼리 자동 반영 session.close();
- 조회된 엔티티의 값을 변경하고 트랜잭션을 커밋하면 자동으로 업데이트됩니다.
- 하이버네이트 영속성 컨텍스트가 엔티티의 변경 내역을 추적하고, 커밋 시점에 UPDATE 쿼리를 실행합니다.
(4) DELETE
Session session = sessionFactory.openSession(); Transaction tx = session.beginTransaction(); Progamer gamer = session.get(Employee.class, 1L); session.delete(gamer); tx.commit(); session.close();
- 엔티티를 검색 후
session.delete
(엔티티)를 호출하면, 커밋 시점에 DELETE 쿼리가 실행됩니다.
3️⃣ 하이버네이트 쿼리: HQL & Criteria
(1) HQL (Hibernate Query Language)
String hql = "FROM Progamer g WHERE g.team = :lol"; List<Progamer> result = session.createQuery(hql, Progamer.class) .setParameter("lol", "Geng") .list();
(2) Criteria API
CriteriaBuilder cb = session.getCriteriaBuilder(); CriteriaQuery<Progamer> cq = cb.createQuery(Progamer.class); Root<Progamer> root = cq.from(Progamer.class); cq.select(root) .where(cb.equal(root.get("department"), "T1")); List<Progamer> result = session.createQuery(cq).getResultList();
⚠️ 하이버네이트 사용 시 주의사항
🔷 지연 로딩(Lazy Loading) 주의
연관된 데이터를 필요 이상으로 로드하지 않도록 전략을 잘 설정해야 합니다.
반대로, 지연 로딩을 사용하다가 예외적으로 데이터가 필요한 시점에 세션이 닫혀 있으면
LazyInitializationException
이 발생합니다.🔷 쿼리 최적화
복잡한 쿼리를 작성할 때는 실행 계획(Explain)을 확인하고, 인덱스나 조인 전략 등을 최적화해야 합니다.
🔷 트랜잭션 범위 설정
트랜잭션이 너무 길면 DB 락이나 성능 문제가 생길 수 있고, 너무 짧으면 일관성 유지가 힘듭니다.
Spring@Transactional 어노테이션 등을 이용해 적절한 트랜잭션 경계를 설정하세요.
🔷 테이블 매핑 설계
잘못된 매핑으로 인해 N+1 쿼리 문제나 쿼리 폭주가 일어날 수 있습니다.
초기 설계 시점부터 일대다(1:N), 다대다(N:N) 관계 등을 꼼꼼히 점검하세요.
영속성은 데이터를 어떻게 저장(유지)할 것인가? 를 다루는 개념입니다.
하이버네이트는 이 영속성을 ORM 방식으로 구현하여, 객체 중심 개발과 DB 간의 매핑과 동기화를 담당합니다.
이를 통해 개발자는 엔티티의 생명주기를 편리하게 제어하고, 트랜잭션, 캐싱, 쿼리, 최적화 등도 체계적으로 관리할 수 있습니다.