3. [JPA] 영속성 컨텍스트란?

프로그래밍|2017. 8. 1. 16:40

내가 처음 영속성 컨텍스트라는 단어를 접했을 때 해당 용어가 의미하는 바를 정확히 이해하지 못하였다.

"영속성" + "컨텍스트" 두 단어의 조합으로 이루어진 용어인 것 같은데

단어 하나하나를 따져보면 영속성은 오래도록 계속 유지되는 성질을 의미하고

컨텍스트는 저장소로써 어떤 정보들을 저장하고 관리하는 용도로써 많이들 생각한다.


영속성 컨텍스트 = "오래도록 계속 유지되는 성질들을 저장하고 관리" 라고 정의할 수 있겠는데...

이게 뭔 소린가?

오래도록 계속 유지되는 성질이라니..


여튼 용어가 전달하는 의미를 내 나름대로의 해석을 통해 이해해 보려고 했다.

그래서 생각해 낸 것이 JPA에서 사용되는 엔티티라는 단어이다.

"엔티티를 저장하고 관리"

사실 JPA를 처음 접하는 분들은 엔티티가 뭔지 궁금해 할 것이다.

Entity는 Table과 매핑된다.

예를 들어 Member 테이블에 10개의 row가 저장되어 있을때 select * from member 와 같이 조회한다면 영속성 컨텍스트에 10개의 엔티티가 생성되고 관리되어 진다.


용어 삼매경은 이쯤에서 마무리 하고 앞으로 꼭 이해하고 넘어가야 할 영속성 컨텍스트에 대해서 알아보겠다.


영속성 컨텍스트는 내부에 캐시를 가지고 있는데 이것을 1차 캐시라 하고 영속 상태의 엔티티는 모두 이곳에 저장이 된다. 

아래 그림과 같이 JDBC와 DB사이에 위치하여 동작한다.

출처 : https://aishwaryavaishno.files.wordpress.com/2013/06/jpa.png



먼저 JDBC 개발 방식 VS JPA 개발 방식의 차이점을 알아보자.

JDBC를 이용해서 애플리케이션을 개발 할 때 쿼리를 실행하면 DB로 쿼리를 바로 날린다.

아래와 같이 코드를 작성하고 execute하면 DB로 쿼리해서 데이터를 가져오는 식이다.

String sql = "SELECT id, first, last, age FROM Registration";

ResultSet rs = stmt.executeQuery(sql); // DB로 쿼리한다.


허나 영속성 컨텍스트는 JDBC와 DB 사이에 위치하여 뭔가의 작업을 진행한다.

그 무엇인가는 다음과 같다.

  • 조회할 데이터가 영속성 컨텍스트에 존재하는지 확인
  • 데이터가 없으면 쿼리를 생성
  • 쿼리를 DB에 전송
  • 결과 값을 영속성컨텍스트가 전달 받음
  • 전달 받은 데이터를 엔티티로 저장
  • 엔티티 인스턴스를 리턴

위의 많은 처리를 코드 단 한 줄이 수행한다.

Emp emp = em.find(Emp.class, 1);


그래서 장점이 뭔데?

지금까지 내가 경험하면서 알게 된 장점을 나열해 보겠다.

  • select * from emp wherer id = 1 로 조회해서 데이터를 가져오면 동일 아이디로 쿼리 재요청 시 영속성 컨텍스트에 있는 엔티티를 가져와 재사용
  • 특정 비즈니스에서 update, insert, delete 를 여러 번 구현했을 때 이전 방식에서는 코드가 수행될 때마다 DB에 요청을 보내는 반면 영속성 컨텍스트는 쿼리를 생성하여 특정 영역에 저장만 해두었다가 flush가 되는 순간 한꺼번에 DB로 쿼리를 날린다. 이를 JPA에서는 "쓰기 지연" 이라고 한다.
  • 업데이트 처리가 필요할 때 Emp emp = em.find(Emp.class, 1); 와 같이 엔티티를 가져온 후 emp.setName("nklee"); 와 같이 엔티티의 속성 값을 변경하면 엔티티 매니저가 flush 되면서 update 쿼리문을 DB로 쿼리한다.
  • 지연 로딩을 통해 특정 엔티티의 데이터를 사용하는 시점에 초기화하여 DB 요청을 보낼 수 있다.
  • 화면마다 쿼리 안 만들어도 된다.
  • 리펙토링이 쉽다.

앞서 장점을 나열한 것 처럼 잘 사용한다면 개발 생산성 및 시스템 성능 향상에 큰 도움이 된다.

다만 학습 코스트가 높은 편이라 대충 학습하고 넘어가면 안 되는 기술이기도 하다. 잘못 사용하게 되어 위의 장점을 누리지 못하는 것은 문제가 되지 않지만 성능에 큰 영향을 줄 수 있기 때문이다.

성능에 대한 이슈도 차근 차근 정리해 나갈 예정이다.


이제 부터는 영속성 컨텍스트 동작 방식에 대해서 테스트 코드와 함께 알아보도록 하자.

위의 테스트 케이스들에 대한 설명을 하자면 다음과 같다.


