프로젝트/기술적 선택

[기술적 선택] JPA 구동 방식과 영속성 컨텍스트

배발자 2023. 1. 5.
반응형

구동방식

오늘 포스팅은 JPA 구동방식에 대해 간단하게 설명을 하고 어떤 객체들을 생성을 해서 트랜잭션이 이루어지는지 확인해보려고 한다. JPA는 자바 애플리케이션과 JDBC API 사이에서 동작이 된다. 즉, 개발자가 JPA에게 명령하면 JPA가 JDBC API를 사용해서 SQL을 호출하고 결과를 받아서 동작하는 것이다. 

 

  1. Persistence 클래스에서 META-INF/persistence.xml 에서 설정 정보를 읽는다. 
  2. EntityManagerFactory를 만든다. 
  3. EntityManager를 필요할 때마다 생성해서 JPA를 동작한다. 

 

 

먼저, Entity라는 것 DB의 테이블과 매칭이 되는 개념이라고 보면 된다. 예를들면, Member 테이블에 id와 name 이라는 컬럼이 존재한다고 가정해보자. 그렇다면 스프링 환경에서 다음과 같은 클래스를 만들어 줄 수 있다. 

 

[Member.class]

package hellojpa;

import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
//jpa가 처음 로딩될 때 jpa 를 사용하는 애구나.
// 관리해야겟구나
@Table(name = "MEMBER")
//테이블 이름 MEMBER로 해야한다.
public class Member {


    @Id   //pk 값 작업
    private Long id;

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

 

 

이렇게 테이블과 연관된 컬럼의 필드로 구성되어 테이블 매핑이 되는 클래스를 엔티티 클래스라고 말을 한다. 즉, 해당 클래스에서 생성된 엔티티 객체와 데이터를 주고 받는 것이다.  

 

[EntityManager] 

Entity를 관리하는 역할을 수행하는 클래스라고 보면 된다. EntityManager 내부에는 영속성 컨텍스트라는 것을 둬서 엔티티들을 관리하게 되는데 추후 영속성 컨텍스트라는 것을 업로드 할 예정이다. 암튼! EntityManager의 역할은 아래와 같다. 

 

  • JPA의 기능 대부분 EntityManager가 제공 
  • EntityManager를 사용해서 엔티티를 DB에 CRUD 할 수 있다. 
  • DB 커넥션을 유지하면서 DB와 통신할 정도로 밀접한 관계가 있으므로 쓰레드 간에 공유 또는 재사용하면 안된다

 

[EntityManagerFactory]

위에서 언급한 EntityManager에서는 쓰레드 간에 공유 또는 재사용을 하면 안된다고 말했다. 멀티 코어에서 멀티 스레드를 구현할 때 각 스레드들은 동시에 동작하게 된다. 그러므로 A 스레드가 데이터를 수정하고 있는데 B 스레드에서 해당 데이터를 수정해버리면 안된다. 동시성 이슈가 발생할 수 있으므로 EntityManager 하나를 공유하면 안되고 계속해서 만들어줘야한다. 즉, EntityManagerFactory는 EntityManager를 만들어내는 공장 역할을 한다.  

 

* 주의할 점은 트랜잭션 없이 데이터를 변경하면 예외가 발생하기 때문에 JPA는 항상 트랜잭션 안에서 데이터를 변경해야한다. EntityManager에서 트랜잭션 API를 받아서 사용을 하고 변경 다하고 끝에 커밋을 하고 예외는 롤백을 한다. 

 

 

 

JPA 는 앱 실행과 동시에 한 DB당 하나의 EntityManagerFactory를 생성을 한다. WAS 가 종료되는 시점에 EntityManagerFactory는 사라진다. 이제 DB에 Insert 하는 코드를 읽어보고 주석과 같이 이해하면 좋을 거 같다.

 

테스트 

[삽입]

package hellojpa;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;

public class JpaMain {


