JPA(Java Persistence API)

[JPA] Part 3. JPA를 뿌리부터 시작해보기

cwchoiit 2023. 10. 17. 13:57
728x90
반응형
SMALL
728x90
반응형
SMALL

JPA를 진짜 깊게 이해하기 위해 아예 순수 JPA 세팅부터 시작해 보려고 한다. 내가 처음 JPA를 사용했을 때 느꼈던 것보다 훨씬 진짜 훨씬 더 많은 내용이 JPA에 있었는데 지금이라도 깊게 공부하게 되서 다행인거 같다.

 

우선 프로젝트를 Maven 기반으로 시작해보자.

 

pom.xml

현재 정말 아무것도 없는 상태에서 pom.xml 파일 하나만 있는 상태다. 이 파일에서 필요한 두 가지가 있는데 하나는 hibernate, 하나는 h2database dependency다.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>hello-jpa</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>18</maven.compiler.source>
        <maven.compiler.target>18</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-entitymanager</artifactId>
            <version>5.6.15.Final</version>
        </dependency>

        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <version>2.2.224</version>
        </dependency>
    </dependencies>

</project>

딱 두개의 Dependency가 있다. 내려받고 나면 우측 Maven의 Dependencies 섹션에 두 개가 노출된다.

 

persistence.xml

이제 JPA를 사용하기 위해 persistence.xml 파일이 필요하다. 설정 파일이라고 생각하면 되는데 사실 거의 JPA를 사용할 때 스프링과 연동하여 사용하기 때문에 xml 파일로 설정값을 지정하지 않는데 정말 뿌리부터 시작해보기 위해 해보려고한다.

 

persistence.xml 파일은 경로가 중요하다. 이 파일은 반드시 META-INF 폴더 아래에 존재해야 한다. 그래서 내 경로는 다음과 같다.

resources/META-INF/persistence.xml  이 경로에 해당 파일이 존재하면 된다. 이 파일은 다음과 같은 설정 파일이 필요하다.

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.2"
             xmlns="http://xmlns.jcp.org/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.xsd">
    <persistence-unit name="hello">
        <properties>
            <!-- 필수 속성 -->
            <property name="javax.persistence.jdbc.driver" value="org.h2.Driver"/>
            <property name="javax.persistence.jdbc.user" value="sa"/>
            <property name="javax.persistence.jdbc.password" value=""/>
            <property name="javax.persistence.jdbc.url" value="jdbc:h2:tcp://localhost/~/h2/test"/>
            <property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/>
            <!-- 옵션 -->
            <property name="hibernate.show_sql" value="true"/>
            <property name="hibernate.format_sql" value="true"/>
            <property name="hibernate.use_sql_comments" value="true"/>
            <property name="hibernate.jdbc.batch_size" value="10"/>
<!--            <property name="hibernate.hbm2ddl.auto" value="create-drop" />-->
        </properties>
    </persistence-unit>
</persistence>

persistence-unit의 name은 나중에 Persistence에서 EntityManagetFactory를 만들 때 필요한 값이다. 저 값을 통해 어떤 설정값을 가져올지를 지정한다. 나머지 속성값은 어떤 데이터베이스를 사용할지 나는 h2 database를 사용할거고, 데이터베이스 접속할 유저정보데이터베이스의 위치정보 그리고 dialect 정보를 지정한다.

 

dialect가 재밌는 옵션인데, 데이터베이스마다 살짝 살짝 다른식으로 표현하는 것을 본적이 있을거다. 예를 들면, MySQL은 VARCHAR Oracle은 VARCHAR2라던가, MySQL은 LIMIT Oracle은 ROWNUM이라던가. 이런걸 데이터베이스 방언이라고 표현하는데 이 방언을 어떤걸 선택할건지를 지정하는 옵션이라고 보면 된다. 나는 H2를 사용하니까 당연히 H2Dialect를 사용하면 된다.

 

show_sql은 데이터베이스에 날리는 쿼리를 보여줄것인지 여부를 의미한다. format_sql은 show_sql이 true인 경우에 한하여 SQL문을 정렬해서 좀 보기 좋게 보여주겠다는 옵션이고, use_sql_comments는 show_sql이 true인 경우에 한하여 SQL문에 주석 데이터를 추가해주는 옵션이다. 그러니까 이게 어떤 SQL문인지 뭐 그런 comments를 보여주는 옵션. batch_size는 이후에 또 배우겠지만 쓰기 지연 SQL 쿼리를 한번에 날릴 수 있는 사이즈를 지정하는 것이다. 이건 이후에 영속성 컨텍스트를 얘기하면서 다시 얘기하겠다.

 