[전체 코드]

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
    @Slf4j
    @RunWith(SpringRunner.class)
    @SpringBootTest
    public class EntityManagerTest {
    
        @PersistenceContext(type = PersistenceContextType.TRANSACTION)
        private EntityManager em;
    
        @Autowired
        private AccountService accountService;
    
        @Before
        public void testData() {
            // test 데이터
            Account account = new Account();
            account.setId(1);
            account.setName("nklee");
            em.persist(account);
            em.flush();
            em.clear();
        }
    
        @Test
        @Transactional
        public void 영속성_컨텍스트에_엔티티_저장되어져있는지_확인() {
            // 엔티티 조회
            Account account = em.find(Account.class1);
            assertNotNull(account);
    
            // 영속성 컨텍스트에 id=1 인 엔티티가 존재하는지 확인
            PersistenceUnitUtil persistenceUnitUtil = em.getEntityManagerFactory().getPersistenceUnitUtil();
            boolean hasEntity = persistenceUnitUtil.isLoaded(account);
            assertThat(true, is(hasEntity));
        }
    
        @Test
        @Transactional
        public void 영속성_컨텍스트에_저장되어_있는_엔티티_재사용_확인() {
            // 엔티티 조회
            Account account = em.find(Account.class1);
            assertNotNull(account);
    
            // 동일 엔티티 조회
            Account account2 = em.find(Account.class1);
            assertNotNull(account2);
    
            // 동일한 인스턴스인지 확인
            assertThat(account, is(sameInstance(account2)));
        }
    
        @Test
        @Transactional
        public void 언제_영속성_컨텍스트_flush가_이뤄지나() {
            // 다른 트랜잭션에서 Account 테스트 데이터 저장.
            // 이와 같이 다른 트랜잭션에서 테스트 데이터를 저장하는 이유는 실제 DB에 commit 되어진 데이터를 생성하기 위함이다.
            accountService.testData();
    
            // 다른 트랜잭션에서 Account where = 2 의 name 변경 처리
            accountService.updateAccount(2);
    
            // 변경되어진 이름 확인
            Account account2 = em.find(Account.class2);
            assertThat("changedName", is(account2.getName()));
        }        
    }
    
    
    @Service(value = "entityManagerAccountService")
    class AccountService {
    
        @Getter
        @PersistenceContext
        private EntityManager em;
    
        @org.springframework.transaction.annotation.Transactional(propagation = Propagation.REQUIRES_NEW)
        public void updateAccount(int id) {
            Account account = em.find(Account.class, id);
            account.setName("changedName");
        }
    
        @org.springframework.transaction.annotation.Transactional(propagation = Propagation.REQUIRES_NEW)
        public void testData() {
            Account account = new Account();
            account.setId(2);
            account.setName("nklee");
            em.persist(account);
        }
    }
    
    
    @Data
    @Entity
    class Account {
    
        @Id
        private int id;
    
        private String name;
    }
cs



[영속성 컨텍스트에 엔티티 저장되어져 있는지 확인]

  1. Account id = 1 인 데이터가 DB에 저장 (JUnit의 @Before 메서드 실행)
  2. Account where = 1 엔티티 조회 // 쿼리가 DB로 전송
  3. 조회된 엔티티가 null이 아님을 체크
  4. PersistenceUnitUtil 클래스를 이용하여 영속성 컨텍스트에 Account where = 1 엔티티가 저장되어 있는지 확인
  5. 엔티티가 존재하면 true를 리턴한다.


[영속성 컨텍스트에 저장되어 있는 엔티티 재사용 확인]

  1. Account id = 1 인 데이터가 DB에 저장 (JUnit의 @Before 메서드 실행)
  2. Account where = 1 엔티티 조회 // 쿼리가 DB로 전송
  3. 조회된 엔티티가 null이 아님을 체크
  4. Account where = 1 엔티티 다시 조회 // 이미 영속성 컨텍스트에 저장되어 있기 때문에 쿼리 전송 안 함
  5. 동일 인스턴스인지 확인
     > 실제 두 인스턴스의 hashCode 값이 같다.
        account hashcode : 104885507
        account2 hashcode : 104885507


[언제 영속성 컨텍스트 flush가 이뤄지나?]

해당 테스트 케이스를 만들기 위해서 테스트 데이터를 생성할 때 별도의 트랜잭션에서 처리되도록 하였고,

또한 Account 의 상태 변경을 처리할 때에도 별도의 트랜잭션에서 동작하도록 코드를 작성하였다.

  1. test 데이터 생성시 별도의 트랜잭션에서 데이터 생성
    > 테스트 데이터를 생성하는 testData 메서드가 종료되면 영속성 컨텍스트가 flush 되고 해당 데이터가 DB에 반영된다.
  2. accountService의 updateAccount 메서드를 호출하면 별도의 트랜잭션이 생성되고 Account where = 2의 name필드 값을 변경한다.
    > updateAccount 메서드도 종료되면 영속성 컨텍스트가 flush 되면서 update 문이 DB에 전송된다.
  3. em.find(Account.class, 2); 와 같이 조회를 하면 select 쿼리가 DB에 전송되고 변경된 이름을 확인할 수 있다.

테스트에서 볼 수 있듯이 영속성 컨텍스트는 트랜잭션이 종료되면 flush 가 동작하게 된다.

즉, 트랜잭션과 영속성 컨텍스트는 동일한 라이프 사이클을 가지고 있다고 보면 된다.



댓글()