PPAK

[Spring/JPA] EntityManagerFactory, EntityManager, EntityTransaction 에 대해서 본문

spring

[Spring/JPA] EntityManagerFactory, EntityManager, EntityTransaction 에 대해서

PPakSang 2022. 7. 6. 00:57

 

 

이전 포스팅 에서 JPA 가 자바 진영의 ORM 기술 표준이라는 것과, 성능 최적화를 위해 내부적으로 영속성 컨텍스트(Persistence Context) 를 사용한다는 것을 알아보았다.

 

[Spring/JPA] JPA 란? (ORM/Persistence Context)

보편적으로 서비스가 구동되는 과정에서 데이터의 최종 저장소는 데이터베이스이다. 그 중에서도 관계형 데이터베이스는 우리가 보편적으로 사용하는 데이터베이스 모델이며 키(pk)를 통해 값

ppaksang.tistory.com

 

이번 포스팅에서는 JPA 를 실제로 사용하기 위한 환경설정과 어떤 흐름으로 DB 와의 연결을 가져오고, Query 를 전송하는지 알아보도록 하겠다.

 

개발환경 셋팅

Build 도구로는 Maven 을 선택하였다.

 

Java : 1.8

Hibernate : 5.3.10

H2 DBMS : 1.4.199

 

아래의 pom.xml (Project Object Model) 통해 의존성을 포함한 빌드 정보 확인

<?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>sample.ppaksang</groupId>
    <artifactId>jpa-prac</artifactId>
    <version>1.0-SNAPSHOT</version>
    
    <dependencies>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-entitymanager</artifactId>
            <version>5.3.10.Final</version>
        </dependency>
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <version>1.4.199</version>
        </dependency>
    </dependencies>

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

</project>

 

아래는 java 11 사용시 발생할 수 있는 오류를 제어해줄 api 의존성

<dependency>
    <groupId>javax.xml.bind</groupId>
     <artifactId>jaxb-api</artifactId>
    <version>2.3.0</version>
 </dependency>

다음으로는 Persistence 객체가 정상적으로 EntityManagerFactory 를 생성할 수 있도록 Meta Data 를 작성해주어야 한다.

 

프로젝트 내의 META-INF 하위 persistence.xml 를 통해

1. JDBC 의 속성(Driver, Username, PW, DB URL)

2. Hibernate 의 속성

을 추가할 수 있다.

 

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">
    <!--jpa 이름-->
    <persistence-unit name="ppaksang-persistence">
        <properties>
            <!-- 필수 속성, database 접근 정보
            javax 로 시작하는 설정은 java 표준-->
            <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/~/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="5"/>-->
            <property name="hibernate.hbm2ddl.auto" value="create" />
        </properties>
    </persistence-unit>
</persistence>

간략하게 설정을 살펴보면

 

persistence-unit 단위로 하나의 Persistence 객체의 Meta Data 를 명시가능하다

name 을 통해 Persistence 객체가 설정정보를 식별할 수 있다.

 

JPA 는 jdbc api 를 직접호출해주기 때문에 이에 대한 기본적인 정보가 명시되어야 한다.

 

Hibernate 같은 경우에는 자동생성된 sql 문을 출력하는 설정을 추가하였다.

dialect 의 경우에는 DBMS 별 상이한 sql 문법을 보정해주는 설정이다.

 

위와 같은 개발환경을 셋팅하고 나면, Persistence 객체는 persistence.xml 을 참고해서 EntityManagerFactory 를 생성한다.

 

기본적으로 EntityManagerFactory 는 애플리케이션 당 1개 생성하도록 설계된다.

 

EntityManagerFactory 는 연결된 설정정보에 명시된 데이터베이스와 연결하고, 클라이언트의 요청이 들어올 때 마다 connection pool 에 여유 connection 이 존재하는지 확인하고, EntityManager 를 생성해 connection 과 연결해준다.

EntityManager

데이터베이스의 테이블에 매핑되는 객체가 Entity 이다.

 

이러한 Entity 를 관리하는 기능을 수행하는 객체가 EntityManager 이고, EntityManager 는 요청 쓰레드 1개에만 제공될 수 있다.

 

EntityManager 는 DB 와의 실제 connection 을 가지고 Transaction 을 수행하기 때문에 여러 쓰레드가 공유하게되면 동시성 문제가 발생할 수 있다.

 

EntityTransaction

기본적으로 데이터베이스에 대한 접근은 Transaction 단위로 명령이 처리된다.

 

JPA 는 이러한 Transaction 단위로 처리되지않는 명령에 대해서는 TransactionRequiredException 을 일으킨다.

 

EntityManager 는 EntityTransaction 을 얻을 수 있으며, transaction 이 begin 되고 commit 되는 사이에 데이터베이스로의 접근이 가능하다

 

Transaction 내부에서 에러가 발생한 경우에는 꼭 rollback 을 수행해야한다.

 

Persistence Context

EntityManager 는 본인의 생명주기 동안 관리 가능한 Persistence Context 와 연결된다.

 

