Java 재활 훈련 16일차 - Hibernate1 - @Entity, @Id, CRUD

0

java

목록 보기
16/18

Hibernate

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

  • build.gradle.kts
    implementation("org.postgresql:postgresql:42.7.5")
    implementation("org.hibernate.orm:hibernate-core:6.6.3.Final")

종속성을 추가해주고 gradle 또는 maven을 한 번 다시 빌드해주면 External Libraries에 hibernate package들이 다운로드 된 것을 볼 수 있다.

hibernate 사용해보기

hibernate를 사용하기 이전에 먼저 연결하려는 database 연결을 준비해야한다. resources/hibernate.cfg.xml 파일을 만들고 다음의 xml 설정 입력을 해주도록 하자.

  • resources/hibernates.cfg.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>를 추가하여 설정을 완료하면 되는데, 각각 다음과 같다.

  1. hibernate.connection.driver_class: 어떤 database에 대한 driver를 사용할 지
  2. hibernate.connection.url: DSN으로 database 접속을 위한 URL
  3. hibernate.connection.username: database 접속을 위한 username
  4. hibernate.connection.password: database 접속을 위한 password
  5. hibernate.hbm2ddl.auto: hibernates option으로 update 설정 시에 database에 해당 table이 없다면 생성해주고, 있다면 기존의 table을 사용하도록 한다. 수정 사항이 있다면 반영되지만, 기존의 row들이 사라지진 않는다.

hibernate.hbm2ddl.auto 이외에는 모두 jdbc에서 볼 수 있는 사항들이다.

이제 hibernate를 통해서 database에 저장할 Student 클래스를 만들어보도록 하자. 이 Student class의 정의가 하나의 table 정의가 되는 것이다.

  • Student.java
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라고 부르는 것이다.

  1. @Entity: 해당 java class를 hibernate와 같은 ORM의 대상이 되도록 한다. 없으면 hibernate에서 해당 class에 대한 manipulation이 불가능하다.
  2. @Id: 맴버 변수 중 하나를 Primary key로 두도록 한다. 없으면 PK가 없어서 에러가 발생할 수 있다.

이제 hibernate를 사용하여 database와 상호작용해보도록 하자.

  • Main.java
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를 사용하는 방법은 다음과 같다.

  1. Configuration을 통해서 config를 읽도록 한다. .configure 메서드를 실행할 때 입력값으로 cfg파일을 넘겨줄 수 있는데, 따로 파일 이름을 안주면 resources/hibernate.cfg.xml에서 하나 가져온다.
  2. addAnnotatedClass로 어떤 java 클래스를 database와 상호작용하도록 할 것인지 설정하는 것이다. 이는 hibernate를 직접 사용할 때만 사용하고, JPA의 도움을 받는다면 자동으로 @Entity가 있는 클래스를 찾아서 호출하도록 할 수 있다.
  3. cfg.buildSessionFactory(): Session을 생성하기 위한 SessionFactory를 만드는 코드이다.
  4. 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>

hibernate fetching

데이터를 가져올 때는 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 and Delete

이제 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가 삭제되었다.

0개의 댓글