[JPA] 영속성 컨텍스트(2)

5 분 소요

영속성 컨텍스트는 왜 사용하는 걸까?

저번 게시물에서는 영속성 컨텍스트를 어떻게 사용하며, 무엇인지에 대해 배워보았습니다.

그럼 바로 데이터베이스에 저장을 하면 되지, 왜 굳이 영속성이라는 개념을 만들어서 모아뒀다가 commit()을 만나면 한번에 처리할까요?

영속성을 사용하면, 바로 데이터베이스에 저장하는것 보다 훨씬 안전하고 유용한 기능들을 사용할 수 있기 때문입니다.

그럼 한번 어떤 장점들이 생기는지 알아봅시다.

영속성 컨텍스트의 장점

1차 캐시

영속성 컨텍스트에 올라간 객체는 1차캐시 상태가 되기 때문에 우리가 만약 이 객체를 다시 조회한다면, 데이터베이스에 접근하지 않고 캐시를 사용해서 가져올 수 있습니다.

아래와 같이 코드를 작성해보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
 public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();

        tx.begin();

        try {


            Member member = new Member();
            member.setId(1L);
            member.setName("이민재");
            member.setAge("26");


            em.persist(member);
            Member member1 = em.find(Member.class, 1L);
            System.out.println("member1 = " + member1.getId());
            System.out.println("member1 = " + member1.getName());

            tx.commit();
        } catch (Exception e) {
            tx.rollback();
        } finally {
            em.close();
        }
        emf.close();
    }

코드를 실행시키고 로그를 확인해 봅시다.

image1

쿼리가 날아갔는데, member1을 조회할 때 SELECT 쿼리가 날아가지 않은걸 확인 할 수 있습니다.

이처럼 영속성 컨텍스트는 1차 캐싱을 지원하여, 영속상태인 객체를 조회 할때는 굳이 DB에 접근하지 않고도 해당 값을 가져옵니다.

또, 만약 이미 데이터베이스에서 데이터를 찾아올때도 1차캐시를 사용할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();

        tx.begin();

        try {


            Member member1 = em.find(Member.class, 1L);
            System.out.println("member1 = " + member1.getId());
            System.out.println("member1 = " + member1.getName());

            Member member2 = em.find(Member.class, 1L);
            System.out.println("member2 = " + member2.getId());
            System.out.println("member2 = " + member2.getName());

            tx.commit();
        } catch (Exception e) {
            tx.rollback();
        } finally {
            em.close();
        }
        emf.close();
    }

위 코드를 실행 하면 어떻게 될까요?

실행 시킨 뒤 로그를 확인해보면,

image1

member1을 조회할 때는 SELECT 쿼리가 날아갔지만, member2를 조회 할때는 1차 캐시에 있는 member를 가져온 것을 확인 할 수가 있습니다. member1이 이미 접근해서 데이터를 가져온 상태이니, member2를 조회 할때는 그냥 캐시에 있는 데이터를 가져온 것이죠.

하지만 사실 대부분 트랜잭션을 사용해서 작업을 하기 때문에 이 기능이 성능에 있어 큰 메리트가 되지는 않습니다. 어차피 트랜잭션이 끝나면 캐시도 사라지기 때문에, 트랜잭션안에서 조회하지 않고서야 이러한 1차 캐시에서의 기능을 사용할 일이 크게 없기 때문이죠.

그래도 성능에 정말 민감한 시스템이라면 이것이 이점이 될 수도 있을것입니다.

하지만 성능을 위해서 사용한다기보다는 더 객체지향적인 코드를 작성하는데에 있어 큰 이점을 줍니다. 이 이점은 뒤에서 마저 설명하도록 하겠습니다.

영속 엔티티의 동일성 보장

영속 컨텍스트에 올라간 엔티티들은 동일성이 보장됩니다. 무슨 말일까요?

다시 위의 코드를 가져와 조금 수정해봅시다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();

        tx.begin();

        try {

            Member member1 = em.find(Member.class, 1L);
            Member member2 = em.find(Member.class, 1L);
            System.out.println("result = " + (member1 == member2));

            tx.commit();
        } catch (Exception e) {
            tx.rollback();
        } finally {
            em.close();
        }
        emf.close();
    }

member1 과 member2는 객체는 다르지만 데이터베이스에서 가져온 값이기 때문에 둘은 같아야합니다. 그럼 이 코드를 실행해 볼까요?

image1

member1과 member2가 같다는걸 알 수 있습니다. member1을 데이터 베이스에서 가져오면서 member1이 영속상태가 되었으므로, member2를 찾아오는 요청에는 그냥 1차캐시에 있었던 member의 값을 그대로 member2에 넣어주면 되겠죠?

이렇게 1차캐시의 기능은 영속엔티티의 동일성을 보장해주어, 우리가 객체 지향으로 코딩하는것을 더 편리하게 도와줍니다.

따라서, 1차캐시로 반복가능한 읽기 등급의 트랜잭션 격리수준은, 굳이 데이터베이스까지 사용하지 않고 애플리케이션 차원에서 제공 받을 수 있는 것이죠.

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

앞에서 부터 영속상태인 객체들은 persist()할 때 데이터 베이스에 쓰여지는게 아니라, 해당 정보들을 모두 영속성 컨텍스트에 쌓아뒀다가 commit()을 만나면 그때 쿼리를 날려 데이터 베이스에 쓰기작업을 한다고 했었습니다.

