본문 바로가기
서버/JPA

[JPA] 영속성 컨텍스트

by 베어 그릴스 2022. 10. 29.
320x100

*본 게시글은 김영한님의 자바 ORM JPA 표준 책을 보고 이해한 내용을 바탕으로 정리한 글입니다.

 

 

JPA가 제공하는 기능은 크게 엔티티와 데이블을 매핑하는 설계 부분과 매핑한 엔티티를 실제 사용하는 부분으로 나눌 수 있다.

 

매핑한 엔티티를 엔티티 매니저를 통해 영속성 컨텍스트와 데이터베이스에 CRUD(create, read, update, delete)하는 등 엔티티와 관련된 모든 일을 처리한다.

 

 

엔티티 매니저 팩토리와 엔티티 매니저

엔티티 매니저는 엔티티 매니저 팩토리를 통해 만들어낼 수 있다.

 

엔티티 매니저 팩토리 생성은 비용이 아주 많이 들고, 엔티티 매니저 팩토리는 애플리케이션을 개발할 때 굳이 여러 개 있을 필요가 없기 때문에 하나만 생성해야 한다.

 

즉, 싱글톤 객체로 관리해야 한다.

//영속성 컨텍스트 생성, 비용이 아주 많이 든다.
// /resources/META-INF/persistence.xml 의 JPA 설정 정보를 읽어와서 설정한다.
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");

여담으로, 스프링 부트를 사용하면 스프링 부트의 application.~의 설정 파일에 JPA 설정을 해주면 스프링 부트 자체적으로 엔티티 매니저 팩토리를 관리하여 @PersistenceContext 어노테이션으로 엔티티 매니저를 생성할 수 있다.

 

반면, 엔티티 매니저는 생성할 때 비용이 거의 들지 않는다.

EntityManager em = emf.createEntityManager();

엔티티 매니저는 보통 다수 생성되고, 엔티티 매니저는 데이터베이스 연결이 꼭 필요한 시점까지 커넥션을 얻지 않는다. (보통, 트랜잭션을 시작할 때 커넥션을 휙득)

 

 

엔티티 매니저 팩토리는 여러 스레드가 동시에 접근해도 안전하므로 서로 다른 스레드 간에 공유해도 되지만, 엔티티 매니저는 여러 스레드가 동시에 접근하면 동시성 문제가 발생하므로 스레드 간에 절대 공유하면 안된다.

 

JPA 구현체들은 EntityManagerFactory를 생성할 때 커넥션 풀(스레드 풀과 같이 n개의 커넥션이 동시에 디비에 접근 가능하게 만드는 것)도 만드는데 이는 J2SE 환경에서 사용하는 방법이다.

 

*참고 : JPA를 J2EE에서 사용하면 해당 컨테이너가 제공하는 데이터소스를 사용한다.

 

 

영속성 컨텍스트

JPA를 이해하는데 가장 중요한 용어이다.

 

엔티티 매니저로 엔티티를 저장하거나 조회하면 엔티티 매니저는 영속성 컨텍스트에 엔티티를 보관하고 저장한다.

//member 데이터베이스에 저장
em.persist(member);

persist 메서드를 통해 회원 엔티티를 영속성 컨텍스트에 저장할 수 있다.

 

영속성 컨텍스트는 논리적인 개념으로 눈에 보이지 않는다.

 

*참고 : J2SE 환경에서는 엔티티 매니저와 영속성 컨텍스트가 1:1로 생성되고, J2EE 환경(스프링 프레임워크 같은 컨테이너 환경)에서는 이 N:1로 생성된다.

 

엔티티의 생명 주기

엔티티의 생명주기

엔티티에는 4가지 생명주기가 있다.

  • 비영속(new) : 영속성 컨텍스트와 전혀 관계가 없는 상태.
    그저 생성한 객체 상태일 때 비영속 상태라고 한다.

  • 영속(managed) : 영속성 컨텍스트에 저장된 상태
    ex) em.persist(member), em.find() 혹은 jpql을 통해 데이터베이스에서 entity를 조회하여도 영속성 컨텍스트에 저장된다. 

  • 준영속(detached) : 영속성 컨텍스트에 저장되었다가 분리된 상태
    ex) em.detach(member) 영속성 컨텍스트에서만 사라진다.
  • 삭제(removed) : 삭제된 상태
    ex) em.remove(member) 데이터베이스와 영속성 컨텍스트 둘 다에서 사라진다.

 

영속성 컨텍스트에 데이터를 저장하면 이 엔티티는 언제 데이터베이스에 저장될까? JPA는 보통 트랜잭션을 커밋하는 순간 영속성 컨텍스트에 새로 저장된 엔티티를 데이터베이스에 반영하는데 이를 플러시라고 한다.

 

*주의 : 영속성 컨텍스트는 엔티티를 식별자 값(@Id로 테이블의 기본 키와 매핑한 값)으로 구분하므로, 식별자 값이 무조건 있어야 한다.

 

엔티티를 바로 데이터베이스에 저장하면 되는데 왜 영속성 컨텍스트를 거치는 걸까?

 

영속성 컨텍스트의 장점을 하나하나 알아보자.

 

 

1차 캐시

영속성 컨텍스트는 내부에 캐시를 가지고 있는데, 이를 1차 캐시라고 한다.

(쉽게 생각하면 내부에 1차 캐시라는 Map이 있고, Id를 key로 하여 영속 상태의 엔티티들이 캐싱되어있다는 것이다.)

위에 4가지 생명 주기에서 설명했지만, 데이터베이스에 저장하거나 조회할 때 엔티티를 영속성 컨텍스트에도 저장한다.

