Hibernate란?
- 객체 지향 프로그래밍과 관계형 데이터베이스 설계의 관점 차이를 해결하여 개발자가 더 객체 지향 프로그래밍에 집중할 수 있도록 해주는 ORM 중 하나
- Java에서는 JPA가 표준 인터페이스로 있으며 구현체 중 가장 많이 사용되는 것이 Hibernate
→ 즉, 앞서 포스팅한 JPA(인터페이스)의 구현체(클래스)가 Hibernate
https://bestdevelop-lab.tistory.com/227
환경 설정 파일
- gradle 또는 maven 프로젝트는 기본적으로 src/main 디렉터리에서 java와 resources 패키지로 나뉘는데 resources에 대부분의 환경 설정 파일 및 정적 파일들이 포함
- Hibernate의 환경 설정도 resources에서 작성하면 되고, 환경 설정의 대표적인 파일 형식으로 properties와 xml이 존재
- 파일 이름은 hibernate.properties, hibernate.cfg.xml로 작성 하거나 resources/META-INF 디렉터리 안에 persistence.xml로 작성 가능
- properties와 xml의 차이점은 xml에서는 사용하는 DB와의 매핑 정보 클래스패스를 추가할 수 있다는 점
- maven에는 기본적으로 application.properties에 추가하긴 하지만, 가독성이 훨씬 뛰어난 application.yml 파일이 더 많이 사용 되는 듯 함
# application.properties 설정
hiberante.dialect=org.hibernate.dialect.H2Dialect
# hibernate.connection.driver_class=org.h2.Driver
# hibernate.connection.url=jdbc:h2:file:./build/music
# hibernate.connection.username=sa
# hibernate.connection.password=
hibernate.hbm2ddl.auto=update
hibernate.show_sql=true
hibernate.format_sql=true
hibernate.current_session_context_class=thread
# hibernate.connection.pool_size=1
hibernate.dialect
- 대부분의 DB는 벤더마다 쿼리 형식이 다름
- 기존에는 DB 환경을 마이그레이션 할 때 해당 DB 쿼리를 전부 수정하는 번거로움이 있었음
→ 마이그레이션 : DB, 시스템, 응용 프로그램의 데이터를 기존 시스템에서 다른 시스템으로 이전하는 과정 - 하지만, hibernate는 dialect 옵션으로 사용할 DB만 지정해주면 자동으로 쿼리를 해당 DB에 맞게 변경
hibernate.connection.driver_class
- 연결할 DB의 드라이버를 명시
hibernate.connection.url
- 연결할 DB의 url을 명시
hibernate.connection.username
- 연결할 DB 계정의 username을 명시
hibernate.connection.password
- 연결할 DB 계정의 password를 명시
hibernate.hbm2ddl.auto
- Hibernate가 실행될 때 ddl 옵션 설정
- create
- 애플리케이션 실행 시 같은 이름의 테이블이 존재할 경우 drop하고 다시 create
- create-drop
- 위의 create와 같으나 프로그램 종료 시 모든 테이블을 drop
- update
- 애플리케이션 실행 시 변경된 부분만 반영하며 기존 테이블을 수정하지 않음
- validate
- 엔티티와 테이블이 잘 매핑되었는지만 확인
- none
- 아무런 작업도 하지 않음
- create
hibernate.show_sql
- Hibernate에서 실행하는 SQL 문을 출력할 지 여부 지정
- true시 콘솔에 출력
hibernate.format_sql
- show_sql로 출력되는 쿼리를 포매팅해주는 옵션
- 가독성이 좋아지므로 show_sql을 사용한다면 함께 지정하는 편
hibernate.current_session_context_class=thread
- DB와의 세션 컨텍스트에 관련된 옵션
- 자세한 설명은 글 후반부에 설명
hibernate.connection.pool_size
- 커넥션 풀의 크기를 지정
HikariCP
- DB의 커넥션풀을 관리하는 라이브러리
- 서버 애플리케이션이 DB로 쿼리를 할 때마다 커넥션을 맺고 DB에서 쿼리 실행 후 결과를 리턴하고 커넥션을 종료하는 작업은 복잡할 뿐만 아니라 자원 소모가 많은 작업
- 이를 해결하기 위해 미리 커넥션을 맺어둔 인스턴스를 만들어놓고 요청이 있을 때 해당 인스턴스를 사용하고 반납하는 방식을 사용하는데 이것이 커넥션풀
- Hibernate에서 HikariCP를 사용하면 Hibernate의 옵션도 아래와 같이 지정 가능
hikari 명령어
# HikariCP
hibernate.hikari.minimumIdle=4
hibernate.hikari.maximumPoolSize=4
hibernate.hikari.idleTimeout=30000
hibernate.hikari.dataSourceClassName=org.h2.jdbcx.JdbcDataSource
hibernate.hikari.dataSource.user=sa
hibernate.hikari.dataSource.password=
hibernate.hikari.minimumIdle
- 풀에서 유지해 줄 유휴 상태의 커넥션 최소 개수를 설정
- 최적의 성능과 응답성을 요구한다면 이 값은 설정하지 않는 게 좋다고 hikariCP github에 명시되어 있음
hibernate.hikari.maximumPoolSize
- 유휴(idle) 상태와 사용 중인 커넥션을 포함하여 풀이 허용하는 최대 크기를 명시
- 풀이 이 크기에 도달하고 유휴 커넥션이 없을 때 connectionTimeout이 지날 때까지 getConnection() 호출 블록킹
hibernate.hikari.idleTimeout
- 풀에서 유휴 상태로 유지시킬 최대 시간을 설정
- 이 값은 minimumIsdle 값이 maximumPoolSize 값보다 작은 경우에만 동작 가능
- 기본값은 600000ms(10분)
hibernate.hikari.connectionTimeout
- 풀에서 커넥션을 얻을 때 걸리는 최대 시간을 명시
- 기본값은 30000ms(30초)
- 시간 안에 커넥션을 맺지 못하면 예외가 발생
hibernate.hikari.dataSourceClassName
- hibernate.connection.driver_class와 같은 옵션 (연결할 DB 드라이버 명시)
hibernate.hikari.dataSource.user
- hibernate.connection.username와 같은 옵션 (연결할 DB 계정의 username)
hibernate.hikari.dataSource.password
- hibernate.connection.password와 같은 옵션 (연결할 DB 계정의 password)
매핑(Mapping)
- Hibernate 매핑은 자바 코드로 작성한 객체와 관계형 데이터베이스의 각종 테이블 간의 관계 및 테이블 스키마 등을 매핑하는 것
- 각 테이블을 Hibernate를 사용하여 자바 코드로 작성한 클래스를 엔티티(Entity)라고 함
- 클래스를 엔티티로 만들기 위해서는 클래스에 @Entity(javax.persistence.Entity) 어노테이션 선언
→ 또는 xml 파일에서 등록 가능 - 엔티티를 작성할 때 주의해야 할 점은 기본 생성자가 필수
- 어플리케이션과 DB 간의 작업을 사용할 때 그 사이에서 사용되는 객체가 엔티티이므로 엔티티 작성은 매우 중요
@Data
@Table(name = "Track")
@Entity
public class Track implements java.io.Serializable {
@Id
@Column(name = "TRACK_ID")
@GenenratedValue(strategy = GenerationType.AUTO)
private Long id;
@Column(name = "title", nullable = false)
private String title;
@Column(name = "filePath", nullable = false)
private String filePath;
private LocalTime playTime = LocalTime.of(0, 0, 0);
private LocalDate added = LocalDate.now();
private short volume = 0;
public Track() {}
public Track(String title, String filePath, LocalTime playTime) {
this.title = title;
this.filePath = filePath;
this.playTime = playTime;
}
public Track(String title, String filePath, LocalTime playTime, short volume) {
this.title = title;
this.filePath = filePath;
this.playTime = playTime;
this.volume = volume;
}
}
- LocalTime, LocalDate, LocalDateTime은 Java 8 부터 추가된 자바 시간 형식
- 이전에는 Date와 Calendar를 사용해서 시간을 다루었는데 여기에는 여러 문제가 있었기 때문에 Java 8에서 이 문제들을 해결한 패키지가 java.time 패키지(LocalOOO)
- LocalOOO 클래스들은 어노테이션을 선언하지 않아도 되지만 Date와 Calendar를 사용하는 경우에는@Temporal 어노테이션 지정 필요
- 단, Hibernate 5 & Java 8 이상 환경일 때만 java.time 패키지 사용 가능
@Temporal(TemporalType.TIME)
private Date playTime;
@Temporal(TemporalType.DATE)
private Date added;
Date와 Calender의 문제
1. 불변성 문제
- Date와 Calendar 객체는 가변적(mutable)
- 즉, 생성된 객체의 상태가 변경될 수 있어 멀티스레드 환경에서 안전하지 않으며, 실수로 값이 변경될 수 있는 리스크 존재
2. 사용의 복잡성
- 날짜와 시간을 다루는 여러 메서드가 직관적이지 않았고, Calendar 클래스는 복잡한 구조를 가져 사용하기 어려웠음
- 특정 작업을 위해 여러 단계를 거쳐야 했고, 코드가 클린하지 않을 가능성이 높았음
3. 타임존과 날짜 연산의 불편함
- Date는 타임존 개념을 내장하지 않아 특정 시점의 의미를 명확히 파악하기 어려웠음
- 날짜 계산 역시 직접 구현해야 해서 오류 발생 확률이 높았음
트랜잭션(Transaction)
- Hibernate로 쿼리를 작성하려면 먼저 세션 생성 필요
→ 이는 하나의 트랜잭션 과정 동안 유지되는 세션 - 그리고 세션 생성은 Hibernate에서 제공하는 팩토리로하며 이 팩토리 객체는 싱글톤(Single-ton) 방식
→ 즉, 하나의 애플리케이션에서 하나의 팩토리 객체만 존재
var sessionFactory = HibernateUtil5.getSessionFactory();
- 세션 팩토리 객체를 통해 세션을 생성 후 트랜잭션의 범위 지정
- session.beginTransaction()을 통해 트랜잭션의 시작을 알림
- 쿼리를 실행하고 오류 없이 성공한다면 tx.commit()을 통해 실제 DB에 쿼리 전송
- 만약 작성한 쿼리 중 에러가 발생하면 catch 문에서 tx.rollback()을 통해 해당 트랜잭션을 롤백
- finally 문에서 세션 종료
- 여기서 사용되는 세션 팩토리와 세션은 JPA 표준에서 EntityManagerFactory와 EntityManager로 매칭
var sessionFactory = HibernateUtil5.getSessionFactory();
var session = sessionFactory.openSession();
Transaction tx = null;
try {
tx = session.beginTransaction();
// 쿼리 작성
tx.commit();
} catch (RuntimeException e) {
// 에러 발생 시 롤백
if (tx != null) tx.rollback();
throw e;
} finally {
// 세션을 종료하지 않으면 자동으로 종료되지 않아 자원을 계속 사용한다.
session.close();
}
// 세션을 종료하지 않으면 자동으로 종료되지 않아 자원을 계속 사용한다.
sessionFactory.close();
위 코드의 가장 마지막 줄에 sessionFactory.close()는 세션 팩토리를 완전히 종료하는 것이므로, 애플리케이션 전체에서 더 이상 sessionFactory가 필요하지 않을 때, 즉 애플리케이션이 종료될 때만 호출하는 것을 권장한다고 함
- 우리가 작성하는 쿼리가 session.beginTransaction()과 tx.commit() 사이에 들어감
- insert문은 save()를 통해 수행 가능
- Ex) Track이라는 엔티티를 저장
- 애플리케이션이 실행됐을 때 DB에는 Track이라는 테이블이 생성되고, 이 테이블에 객체를 저장할 예정
- Track 엔티티는 위에서 작성한 것과 동일
- save()는 session 인스턴스의 메서드
import java.time.LocalTime;
import org.hibernate.Transaction;
import com.example.demo.Track;
public class CreateTest {
public static void main(String[] args) {
var sessionFactory = HibernateUtil5.getSessionFactory();
var session = sessionFactory.openSession();
Transaction tx = null;
try {
tx = session.beginTransaction();
var track = new Track("Dynamite",
"vol2/album610/track02.mp3",
LocalTime.of(00,03,30));
session.save(track);
track = new Track("Permission To Dance",
"vol2/album611/track01.mp3",
LocalTime.of(00,04,31));
session.save(track);
track = new Track("My Universe",
"vol2/album612/track00.mp3",
LocalTime.of(00,05,32));
session.save(track);
tx.commit();
} catch (RuntimeException e) {
if (tx != null) tx.rollback();
throw e;
} finally {
session.close();
}
sessionFactory.close();
}
}
- session.save()를 한다고 바로 DB에 쿼리가 발생되는 것이 아니라 JPA는 내부적으로 캐시를 가지고 있고 save()와 같은 쿼리를 저장해둔 뒤 commit()이 수행되면 이 때 실제 DB에 쿼리가 수행
→ tx.commit()을 주석 처리하고 프로그램을 실행시키면 콘솔에 아무런 쿼리도 발생하지 않음
hibernate.current_session_context_class=thread
- 세션 객체는 다음 2가지 방법으로 생성 가능
- sessionFactory.openSession()
- 매번 새로운 세션을 생성하며, 트랜잭션 범위와 무관하게 사용 가능
- 단, close()를 명시적으로 호출하지 않으면 세션이 닫히지 않아 자원 누수 위험이 존재
- sessionFactory.getCurrentSession()
- 현재 스레드와 트랜잭션에 바인딩된 세션을 반환하며, 트랜잭션이 끝나면 세션을 자동으로 닫음
- 이 메서드를 사용하려면 hibernate.current_session_context_class=thread 옵션 설정 필요
- 이 방식은 하나의 트랜잭션에서 동일한 세션을 사용하도록 보장하므로 자원 관리 측면에서 효율적
- 세션은 데이터베이스와의 상호작용을 위한 컨텍스트로, 하나의 트랜잭션 동안 동일한 세션을 유지하는 것을 권장
→ 이는 자원 사용을 최소화하고 데이터 일관성을 유지하는 데 도움 - getCurrentSession()을 사용하면 트랜잭션이 종료될 때 자동으로 세션이 닫히기 때문에 자원 관리가 편리
- 반면, openSession()을 통해 얻은 세션은 트랜잭션이 끝나더라도 자동으로 닫히지 않으므로 반드시 수동으로 close() 호출
- 또한, openSession()으로 세션을 생성한 후 트랜잭션 중 다시 openSession()을 호출하면 새로운 세션 객체가 생성되지만, getCurrentSession()을 호출하면 동일한 세션 객체를 반환
Session session1 = sessionFactory.openSession();
Transaction tx = session1.beginTransaction();
Session session2 = sessionFactory.openSession();
(session1 == session2); // false → openSession()은 새로운 객체 생성
Session session1 = sessionFactory.getCurrentSession();
Transaction tx = session1.beginTransaction();
Session session2 = sessionFactory.getCurrentSession();
(session1 == session2); // true → getCurrentSession()은 현재 스레드와 트랜잭션에 바인딩된 세션 반환
객체 상태
- JPA/Hibernate를 사용할 때 객체는 4가지 상태로 구분
transient(비영속)
- 객체가 막 생성된 상태
→ 즉, 순수한 자바의 인스턴스로만 존재하는 상태 - 영속성 컨텍스트 등록 X, DB 저장 X
persistence(영속)
- 객체가 영속성 컨텍스트에 등록된 상태
→ 즉, 영속성 컨텍스트에서 관리하는 객체가 된 상태 - 변경 감지(Dirty Checking) 가능
- transient(비영속), detached(준영속) 상태의 객체를 persistence(영속)로 만들기 위해서는 아래와 같은 메서드를 사용
→ 비영속, 준영속 ▶ 영속
- persist(Entity)
- save(Entity) : 엔티티 영속화(영속성 컨텍스트 등록)
- saveOrUpdate(Entity) : 영속성 컨텍스트에 없는 경우 저장, 존재하는 경우 업데이트
- Entity 객체를 persistence로 변경
- JPA에는 persist()만 지원
detached(준영속)
- 객체가 영속성 컨텍스트에서 분리되어 관리되지 않는 상태
- 변경 감지 불가
- persistence객체를 detached로 만들려면 아래와 같은 메서드를 사용
→ 영속 ▶ 준영속- evict(Entity)
- 특정 엔티티를 영속성 컨텍스트에서 분리하여 detached 상태로 변경
- clear()
- 현재 영속성 컨텍스트의 모든 엔티티를 detached 상태로 변경
- JPA에는 detach()만 지원(evict와 유사)
- evict(Entity)
- detached 객체를 persistence로 만들려면 아래와 같은 메서드를 사용
- merge(Entity)
- 해당 엔티티의 식별자를 가진 객체가 영속성 컨텍스트가 있다면 인자로 받은 엔티티로 덮어씀
- 동일한 식별자인 엔티티가 없으면 영속성 컨텍스트에 등록
- update(Entity)
- detached 객체의 식별자를 사용해서 persistence 객체로 변경
- 이 때 이미 같은 식별자로 영속성 컨텍스트에 등록된 객체가 있다면 예외 발생
- JPA에는 merge()만 지원
- merge(Entity)
- merge()나 update()로 객체를 수정하기 보다 영속성 컨텍스트의 변경 감지(dirty checking)를 활용하는 편이 바람직 함
remove(삭제)
- 객체가 영속성 컨텍스트와 DB에서 삭제된 상태
- 바로 DB에서 삭제되는 것은 아니고 commit(flush)가 실행되어야 함
- remove(Entity)
- 영속 상태의 객체를 삭제 예약 상태로 만듦
- commit() 또는 flush()가 호출될 때 DB에서 삭제
- delete(Entity)
- 영속 상태의 엔티티 삭제
- remove(Entity)
- delete 객체를 다시 persistence로 만들려면 persist() 사용
- 객체가 복구되는 것은 아니고 인자로 넘어온 엔티티를 영속성 컨텍스트에 등록
- JPA에는 remove()만 지원
단일 쿼리
- JPA/Hibernate에서 단일 쿼리는 작성하는 방법
- session.get(entity.class, id) → 즉시 로딩
- 즉시 DB에서 해당 엔티티를 조회하고 결과 반환
- 조회하려는 엔티티가 없으면 null 반환
- JPA의 find() 메서드
- session.load(entity.class, id) → 지연 로딩
- 즉시 DB에서 조회하지 않고 프록시 객체 반환
- 프록시 객체는 해당 엔티티의 기본 정보(예: 식별자)만 포함된 객체로, 실제 데이터는 참조 시점에 로드
- 만약 엔티티가 존재하지 않으면 프록시가 처음 접근될 때 ObjectNotFoundException이 발생
- 이처럼 load()는 지연 로딩(lazy loading)을 지원하므로, 엔티티가 참조될 때까지 데이터베이스 접근 지연
- load()로 반환된 프록시 객체의 식별자(예: id) 같은 기본 필드는 DB 접근 없이도 사용할 수 있지만, 다른 속성에 접근할 경우 DB에서 데이터를 로드
- JPA의 getReference() 메서드
NamedQuery
- JPA/Hibernate에서는 엔티티에 쿼리를 지정해놓고 메서드 사용
단, 많이 쓰이는 방법은 아니라고 함 - 장점
- 쿼리 재사용 가능
- 컴파일 시 SQL 문법 오류를 잡을 수 있음
@NamedQuery(
name = "com.example.demo.tracksNoLongerThan",
query = "from Track as t where t.playTime <= :length"
)
@Table(name = "TRACK")
@Entity
public class Track implements Serializable {
// 엔티티 정의...
}
public class QueryTest {
public static List<Track> tracksNoLongerThan(LocalTime length, Session session) {
// NamedQuery를 사용하여 쿼리 실행
Query<Track> query =
session.createNamedQuery("com.example.demo.tracksNoLongerThan", Track.class);
// 파라미터 설정
query.setParameter("length", length);
// 결과 반환 (getResultList() : JPA 표준)
// query.list()는 Hibernate 전용 메서드
return query.getResultList();
}
}
매핑 파일에 쿼리 등록하기
- xml 파일에 쿼리를 등록해둘 수 있음
- 사용 방법은 NamedQuery와 동일하고, 이렇게 작성하면 쿼리 작성 시 띄어쓰기와 같은 실수를 줄일 수 있음
<query name="com.example.demo.tracksNoLongerThan">
<![CDATA[
from Track as track
where track.playTime <= :length
]]>
</query>
참고 자료
'Language > Spring' 카테고리의 다른 글
[AOP] AOP란? (1) | 2024.11.14 |
---|---|
[Entity] 엔티티와 영속성 컨텍스트 (1) | 2024.11.09 |
[JPA] JPA의 개념 / JPA를 사용해야하는 상황과 그렇지 못한 상황 (1) | 2024.11.08 |
[Redis] 캐시란? (0) | 2024.09.07 |
[JWT] Refresh Token을 이용한 Access Token 재발급 (0) | 2024.08.28 |