카테고리 없음

JPA 영속성 컨텍스트 entityManager

최동훈1 2024. 1. 26. 20:32

JPA는 애플리케이션과 DB 사이에 있는 기술이다. 사이에 있음으로써 얻게되는 이점은 명확하다.

또한 가장 중요한 점은 JPA의 모든 함수의 동작은 트랜잭션 안에서만 동작한다는 것이다.

 

1. 버퍼링 기능이 가능하다.(묶어놓고 트랜잭션이 끝날때 한꺼번에 DB로 쿼리를 날릴수 있다.
2. 캐시가 가능하다.(우선적으로 영속성 컨텍스트내 1차캐시에서 조회를 하고, 없으면 DB에 조회해서 캐시에 저장한다.)

 

트랜잭션이 커밋된 후에 쿼리가 나가는 모습이다.

 

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 가 나갈 필요가 없어진다.