3. [JPA] 영속성 컨텍스트란?
내가 처음 영속성 컨텍스트라는 단어를 접했을 때 해당 용어가 의미하는 바를 정확히 이해하지 못하였다.
"영속성" + "컨텍스트" 두 단어의 조합으로 이루어진 용어인 것 같은데
단어 하나하나를 따져보면 영속성은 오래도록 계속 유지되는 성질을 의미하고
컨텍스트는 저장소로써 어떤 정보들을 저장하고 관리하는 용도로써 많이들 생각한다.
영속성 컨텍스트 = "오래도록 계속 유지되는 성질들을 저장하고 관리" 라고 정의할 수 있겠는데...
이게 뭔 소린가?
오래도록 계속 유지되는 성질이라니..
여튼 용어가 전달하는 의미를 내 나름대로의 해석을 통해 이해해 보려고 했다.
그래서 생각해 낸 것이 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.class, 1); 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.class, 1); assertNotNull(account); // 동일 엔티티 조회 Account account2 = em.find(Account.class, 1); 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.class, 2); 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 |
[영속성 컨텍스트에 엔티티 저장되어져 있는지 확인]
- Account id = 1 인 데이터가 DB에 저장 (JUnit의 @Before 메서드 실행)
- Account where = 1 엔티티 조회 // 쿼리가 DB로 전송
- 조회된 엔티티가 null이 아님을 체크
- PersistenceUnitUtil 클래스를 이용하여 영속성 컨텍스트에 Account where = 1 엔티티가 저장되어 있는지 확인
- 엔티티가 존재하면 true를 리턴한다.
[영속성 컨텍스트에 저장되어 있는 엔티티 재사용 확인]
- Account id = 1 인 데이터가 DB에 저장 (JUnit의 @Before 메서드 실행)
- Account where = 1 엔티티 조회 // 쿼리가 DB로 전송
- 조회된 엔티티가 null이 아님을 체크
- Account where = 1 엔티티 다시 조회 // 이미 영속성 컨텍스트에 저장되어 있기 때문에 쿼리 전송 안 함
- 동일 인스턴스인지 확인
> 실제 두 인스턴스의 hashCode 값이 같다.
account hashcode : 104885507
account2 hashcode : 104885507
[언제 영속성 컨텍스트 flush가 이뤄지나?]
해당 테스트 케이스를 만들기 위해서 테스트 데이터를 생성할 때 별도의 트랜잭션에서 처리되도록 하였고,
또한 Account 의 상태 변경을 처리할 때에도 별도의 트랜잭션에서 동작하도록 코드를 작성하였다.
- test 데이터 생성시 별도의 트랜잭션에서 데이터 생성
> 테스트 데이터를 생성하는 testData 메서드가 종료되면 영속성 컨텍스트가 flush 되고 해당 데이터가 DB에 반영된다. - accountService의 updateAccount 메서드를 호출하면 별도의 트랜잭션이 생성되고 Account where = 2의 name필드 값을 변경한다.
> updateAccount 메서드도 종료되면 영속성 컨텍스트가 flush 되면서 update 문이 DB에 전송된다. - em.find(Account.class, 2); 와 같이 조회를 하면 select 쿼리가 DB에 전송되고 변경된 이름을 확인할 수 있다.
테스트에서 볼 수 있듯이 영속성 컨텍스트는 트랜잭션이 종료되면 flush 가 동작하게 된다.
즉, 트랜잭션과 영속성 컨텍스트는 동일한 라이프 사이클을 가지고 있다고 보면 된다.
'프로그래밍' 카테고리의 다른 글
7. [JPA] fetch type - 로딩 기법 (0) | 2017.08.21 |
---|---|
6. [JPA] 영속성 전이 - Cascade (0) | 2017.08.17 |
5. [JPA] 엔티티 매니저 (0) | 2017.08.16 |
4. [JPA] 엔티티 매니저 팩토리 (0) | 2017.08.10 |
2. [JPA] 테스트 환경 (0) | 2017.07.21 |
1. [JPA] 사용 경험 (2) | 2017.07.21 |
spark framework (0) | 2016.09.28 |
iframe에서 parent 페이지 접근을 위해서는 document.domain 필요 (0) | 2016.08.29 |