    public static void main(String[] args) {

        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        // <persistence-unit name="hello"> 설정 정보 
        
        EntityManager em = emf.createEntityManager();
        // EntityManger 생성 ->  정말 쉽게 말하면 데이터베이스 커넥션을 받아왔다.  

        EntityTransaction tx = em.getTransaction();
        // 트랜잭션을 얻어와야돼 
        
        
        tx.begin();

        try {
        
            Member member = new Member();
            // 객체 생성

            member.setId(1L);
            // ID 1 
            member.setName("HelloA");
            // name : "HelloA"
            
            em.persist(member);
            // 영속성 컨텍스트 관련 부분이라 추후 업로드 
            
            tx.commit();
            //커밋
            
        }
        catch (Exception e){
        
            tx.rollback();
            
        }finally{
        
            em.close();
            //em 종료 
        }
        
        emf.close();
		// emf 종료 
    }
}

 

[조회]

try {
    Member findMember = em.find(Member.class, 1L);
    // id 가 1인 entity DB에서 들고와서 Member 객체에 맵핑
    
    System.out.println("findMember = " + findMember.getId());
    System.out.println("findMember.name = "+ findMember.getName());

    tx.commit();
}

 

[수정]

try {
    Member findMember = em.find(Member.class, 1L);
    findMember.setName("HelloJPA");

    tx.commit();
}

 

JPA를 통해서 Entity 를 가져오면 JPA에서 관리를 해주는데, JPA가 뭔가 변경이 됐는지 안됐는지 트랜잭션을 커밋하는 시점에서 관리를 해준다. 트랜잭션이 시작이 되고 특정 행에 대한 정보를 들고와 객체로 매핑하고 마치 컬렉션을 다루듯이 해당 데이터를 관리한다. 그리고 데이터의 수정이 발생하면 수정된 데이터를 자바 환경에서 들고있다가 comit() 코드에서 실제로 업데이트 쿼리가 생성되어 DB에 변경 사항을 commit 하게 된다. 이 부분은 영속성 컨텍스트의 1차 캐시 관련된 부분을 알아야하기 때문에 바로 영속성 컨텍스트에 대해서 학습해보자. 

 

영속성 컨텍스트

JPA에서 가장 중요한 2가지가 있다. 첫번째는 관계형 데이터베이스에 존재하는 데이터를 객체와 매핑하는 것이다. 

두번째는 지금 포스팅 하는 영속성 컨텍스트에 관한 것이다. 영속성 컨텍스트는 엔티티를 영구 저장하는 환경이라는 뜻이다. 위의 글에서 객체를 삽입하는 코드에서 "EntityManager.persist(entity); " 라는 것이 있다. 이 코드가 영속성 컨텍스트와 관련이 있다. 짧게 말하면 "엔티티를 영구 저장하는 환경" 이라는 정도만 익혀두자. 

 

엔티티의 생명주기 