다시 한번 똑같은 엔티티가 조회될 때 이미 영속성 컨텍스트에 해당 엔티티가 존재하므로 데이터베이스에 조회할 필요 없이 1차 캐시에서 가져올 수 있다.

 

즉, 성능상 엄청난 이점을 얻을 수 있다.

 

1차 캐시는 인스턴스의 동일성을 보장해준다.

Member a = em.find(Member.class, "member1"); 
Member b = em.find(Member.class, "member1");
System.out.println(a == b); //동일성 비교 true

find를 반복 호출해도 1차 캐시에 저장되어 있는 같은 엔티티 인스턴스를 반환하기 때문에 동일성을 보장해준다.

 

즉, 1차 캐시를 통해 REPEATABLE READ 등급의 트랜잭션 격리 수준을 애플리케이션 차원에서 제공해준다.

 

*동일성 vs 동등성

동일성 : 실제 인스턴스가 같은 것.

동등성 : 인스턴스는 다를 수 있지만, 인스턴스 내부의 값이 같은 것

 

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

 

persist()가 들어오면 우선 sql문을 생성하는데, 바로 데이터베이스에 쿼리를 보내는 게 아니라 우선 쓰기 지연 SQL 저장소에 저장하고, 1차 캐시에만 엔티티를 저장한다. 이후, 트랜잭션 커밋이 될 때 sql 저장소에 있는 쿼리를 한 번에 DB에 보낸다.

플러시는 영속성 컨텍스트의 변경 내용을 데이터베이스에 동기화하는 작업인데 이때 등록, 수정, 삭제한 엔티티를 데이터베이스에 반영한다. 이렇게 영속성 컨텍스트의 변경 내용을 데이터베이스에 완벽히 동기화한 후에 실제 데이터베이스 트랜잭션을 커밋한다.

 

플러시 되는 경우 3가지

  • 직접 em.flush()
  • 트랜잭션 커밋
  • JPQL 쿼리 실행 ( 영속성 컨텍스트에 있는 내용도 DB에 동기화 해주어야만 해당 쿼리에도 원하는 결과를 받을 수 있기 때문에 플러시 된다.)

플러시 해도 영속성 컨텍스트는 계속 유지된다.

 

중간중간 쿼리를 보내고 맨 마지막에 커밋하는 것이나, 쿼리를 모아 두고 한 번에 보내어 커밋하는 것이나 결과는 같기 때문에 쓰기 지연이 가능해진다.,

 

이 기능을 잘 활용하면 모아둔 등록 쿼리를 데이터베이스에 한 번에 전달해서 성능을 최적화할 수 있다.

 

*참고 : find 시에 영속성 컨텍스트에 엔티티가 없다면 당연하겠지만 자동으로 select 쿼리가 데이터베이스에 전달 된다.(주의!! 중요!! 쓰기 지연 sql문들이 전부 나가는 것이 아니다. select문만 나간다. 즉, 영속성 컨텍스트에만 있고 데이터베이스에는 값이 아직 없는 엔티티를 조회할 때 조심해야한다.) 이때, 성능을 좀 더 최적화하기 위해 지연 로딩 전략을 사용할 수 있다. 우선 연관관계에 있는 엔티티들을 프록시 객체를 받아두고, 실제로 사용될 때 DB에서 조회하여 실제 데이터들을 넣어주는 것이다. 보통 모든 엔티티에 지연 로딩 전략을 사용하는 것이 좋은데, join으로 얽히고설킨 엔티티를 조회했다가 엄청난 성능 이슈가 발생할 수 있기 때문이다. 지연 로딩 전략은 매우 중요하기 때문에 추후 다루도록 하겠다. 

 

변경 감지(Dirty check)

보통 sql문으로 update 쿼리를 작성할 때 회원의 이름을 수정해야 하면 name =~ 쿼리를 하나 작성하고, 회원의 등급을 수정해야하면 grade = ~ 로 작성하여 수정 쿼리가 많아지고 비즈니스 로직을 분석하기 위해 SQL을 계속 확인해야 한다.

결국, 비즈니스 로직이 SQL에 의존하게 된다.

 

그러나 JPA에서는 별다른 수정 문 없이 영속성 컨텍스트에서 조회해서 데이터를 변경하면 된다.

 

이후 flush가 발생하면 현재 엔티티와 영속성 컨텍스트의 스냅샷과 비교하여 수정 쿼리를 생성해서 쓰기 지연 저장소에 보내고 SQL이 데이터베이스에 보내진다.

 

변경 감지는 영속 상태의 엔티티에만 적용된다.

 

JPA의 기본 전략은 엔티티의 모든 필드를 업데이트하는 sql문을 작성하는 것이다.

 

이렇게 하면 데이터 전송량이 증가하는 단점이 있지만,

수정 쿼리가 항상 같아서 애플리케이션 로딩 시점에 수정 쿼리를 미리 생성해두고 재상용 할 수 있고, 동일 쿼리를 보내면 데이터베이스가 이전에 한 번 파싱된 쿼리를 재사용할 수 있는 장점이 있어 해당 전략을 사용한다.

 

*참고 : 보통 컬럼이 30개 이상 넘어가면 해당 전략이 아닌 동적 수정 쿼리가 더 빠르다고 한다.

 

 

위에서 설명한 엔티티 매니저와 영속성 컨텍스트는 매핑한 엔티티를 실제 사용하는 동적인 부분에 해당한다.

 

위 개념은 JPA를 다루려면 필수적인 개념이기에 꼭 이해하고 넘어가도록 하자🚗

 

728x90