Persistence Context 내부에는 1차 캐시 역할을 하는 공간과, 쓰기 지연 SQL 저장소가 존재한다.

 

이전 포스팅에서 언급했듯 이러한 Persistence Context 는 JPA 의 성능을 최적화 하기 위해(DB connection 을 최소화 하기 위해) 고안되었다.

 

아래의 간단한 예시 코드를 통해 위에서 설명한 내용들을 정리해보자

 

 

import javax.persistence.*;

@Entity
public class Member {
    @Id @GeneratedValue
    private Long id;

    private String name;

    @ManyToOne
    @JoinColumn(name = "team_id")
    private Team team;

    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;
    }

    public Team getTeam() {
        return team;
    }

    public void setTeam(Team team) {
        this.team = team;
    }
}
import javax.persistence.*;
import java.util.List;

@Entity
public class Team {
    @Id @GeneratedValue
    private Long id;

    private String name;

    @OneToMany(mappedBy = "team")
    private List<Member> members;

    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;
    }

    public List<Member> getMembers() {
        return members;
    }

    public void setMembers(List<Member> members) {
        this.members = members;
    }
}

 

간단한 Member 와 Team Entity 를 생성하고, 하나의 Team 은 다수의 Member 를 가지는 연관관계를 맺는다.

 

실전에서는 setter 를 남용하지 않지만, 위에선 예제를 위해 모두 설정해준다.

 

public class JPATest {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("ppaksang-persistence");

        EntityManager em = emf.createEntityManager();

        EntityTransaction tx = em.getTransaction();
        tx.begin();

        try {
            Member a = new Member();
            a.setName("PPakSang");
            
            System.out.println("====");
            em.persist(a);
            System.out.println("====");
            
            tx.commit();
        } catch (Exception e) {
            tx.rollback();
        } finally {
            em.close();
        }

        emf.close();
    }
}

 

위에서 언급한

1. Persistence 객체를 통한 EntityManagerFactory 생성

2. EntityManager 생성

3. Transaction 획득

4. Transaction 내부에서 DB 접근 코드 작성 및 예외 발생 시 rollback 진행

 

를 모두 수행하였고, 한 가지 주의해야할 것은

꼭 마지막에 em.close() 를 통해서 EntityManager 가 가지고 있는 DB connection 을 반환할 수 있도록 해야한다.

 

위의 예제를 통해 정상적으로 데이터베이스에 쿼리가 날아감을 확인할 수 있다.

 

한 가지 흥미로운 점은 insert 쿼리가 날아간 시점인데, 두 ==== 가 모두 출력되고 나서 전송됨을 알 수 있다

 

여기서 em 이 쓰기 지연 SQL 저장소를 사용하고 있다는 것을 알 수 있다.

 

엄밀히 이야기하자면 Id 를 생성하는 strategy 가 Sequence(DBMS 의 Sequence 와 같음) 이기 때문에, 현재 sequence number 를 받아왔기 때문에 쓰기 지연이 가능하다 (allocation size 까지는 id 가 확보가 되기 때문)

 

strategy 를 IDENTITY (auto_increment) 로 설정한다면, insert 쿼리 생성시 곧바로 날아감을 알 수 있다. (매 insert query 마다 id 값을 얻어와야하기 때문)

 

실제로 쿼리 생성을 호출(em.persist()) 하고 나서, 실제로 쿼리가 나가는 시점은 em.flush() 가 호출될 때 이다.

위의 경우(IDENTITY) 에는 아주 예외적으로 있는 방식이고, 보통의 경우에는 Transaction 이 commit 되는 시점에 em.flush 가 자동적으로 호출된다.

 

다시 아래의 코드를 살펴보자

 

public class JPATest {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("ppaksang-persistence");

        EntityManager em = emf.createEntityManager();

        EntityTransaction tx = em.getTransaction();
        tx.begin();

        try {
            Team team = new Team();
            team.setName("Team1");
            em.persist(team);

            Member member = new Member();
            member.setName("PPakSang");
            member.setTeam(team);

            em.persist(member);
  
            System.out.println(em.find(Member.class, member.getId()).getName());
            System.out.println(em.find(Team.class, team.getId()).getName());
            
            tx.commit();
        } catch (Exception e) {
            tx.rollback();
        } finally {
            em.close();
        }

        emf.close();
    }
}

위의 코드에서 insert, select 쿼리는 총 몇 번 날아갈까?

persist 2번에 find 2 번이니 총 4번이 날아갈까?

 

정답은 2번이다. 왜 그런지 이유가 궁금하다면 이전 JPA 포스팅에서 1차 캐시가 운용되는 방식을 참고한다면 이해할 수 있을 것이다.

 

최종적으로 insert 쿼리가 2번 나가게 되고, 1차 캐시에 저장된 두 Entity 객체를 id 를 통해서 조회하기 때문에 별도로 select query 가 나갈 필요가 없어진다.

 

잘못된 정보가 있다면 댓글로 알려주시면 감사하겠습니다!!

Comments