  • 비영속(new/transient): 영속성 컨텍스트와 전혀 관계가 없는 상태
  • 영속(managed): 영속성 컨텍스트에 저장된 상태
  • 준영속(detached): 영속성 컨텍스트에 저장되었다가 분리된 상태
  • 삭제(removed): 삭제된 상태

 

JPA 세상에는 영속 컨텍스트라는 것이 있다. 이게 어떤 놈이냐? 

 

//객체를 생성한 상태(비영속)
Member member = new Member();
member.setId("member1");
member.setUsername("회원1");

 

자바 세상에서 이런 식으로 코드를 짰다면 객체를 생성하고 데이터만 주입하였다. 즉, 영속 컨텍스트를 전혀 활용하지 않았다는 뜻이다. 영속 컨텍스트를 전혀 활용하지 않은 상태 비영속이라고 한다. 

 

//객체를 생성한 상태(비영속)
Member member = new Member();
member.setId("member1");
member.setUsername(“회원1”);

//객체를 저장한 상태(영속)
em.persist(member);

 

바로 위의 코드에서 이전 시간에 배웠던 em.persist(memeber) 를 적용해보았다. 그러면 영속 컨텍스트가 어떻게 변하냐? 

 

 

 

member라는 객체가 영속 컨텍스트라는 통 안에 들어가있다. 이 상태를 영속 상태라고 부른다. 

//회원 엔티티를 영속성 컨텍스트에서 분리, 준영속 상태
em.detach(member);

 

만약 이렇게 detach 라는 메소드를 쓰면 영속성 컨텍스트에서 분리한다는 코드다. 즉, 더 이상 영속 컨텍스트에서 관리하지 않는다는 뜻이기에 준영속이라고 말을 한다. 

 

//객체를 삭제한 상태(삭제)
em.remove(member);

 

remove 라는 메소드를 활용하면 객체를 삭제한 상태가 된다. 지금까지 비영속, 영속, 준영속, 삭제라는 개념을 알아두었다. 쉽다. 그렇다면 영속성 컨텍스트의 이점이라는 것이 대체 뭐가 있을까 생각해봐야한다. 가장 큰 이점은 1차 캐시, 동일성 보장, 트랜 잭션을 지원하는 쓰기 지연이다. 

 

1차 캐시 

영속 컨텍스트 안에는 1차 캐시라는 놈이 있다. 위에서 보여줬던 영속 컨텍스트를 한 번 확대해보면 사실상 1차 캐시라는 놈한테 들어간다. 

 

//엔티티를 생성한 상태(비영속)
Member member = new Member();
member.setId("member1");
member.setUsername("회원1");
//엔티티를 영속
em.persist(member);

 

 

member 객체를 em.persist 를 하면 member 의  id (PK)를 가지는 key 값과  member 객체 자체 value로 가지고 MAP 구조로 들어가게 된다. 그렇게 된다면 id 값을 알고 있다면 바로 객체를 들고 올 수 있다. 여기서 중요한 것은 실제로 DB에 접근을 하지 않아도 된다는 것이다. 이게 무슨 말이냐? 

 

만약 영속 컨텍스트에 pk 값이 "member1" 이라는 놈이 저장되어있다면, DB 공간에 가서 "member1" 을 PK 로 갖는 MEMBER 테이블에 접근하지 않아도 스프링 환경의 영속 컨텍스트에 저장되어 있으니 DB에 접근하지 않아도 바로 들고와진다는 점! 그게 1차 캐시의 위력이다.  그럼 어떻게 조회하느냐? 

 

Member member = new Member();
member.setId("member1");
member.setUsername("회원1");
//1차 캐시에 저장됨
em.persist(member);
//1차 캐시에서 조회
Member findMember = em.find(Member.class, "member1");

 

 

find 메서드를 이용해서 PK 값으로 바로 찾아내면 된다. 그렇다면 em.persist를 하면 영속 컨텍스트에 객체가 들어간다는 것을 알았다. DB에서 실제 저장된 member 를 들고 오고 싶을 때 어떤 현상이 발생하는지도 봐야한다. 

 

무슨 말이냐면 지금까지 스프링 환경에서 객체를 생성해 데이터를 주입하고 영속 컨텍스트에 넣기만 했다. 그렇다면 실제 DB 세상에 존재하는 개체를 끌고올 때는 어떻게 동작하느냐?  

 

현재 영속 컨텍스트는 "member1" 을 PK 로 가지는 객체 하나가 들어있다. 여기서 "member2" 를 PK 로 가지는 객체를 들고 오고 싶다. 하지만 현재 영속 컨텍스트는 "member2"를 PK 로 가지는 객체는 없다. 

 

Member findMember2 = em.find(Member.class, "member2");

 

 

먼저 "member2" 라는 PK 를 가지는 key값이 있는지 찾아본다. 없다. 그러면 DB에 가서 해당 데이터를 들고와서 객체에 매핑 시키고 동시에 1차 캐시에 저장해버리는거다. 그럼 다시, 현재 영속 컨텍스트에 아무것도 없다고 생각해보자. 

 

Member findMember2 = em.find(Member.class, "member2");
Member findMember22 = em.find(Member.class, "member2");

 

 

위와 같이 member2 를 찾는 코드를 두개를 작성한다면 첫번째 줄에선 실제 DB에 select 쿼리가 날려질테고 두번째 코드에선 영속 컨텍스트에서 바로 들고오기 때문에 쿼리문은 수행되지 않는다. 즉, 쿼리는 한 번 실행된다. 

 

영속 엔티티의 동일성 보장 

JPA 를 활용하지 않고 쿼리문을 직접 날려서 member 객체를 받아오면 매번 new 키워드를 활용하여 새로운 객체를 할당해줬다. 즉, DB에서는 같은 놈을 던져주어도 Spring 환경에선 매번 인스턴스를 생성해주기 때문에 같은 데이터를 가진 놈이라도 다른 놈이라고 판단되는 것이다. 하지만 영속 컨텍스트 안에서는 마치 컬렉션처럼 수행된다는 것이다. 

 

Member a = em.find(Member.class, "member1");
Member b = em.find(Member.class, "member1");

System.out.println(a == b); //동일성 비교 true

 

 

위 코드를 보면 "member1" PK 를 갖는 놈을 영속 컨텍스트에 찾으면 같은 주소를 갖는 동일한 객체를 반환해준다는 뜻이다. 마치 컬렉션을 다루듯이. 

 

트랜잭션을 지원하는 쓰기 지연 

EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
//엔티티 매니저는 데이터 변경시 트랜잭션을 시작해야 한다.
transaction.begin(); // [트랜잭션] 시작
em.persist(memberA);
em.persist(memberB);
//여기까지 INSERT SQL을 데이터베이스에 보내지 않는다.
//커밋하는 순간 데이터베이스에 INSERT SQL을 보낸다.
transaction.commit(); // [트랜잭션] 커밋

 

em.persist (memberA)와 em.persist(memberB)를 코드를 작성한다면, 쓰기 지연 SQL 저장소라는 것이 있다. 

그러면 쓰기 지연 저장소라는 곳에 다음과 같은 쿼리문들이 저장된다. 

 

insert into MEMBER vlalues (memberA의 정보들) ; 
insert into MEMBER vlalues (memberB의 정보들) ;

 

그리고 트랜잭션 commit() 을 수행하면 그때서야 쓰기 지연 SQL 저장소에 있던 쿼리문들이 DB로 날려진다. 

 

반응형

댓글