이렇게 설정해놓으면 필요한 설정 파일은 전부 구성했고 이제 실제 코드를 작성해보자. 

 

 

Member Entity

package org.example.entity;

import javax.persistence.*;

@Entity
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(unique = true, nullable = false, length = 50)
    private String name;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

Member라는 간단한 엔티티는 id, name 컬럼을 가진다. 이 클래스의 @Entity 어노테이션을 붙이면 JPA는 해당 클래스를 테이블로 매핑한다. 이렇게 클래스를 엔티티로 구현하고 h2 database에 테이블로 만드는 방법은 persistence.xml 파일의 프로퍼티 중 ddl-auto를 create으로 설정하거나, 직접 h2 database에 들어가서 테이블을 만들면 된다. 

 

아 그리고 한가지 짚고 넘어갈 내용은 이 아래 @Column에 적힌 unique, nullable, length 이런 제약 조건은 데이터베이스 테이블에 영향을 주는거고 애플리케이션에는 아무런 영향이 없다는 것. 그러니까 객체로 만들고 데이터베이스에 저장하지 않으면 객체가 같은 이름을 가지던 길이가 50이 초과되던 상관없단 소리다.

@Column(unique = true, nullable = false, length = 50)

H2 Database

우선 H2 Database Engine을 다운받아야 한다. 

https://www.h2database.com/html/main.html

 

H2 Database Engine

H2 Database Engine Welcome to H2, the Java SQL database. The main features of H2 are: Very fast, open source, JDBC API Embedded and server modes; in-memory databases Browser based Console application Small footprint: around 2.5 MB jar file size     Supp

www.h2database.com

위 경로에서 Download 받으면 된다.

 

2.2.224 버전을 사용하게 되면 자동으로 데이터베이스를 만들어주지 않는다. 그래서 데이터베이스를 직접 만들어야 하는데, 나같은 경우엔 데이터베이스 경로를 /Users/cw.choiit/h2 이렇게 설정했다. 여기에 데이터베이스명은 'test'라고 할 것이기 때문에 해당 경로에 파일 하나를 추가하면 된다. 'test.mv.db' 이 파일 하나만 추가해주면 H2 database를 실행하고 콘솔에서 연결할 수 있다.

H2 database를 로컬에서 실행하면 위 경로에 들어가서 콘솔을 이용할 수 있는데 여기서 원하는 경로에 데이터베이스를 연결하고 'Connect' 버튼을 클릭하면 데이터베이스 안으로 들어갈 수 있다.

 

Main Class

package org.example;

import org.example.entity.Member;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        // ! EntityManagerFactory 이 녀석은 서비스가 띄워지면 딱 한개만 생성되어야 한다.
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");

        // ! EntityManager 이 녀석은 어떤 디비에 작업을 할 때마다 하나씩 만들어지고 작업이 끝나면 버려져야 한다.
        EntityManager em = emf.createEntityManager();
        // ! 모든 데이터베이스에 대한 변경 작업은 트랜잭션 안에서 일어나야 한다. (조회는 꼭 트랜잭션 안에서가 아니더라도 상관없다)
        // ! 하나의 트랜잭션에서 원하는 작업을 끝내고 그 트랜잭션안에서 커밋을 해줘야 변경이 적용된다.
        EntityTransaction tx = em.getTransaction();

        // ! Transaction 시작
        tx.begin();

        try {
            /* CREATE */
            /*Member member = new Member();
            member.setName("helloA");
            em.persist(member);*/

            /* READ */
            /*Member member = em.find(Member.class, 1L);
            System.out.println("member ID = " + member.getId());
            System.out.println("member Name = " + member.getName());*/

            /* UPDATE */
            // ! Update 에서는 persist() 호출하지 않아도 상관없이 그냥 변경할 거 변경하고 트랜잭션을 commit() 해주면 된다.
            // ! 그 이유는 JPA 이 녀석이 데이터베이스의 데이터를 컬렉션처럼 다루기 때문
            /*Member member = em.find(Member.class, 1L);
            member.setName("HelloB");*/

            /* DELETE */
            /*Member member = em.find(Member.class, 1L);
            em.remove(member);*/

            /* Entity 객체를 대상으로 쿼리를 날릴 수 있는 JPQL 이라는 녀석을 사용 / SQL 은 테이블을 대상으로 날리는 것 */
            List<Member> result = em.createQuery("select m from Member m", Member.class).getResultList();
            for (Member member : result) {
                System.out.println("member = " + member.getName());
            }

            tx.commit();
        } catch (Exception e) {
            tx.rollback();
        } finally {
            em.close();
            emf.close();
        }
    }
}