한번 코드로 확인해 볼까요?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();

        tx.begin();

        try {
            Member member1 = new Member(1L, "이민재", "26");
            Member member2 = new Member(2L, "김아무개", "26");
            
            em.persist(member1);
            em.persist(member2);
            System.out.println("==================="); 
            
            tx.commit();
        } catch (Exception e) {
            tx.rollback();
        } finally {
            em.close();
        }
        emf.close();
    }

보면 두개의 컬럼을 만들어준뒤, 이를 persist()를 이용해 영속성 컨텍스트에 올려봅시다. 그리고 commit()과 persist() 사이에 구분선을 하나 출력해줌으로써 쿼리가 언제 나가는지 확인해보도록 하죠.

image1

역시 앞서 이야기 한대로 persist()가 아니라 commit()이 이뤄져야 쿼리가 날아가 데이터 베이스에 쓰기 작업을 하는 것을 확인 할 수 있습니다.

그럼 이렇게 굳이 느리게 영속성 컨텍스트에 모아놨다가 한번에 데이터베이스에 쓰는 것에는 어떤 이점이 있는 걸까요?

네트워크를 호출 하는 것은 애플리케이션 안에서 단순 메소드를 수만번 호출하는 것 보다 더 큰 비용이 듭니다.
따라서 데이터베이스에 변경점이 있다면, 최대한 모아두었다가 한번 네트워크를 호출할때 모두 처리해버리는 것이 비용이 저렴할 것입니다.

위에서만 해도 쓰기 지연이 되지 않았다면 두번의 네트워크 호출이 필요했던 것을, 모아서 한번에 처리 함으로써 한번의 네트워크 호출로 마무리 한 것을 알 수 있습니다. 모아서 한번에 처리함으로써 네트워크 호출을 최대한 줄이게 되는 것이죠.

엔티티 변경 감지

JPA의 목적은 데이터베이스를 객체지향적으로 마치 자바 컬렉션을 다루듯이 사용할 수 있도록 하는 것입니다. 그렇기 때문에 데이터를 바꾸는 쿼리를 따로 날리지 않고, 그냥 데이터베이스에서 데이터를 불러와 이 객체의 값을 바꿔주면, 알아서 UPDATE 쿼리가 날아가게 됩니다. 코드로 한번 확인해보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static void main(String[] args) {
	    EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
	    EntityManager em = emf.createEntityManager();
	    EntityTransaction tx = em.getTransaction();
	
	    tx.begin();
	
	    try {
	        Member member1 = em.find(Member.class, 1L);
	        member1.setName("이름변경");
	
	        tx.commit();
	    } catch (Exception e) {
	        tx.rollback();
	    } finally {
	        em.close();
	    }
	    emf.close();
}

위 코드를 보면, 데이터베이스로 부터 Member 테이블의 1번 id값을 가져와 객체 member1로 저장한뒤, setName()을 통해서 해당 값을 변경해주었습니다. 코드로만 봐서는 데이터베이스의 값이 바뀌지 않을 것 같다는 생각이 듭니다.

하지만 로그를 확인해보면,

image1

UPDATE 쿼리가 데이터베이스로 날아간 것을 확인 할 수 있습니다.

image1

h2 콘솔로 들어가보니 데이터베이스에서도 올바르게 변경 사항이 적용된 것을 확인 할 수 있습니다.

이것이 바로 변경 감지입니다. 이렇게 하면 벌써 훨씬 객체지향으로 코딩 할 수 있을 것 같이 느껴지지 않으신가요?

그럼 이러한 변경은 어떻게 동작하는걸까요?? 아래 그림을 보도록 합시다.

image1

우리가 member의 데이터를 찾아와 memberA라는 객체에 저장하게 되면, JPA는 1차캐시에 이를 저장함과 동시에 이에 대한 스냅샷을 만듭니다. 스냅샷은 데이터를 받아왔을 때의 데이터를 그대로 담아놓은 것이죠.

그래서 commit()명령을 만나게 되면, 이 스냅샷과 현재 객체의 데이터를 비교한 뒤 다른 부분에 대해서 UPDATE 쿼리를 쓰기 지연 저장소에 모은 뒤, 앞서 말했던 것 처럼 네트워크와 접촉해 데이터 베이스에 반영해주는것이죠.

그림에 보면 flush()라는 명령어가 있는데, 사실 영속성 컨텍스트의 내용을 DB에 반영하는 것은 commit()이 아니고 flush()입니다. commit() 내부에서 flush()를 호출하기 때문에 commit()이 이루어지면 DB에 컨텍스트가 반영 되는 것이죠. commit() 메소드는 flush()를 호출한 뒤 영속성 컨텍스트의 내용과 1차캐시를 모두 제거합니다.

마무리…

이렇게 JPA의 영속성 컨텍스트가 무엇이고, 이로써 얻는 장점이 무엇인지 알아보았습니다.

영속성 컨텍스트가 어떤일을 하는지 알고나니, 왜 JPA가 객체지향적으로 코드를 작성할 수 있게 해준다는건지 조금은 와닿으셨으리라 생각됩니다.

이제 기존의 방식처럼 SQL과 DB에 의존하지 않고 마치 컬렉션을 사용하듯이 값을 저장하고 수정하고, 삭제 할 수 있겠죠?

다음게시물에서는 JPA를 사용하기 위한 테이블 매핑과 연관관계 매핑에 대해 다뤄 보겠습니다.

카테고리:

업데이트:

댓글남기기