JDBC를 사용하게 되면 SQL 쿼리 문을 계속해서 만들게 되고, java의 OOP 특성을 최대한 살리기 어렵다는 단점이 있다. 이러한 문제를 해결하기 위해서 ORM을 지원하는 hibernate를 사용하는 것이 좋다.
hibernate는 Object Relational Mapping(ORM)을 지원한다. 이는 java의 객체들을 ORM으로 표현하여 database와 상호 작용할 수 있다는 것이다.
즉, java 객체를 database data에 상응하도록 만들어 java code처럼 data를 쉽게 다루겠다는 것이다.
class Student {
int rollNo;
String name;
int age;
}
다음의 Student
class가 있다면, hibernate를 통해서 아래와 같은 table이 database에 생길 수 있다는 것이다.
CREATE TABLE Student (
rollNo INTEGER,
name TEXT,
age INTEGER
);
Student
의 인스턴스들은 database에 하나의 row에 해당하게 되는 것이다.
이제 hibernate를 설치해보도록 하자. 먼저 우리는 postgres를 사용하고 있기 때문에 postgres driver를 설치해주어야 한다. postgres 설정에 관한 글은 이전 포스트를 참고하도록 하자.
hibernate library는 해당 링크에서 얻을 수 있다. https://mvnrepository.com/artifact/org.hibernate.orm/hibernate-core/6.6.3.Final
implementation("org.postgresql:postgresql:42.7.5")
implementation("org.hibernate.orm:hibernate-core:6.6.3.Final")
종속성을 추가해주고 gradle 또는 maven을 한 번 다시 빌드해주면 External Libraries
에 hibernate package들이 다운로드 된 것을 볼 수 있다.
hibernate를 사용하기 이전에 먼저 연결하려는 database 연결을 준비해야한다. resources/hibernate.cfg.xml
파일을 만들고 다음의 xml 설정 입력을 해주도록 하자.
<hibernate-configuration xmlns="http://www.hibernate.org/xsd/orm/cfg">
<session-factory>
<property name="hibernate.connection.driver_class">org.postgresql.Driver</property>
<property name="hibernate.connection.url">jdbc:postgresql://localhost:5432/demo</property>
<property name="hibernate.connection.username">postgres</property>
<property name="hibernate.connection.password">1234</property>
<property name="hibernate.hbm2ddl.auto">update</property>
</session-factory>
</hibernate-configuration>
<session-factory>
태그 안에 <property>
를 추가하여 설정을 완료하면 되는데, 각각 다음과 같다.
hibernate.connection.driver_class
: 어떤 database에 대한 driver를 사용할 지hibernate.connection.url
: DSN으로 database 접속을 위한 URL hibernate.connection.username
: database 접속을 위한 usernamehibernate.connection.password
: database 접속을 위한 passwordhibernate.hbm2ddl.auto
: hibernates option으로 update
설정 시에 database에 해당 table이 없다면 생성해주고, 있다면 기존의 table을 사용하도록 한다. 수정 사항이 있다면 반영되지만, 기존의 row들이 사라지진 않는다.hibernate.hbm2ddl.auto
이외에는 모두 jdbc에서 볼 수 있는 사항들이다.
이제 hibernate
를 통해서 database에 저장할 Student
클래스를 만들어보도록 하자. 이 Student
class의 정의가 하나의 table 정의가 되는 것이다.
package org.example;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
@Entity
public class Student {
@Id
private int rollNo;
private String sName;
private int sAge;
public int getRollNo() {
return rollNo;
}
public void setRollNo(int rollNo) {
this.rollNo = rollNo;
}
public String getsName() {
return sName;
}
public void setsName(String sName) {
this.sName = sName;
}
public int getsAge() {
return sAge;
}
public void setsAge(int sAge) {
this.sAge = sAge;
}
@Override
public String toString() {
return "Student{" +
"rollNo=" + rollNo +
", sName='" + sName + '\'' +
", sAge=" + sAge +
'}';
}
}
여기서 중요한 것은 @Entity
와 @Id
annotation이다. 이 두 annotation은 재밌게도 import된 경로를 보면 jakarta persistent에서 온 것을 볼 수 있다.
이는 하나의 interface라고 생각하면 된다. ORM framework는 hibernates만 존재하는 것이 아니라, 다양한데 구현체인 ORM framework들에 일관된 사용 방법을 만들기 위해서 jakarta persistent API를 만든 것이고, 이를 오늘 날에 JPA
라고 부르는 것이다.
@Entity
: 해당 java class를 hibernate와 같은 ORM의 대상이 되도록 한다. 없으면 hibernate에서 해당 class에 대한 manipulation이 불가능하다.@Id
: 맴버 변수 중 하나를 Primary key로 두도록 한다. 없으면 PK가 없어서 에러가 발생할 수 있다. 이제 hibernate를 사용하여 database와 상호작용해보도록 하자.
package org.example;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.cfg.Configuration;
import java.sql.*;
public class Main {
public static void main(String[] args) {
Student s1 = new Student();
s1.setRollNo(101);
s1.setsName("Navin");
s1.setsAge(30);
Configuration cfg = new Configuration().configure();
cfg.addAnnotatedClass(Student.class);
// 여러 개의 session을 가지고 있는 session factory는 한 번만 선언하는게 좋다.
SessionFactory sf = cfg.buildSessionFactory();
// 매번 db와 상호작용 시에 session을 열고, 닫는 것이 좋다.
Session session = sf.openSession();
Transaction tx = session.beginTransaction();
session.persist(s1);
tx.commit();
session.close();
sf.close();
System.out.println(s1);
}
}
hibernate를 사용하는 방법은 다음과 같다.
Configuration
을 통해서 config를 읽도록 한다. .configure
메서드를 실행할 때 입력값으로 cfg파일을 넘겨줄 수 있는데, 따로 파일 이름을 안주면 resources/hibernate.cfg.xml
에서 하나 가져온다.addAnnotatedClass
로 어떤 java 클래스를 database와 상호작용하도록 할 것인지 설정하는 것이다. 이는 hibernate를 직접 사용할 때만 사용하고, JPA의 도움을 받는다면 자동으로 @Entity
가 있는 클래스를 찾아서 호출하도록 할 수 있다.cfg.buildSessionFactory()
: Session
을 생성하기 위한 SessionFactory
를 만드는 코드이다. Session
: SessionFactory
에서 openSession
을 실행시키면 session을 열어준다.여기서 Transaction
을 열어주고 commit
해주는 코드가 있는데, Transaction
으로 commit
을 시켲지 않으면 session.persist
를 해도 저장되지 않는다.
이제 위의 코드를 실행시키고 결과를 확인해보도록 하자. psql
로 접속하고 다음의 명령어를 쳐보도록 하자.
SELECT * FROM student;
101 | 30 | Navin
잘 저장된 것을 볼 수 있다.
재미난 점은 hibernate
가 내부적으로 백 그라운드에서는 jdbc를 사용하고 있다는 것이다. 즉, sql문으로 해당 코드를 바꾸어 실행하고 있다는 것이다.
이를 보기 위해서는 cfg 설정을 해주면 된다.
<hibernate-configuration xmlns="http://www.hibernate.org/xsd/orm/cfg">
<session-factory>
...
<property name="hibernate.show.sql">true</property>
</session-factory>
</hibernate-configuration>
이 다음 Student
객체에 대한 정보들을 바꾸자
Student s1 = new Student();
s1.setRollNo(102);
s1.setsName("Gamma");
s1.setsAge(32);
이제 실행시키면 SQL 결과가 나온다.
Hibernate: insert into Student (sAge,sName,rollNo) values (?,?,?)
이전 시간에 JDBC로 했었던 prepared statement가 나오는 것을 볼 수 있다. hibernate는 java object를 이러한 jDBC sql문으로 변환해주어 실행하는 것이었다.
또 다른 설정 값으로는 hibernate.format_sql
이 있는데 이 값을 사용하면 좀 더 보기 좋게 sql문을 보여준다. 더 나아가서 hibernate
의 dialect를 더 정교하게 표시할 수도 있는데, postgres의 경우 다음과 같이 입력하면 된다.
<hibernate-configuration xmlns="http://www.hibernate.org/xsd/orm/cfg">
<session-factory>
...
<property name="hibernate.dialect">org.hibernate.dialect.PostgreSQLDialect</property>
<property name="hibernate.show_sql">true</property>
<property name="hibernate.format_sql">true</property>
</session-factory>
</hibernate-configuration>
데이터를 가져올 때는 transaction을 생성하지 않아도 된다. 이는 database의 data 수정이 있는 경우에만 transaction
을 사용한다는 점을 잘 알도록 하자.
다음과 같이 fetching이 가능하다.
public class Main {
public static void main(String[] args) {
Configuration cfg = new Configuration().configure();
cfg.addAnnotatedClass(Student.class);
SessionFactory sf = cfg.buildSessionFactory();
Session session = sf.openSession();
Student s2 = session.get(Student.class, 102);
System.out.println(s2);
session.close();
sf.close();
}
}
get
을 사용하면 데이터를 가져올 수 있다. 이때 첫번째 인자는 반환되는 인스턴스에 대한 타입을 지정해주기 위함이다. 이는 SQL에서 query문을 실행한 결과로는 해당 결과를 어떤 java class로 변환하여 전달해줄 지 모르기 때문에 제공해주어야 한다. 두번째 인자는 인스턴스의 PK이다.
결과는 다음과 같다.
Hibernate:
select
s1_0.rollNo,
s1_0.sAge,
s1_0.sName
from
Student s1_0
where
s1_0.rollNo=?
Student{rollNo=102, sName='Gamma', sAge=32}
hibernate의 코드가 SELECT
SQL문을 실행하는 것을 볼 수 있다. 결과로 Student{rollNo=102, sName='Gamma', sAge=32}
가 나왔다.
만약, 데이터가 database에 없다고 하면 어떤 결과가 나올까? null
이 나오는데, 직접 출력하면 null
만 나오기 때문에 문제없지만, 관련된 method를 호출할 시에 문제가 생긴다.
public class Main {
public static void main(String[] args) {
Configuration cfg = new Configuration().configure();
cfg.addAnnotatedClass(Student.class);
SessionFactory sf = cfg.buildSessionFactory();
Session session = sf.openSession();
Student s2 = session.get(Student.class, 112);
System.out.println(s2); // null
s2.setsAge(1);
session.close();
sf.close();
}
}
위는 없는 데이터의 sid로 query를 보낸 것으로 112
를 query함에 따른 결과가 null
이 나오는 것이다. 문제는 null
만 출력하게하면 별 문제가 없지만, null
인스턴스가 들은 변수에 메서드를 호출하는 순간 아래와 같은 exception이 발생하는 것이다.
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "org.example.Student.setsAge(int)" because "s2" is null
at org.example.Main.main(Main.java:22)
따라서, get
을 진행할 때는 반드시 null
검사를 해주는 것이 좋다.
이제 update와 delete를 해보도록 하자.
update
의 경우, database에 데이터가 있다면 insert하고 없다면 update하는 방식이다. hibernate에서는 merge
라는 메서드를 통해 가능하고, 이는 database의 상태를 수정하므로 transaction으로 적어주어야 한다.
public class Main {
public static void main(String[] args) {
Student s1 = new Student();
s1.setRollNo(103);
s1.setsName("Harsh");
s1.setsAge(23);
Configuration cfg = new Configuration().configure();
cfg.addAnnotatedClass(Student.class);
SessionFactory sf = cfg.buildSessionFactory();
Session session = sf.openSession();
Transaction tx = session.beginTransaction();
session.merge(s1);
tx.commit();
session.close();
sf.close();
}
}
id가 103인 Harsh
data를 넣어보도록 하자.
현재 database에서는 Harsh
가 없다.
SELECT * FROM student;
101 | 30 | Navin
102 | 32 | Gamma
110 | 21 | Avni
위 java code를 실행하면 아래의 결과가 나온다.
Hibernate:
select
s1_0.rollNo,
s1_0.sAge,
s1_0.sName
from
Student s1_0
where
s1_0.rollNo=?
Hibernate:
insert
into
Student
(sAge, sName, rollNo)
values
(?, ?, ?)
insert
를 실행한 것을 볼 수 있다. 처음 부분에 select
문이 실행된 이유는 hibernate의 동작에 있어서 데이터가 있는 지 없는 지 먼저 가져오고 없다면 insert
있다면 update
를 하기 때문이다.
다시 java code에서 나이를 24
로 바꾸고 실행하면 아래와 같은 sql문이 실행되었다고 나온다.
Hibernate:
select
s1_0.rollNo,
s1_0.sAge,
s1_0.sName
from
Student s1_0
where
s1_0.rollNo=?
Hibernate:
update
Student
set
sAge=?,
sName=?
where
rollNo=?
database에 data가 있으므로 select
문에서 data가 검출되고 update
로 실행되는 것이다. 보기보다 굉장히 간단한 매커니즘으로 실행된다는 것을 알 수 있다. 물론, 필자의 경우 이러한 부분은 개발자가 담당하는게 최적화 면에서 더 좋지 않을까 생각한다.
다음으로 database에 실제로 저장되고 수정된 data가 반영되었는 지 확인해보도록 하자.
SELECT * FROM student;
101 | 30 | Navin
102 | 32 | Gamma
110 | 21 | Avni
103 | 24 | Harsh
반영된 것을 확인할 수 있다.
이제 새로 저장한 데이터를 삭제해보도록 하자. 삭제에는 remove
를 사용하면 된다.
public class Main {
public static void main(String[] args) {
Configuration cfg = new Configuration().configure();
cfg.addAnnotatedClass(Student.class);
SessionFactory sf = cfg.buildSessionFactory();
Session session = sf.openSession();
Transaction tx = session.beginTransaction();
Student s1 = session.get(Student.class, 103);
session.remove(s1);
tx.commit();
session.close();
sf.close();
}
}
session.remove
에 삭제할 대상이 되는 객체를 넘겨주면 된다.
실행된 SQL문을 보도록 하자.
Hibernate:
select
s1_0.rollNo,
s1_0.sAge,
s1_0.sName
from
Student s1_0
where
s1_0.rollNo=?
Hibernate:
delete
from
Student
where
rollNo=?
delete
query가 잘 실행되었고, database 내부를 보면 다음과 같다.
SELECT * FROM student;
101 | 30 | Navin
102 | 32 | Gamma
110 | 21 | Avni
103
id를 가진 Harsh
가 삭제되었다.