위 코드에서 간단한 CRUD를 실행해보았다. 한 줄 한 줄 이해해보자.

 

Persistence 클래스의 createEntityManagerFactory() 메소드를 호출하고 해당 메소드에 'hello' 라는 값을 지정한다. 이 값은 persistence.xml 파일에 name으로 지정한 값이다. 해당 속성값을 불러오겠다는 의미가 된다.

 

이제 팩토리에서 엔티티 매니저를 생성한다. 이 엔티티 매니저는 고객의 요청에 의해 필요한 데이터베이스 작업을 할 때 트랜잭션이라는 단위를 생성해내는데 고객의 요청 하나에 하나의 트랜잭션이 사용된다고 생각하면 된다. 즉, 요청 하나에 엔티티 매니저 하나가 생성된다는 뜻이다. 요청에 의해 필요한 작업이 다 끝나면 엔티티매니저는 종료되어야 한다. 커넥션 풀에 한계가 있기 때문에 잡고 있는 커넥션을 놓아준다고 생각하면 된다.

 

그렇게 엔티티 매니저가 생성되면, 엔티티 매니저로부터 트랜잭션을 가져올 수 있다. 이 트랜잭션을 가져와 시작하게 되면 이 때부터 모든 데이터 베이스의 변경 작업을 진행할 수 있게된다. 어떤 데이터베이스든 모든 데이터베이스의 대한 작업은 트랜잭션이라는 단위 안에서 일어난다.

 

CREATE

멤버를 생성하는 방법은 정말 간단하게 새로운 멤버 객체를 만들고 필요한 데이터를 추가한 후 엔티티매니저의 persist() 메소드에 만든 멤버를 추가해주면 된다. 그럼 끝이다.

 

READ

조회는 엔티티매니저의 find() 메소드를 호출한다. 여기서 원하는 객체 타입과 PK 정보를 던져주면 엔티티매니저가 데이터베이스에서 알맞은 데이터를 찾아낸다. 

 

UPDATE

데이터를 수정하는 방법은 조회한 후 해당 데이터를 변경해주기만 하면 된다. 이러고 persist() 메소드를 호출하지도 않는다. 이를 제대로 이해하려면 영속성 컨텍스트를 알아야 하는데 다음 파트에서 알아 볼 예정이다. 그러니까 쉽게 생각해서 자바에서 컬렉션을 다룰 때 컬렉션의 데이터를 꺼내 데이터를 변경하면 그 데이터를 다시 컬렉션에 추가하지 않는것처럼 똑같은 방식으로 동작한다고 생각해보자.

 

DELETE

엔티티매니저로부터 remove() 메소드를 호출하면 끝.

 

JPQL

그리고 엔티티매니저가 제공하는 기본 메소드 말고도 JPQL을 사용할 수 있는데 JPQL은 SQL과 유사하나, SQL은 테이블을 대상으로 한 쿼리이고 JPQL은 객체를 대상으로 하는 쿼리이다. 즉 JPQL을 통해 데이터를 객체로 가져온다고 생각하면 된다. 

 

 

트랜잭션 커밋

이렇게 모든 작업이 트랜잭션안에서 이루어지면 트랜잭션의 커밋을 반드시 해줘야한다. 커밋을 하지 않으면 데이터베이스에 변경 작업은 단 하나도 이루어지지 않는다. 그리고 커밋이 행해지기 전 어떤 오류가 있다면 트랜잭션 안에서 작업한 모든 작업을 롤백하는 트랜잭션 롤백이 있다. 다시 한 번 말하지만 모든 데이터베이스는 항상 트랜잭션이라는 단위안에서 데이터베이스에 대한 작업이 이루어진다.

 

 

이렇게 모든 작업을 끝내면 엔티티 매니저를 닫고, 엔티티 매니저 팩토리를 닫는다. 엔티티 매니저는 커넥션을 반납하는 개념이고 엔티티매니저 팩토리는 서비스가 종료될 때 닫으면 된다. 또한 서비스가 띄워질 때 딱 한 번만 생성된다. 

 

728x90
반응형
LIST