JPA 영속성 컨텍스트

Posted by sJun on November 02, 2020 ·

JPA를 사용하기로 마음먹었다면 가장 먼저 알아야 할 것이 영속성 컨텍스트 라고 생각합니다.

영어로 번역하면 Persistence Context 입니다.

먼저 영속성 컨텍스트를 이해하기 위해서는 JPA의 EntityManagerFactory와 EntityManager 를 알아야합니다.

EntityManagerFactory emf = Persistence.createEntityManagerFactory("name);

EntityManager em = emf.createEntityManager();


사실 영속성 컨텍스트 같은 경우 논리적인 개념으로 실제 사용자가 볼 수 있는 것은 아닙니다.

즉 영속성 컨텍스트를 사용하기 위해선 그림에서 볼 수 있듯이 EntityManger를 사용해야합니다.

이 EntityMagager를 통해서 영속성 컨텍스트에 접근할 수 있게 됩니다.

하지만 EntityManger 도 자신의 인스턴스를 관리해줄 곳이 필요한데 그런 공간을 만들어 주는 곳이 EntityManagerFactory 입니다.

EntityMangerFactory 생성 시 커넥션 풀도 만들어 줍니다.

그리고 Factory같은 경우는 한번 생성하는데 비용이 많이 들기 때문에 한번 생성 후 애플리케이션 전체에 공유됩니다.

이러한 Entity의 생명 주기에는 4가지가 있는데 차례대로 비영속 / 영속 / 준영속 / 삭제 가 있습니다.

위에서 가장 애매한 부분이 준영속 부분인데 이 부분은 이론적인 설명만으로는 이해하기 힘든 부분이 있어 나중에 예제 코드로 설명하겠습니다.

@Getter @Setter // Lombok Getter / Setter 자동 생성
class Member(){
    @Id
    private Long id; 
    private String name;
}

...

Member member = new Member();
member.setId(1L);
member.setName("kim");

JPA는 클래스에서 DB로 자동 변환을 해주지만 PK는 사용자가 설정해줘야합니다.

간단하게 PK로 사용할 필드위에 Id 애노테이션을 사용하면 됩니다.

그 후 멤버 다른 곳에서 Member 객체를 생성한 코드입니다.

이 때 단순 객체를 생성한 상태를 비영속 이라고 합니다.

여기서 코드를 하나 추가합니다.

EntityManager em = emf.createEntityManager();
em.getTransaction().begin();

em.persist(member);
transaction.commit();

첫째 줄은 단순히 EntityManger를 Factory로부터 생성, 둘째 줄은 트랜잭션 작업을 시작한다는 뜻입니다.

여기서 가장 중요한 것은 트랜잭션 입니다

JPA는 트랜잭션이라는 작업 단위를 통해서 동기화가 되기 때문에 실제로 트랜잭션을 작업 단위를 지정해주지않으면 에러가 발생 할 수 있습니다.

이제 em.persist를 통해서 member 객체를 넣어줍니다. em.persist가 쿼리문으로 따지면 DB에 저장하는 INSERT 문과 비슷합니다.

제가 왜 비슷하다고 했냐면 em.persist가 이루어진 시점에서 member 객체를 바로 DB에 저장하는 게 아니라 위에서 언급드렸던 영속성 컨텍스트 의 관리 대상이 되는 것입니다.

그 후 밑의 commit() 코드를 만나게 되면 영속성 컨텍스트가 DB로 직접 쿼리문을 보내줍니다.

즉 persist 자체는 객체를 직접적으로 DB에 넣지않습니다

정리하면 DB와 JPA 사이에 중간 다리가 하나 놓여있다고 머릿속으로 그림을 그려보시면 됩니다.

그럼 의문이 하나 생깁니다

귀찮게 중간다리는 왜 놓는거야?

사실 데이터를 DB로 보내지 않고 영속성 컨텍스트에서 1차적으로 관리하는 행위는 여러가지 이점이 있습니다만..

가장 대표적인 것은 ‘변경 감지’라는 것입니다.

JPA로 데이터를 수정하려면 다음 코드를 보시면 됩니다

Member member1 = em.find(Member.class, 1L); // 조회
member1.setName("park");

em.persist(member1) // ???

transaction.commit();

여기서 보시면 단순히 member1 객체를 id값을 통해 찾은 다음 해당 이름을 “kim” 에서 “park”으로 변경하고자 합니다.

그럼 park으로 이름을 바꿨는데 다시 그 객체를 persist 해줘야하지 않을까? 이런 의문이 드는데 결론은

안해도 됩니다

위 이미지를 보시면 EntityManger 내부에 1차 캐시라는 공간이 있습니다.

1차 캐시 내부 공간에서 만약 사용자가 em.persist를 하면 스냅샷으로 해당 객체를 저장해둡니다.

그 후

member.setName('park')

와 같은 코드로 setter를 호출해서 데이터를 바꿀 때 기존에 있던 스냅 샷과 바꾼 Member 객체 간 데이터를 비교해서 다르다면 Update 쿼리문을 자동으로 날려줍니다.

flush()라는 명령어가 있는데 flush가 발생하면 변경을 감지하고 수정된 엔티티를 쓰기 지연 SQL 저장소에 넣거나 해당 저장소에 있는 SQL 문을 DB로 쏴주는 역할을 합니다.

주의하실 점은 flush가 영속성 컨텍스트를 비우는 명령어같지만 절대 비우지 않습니다.

단순히 컨텍스트 변경 내용을 DB에 동기화 할 뿐입니다.


이 외의 영속성 컨텍스트를 사용하는 것의 이점에는 변경 감지를 제외하고도

1차 캐시를 이용해서 조회를 여러번 할 경우 INSERT 문을 여러번 호출하는 것 대신 캐시에서 계속 꺼내쓸 수 있기 때문에 조회 성능이 약간은 상승할 수 있다는 점과

영속성 컨텍스트의 ‘쓰기 지연’과 같은 이점 때문에 변경 감지도 가능한 점, EntityManager에서 같은 객체를 불렀을 때 동일성을 보장한다는 점이 있습니다.

예를 들면 이렇겠네요

Member a = em.find(Member.class, "memberA");
member b = em.find(Member.class, "memberB");

a == b ?? // true
// 기존의 다른 DB 연동 프레임워크 방식은 불러오면 SELECT 문을 각각 호출해 가져오므로 false 가 나온다.

오늘 내용의 중요한 점을 축약하면 이렇습니다.


● JPA는 항상 트랜잭션 단위로 작동한다. 즉 commit()이 중요하다.

● em.persist는 DB의 INSERT문과 유사하다곤 볼 수 있으나 영속성 컨텍스트를 거친 후에 commit을 만나면 DB로 쿼리문을 보낸다.

● 영속성 컨텍스트의 쓰기 지연, 변경 감지로 인해 DB 데이터의 수정을 Java의 컬렉션에 데이터를 수정하듯이 사용이 가능하다.

즉 수정하고 em.persist를 다시 해줄 필요가 없다.