이번 시간에는 JPA의 핵심 개념 중 하나라고 할 수 있는 영속성에 대해 알아보도록 하겠다.
영속성 컨텍스트 (persistence context)는 엔티티를 영구 저장하는 환경이라고 볼 수 있다.
앞장에서 CRUD의 기능을 제공한다는 엔티티 매니저가 생성될 때 하나의 영속성 컨텍스트도 생성된다.
우리는 엔티티 메니저를 통해 영속성 컨텍스트에 접근할 수 있고, 관리할 수 있다.
엔티티 매니저로 엔티티를 저장하거나 조회하면, 엔티티 매니저는 영속성 컨텍스트에 엔티티를 보관하고 관리한다.
그래서 뭐하는건데, 싶을 수 있어 간단히 예시를 들어보겠다.
영속성 컨텍스트는 Map 형식으로, 식별자(@ID로 테이블의 PK와 매핑한 값)를 키로 엔티티 인스턴스를 value로 가지는 1차 캐시를 가진다.
식별자를 통해 find 메소드(조회하는 메소드)를 이용하면, 엔티티 매니저는 먼저 이 영속성 컨텍스트를 찾아가 해당 식별자를 가진 엔티티가 있는지 확인한다.
그래서 같은 식별자로 2번 조회를 하게 되면, 처음에는 영속성 컨텍스트에 해당 엔티티가 없으니 DB에 직접 조회하여 가져온 다음, 영속성 컨텍스트에 이를 저장하고 반환한다. 두 번째로 조회할 때는 영속성 컨텍스트에 해당 엔티티가 있으니 이를 바로 반환하여 DB를 조회하는 시간을 없앨 수 있다.
이러한 영속성 컨텍스는 다음과 같은 장점들을 가진다.
영속성 컨텍스트의 장점
1. 1차 캐시
2. 동일성 보장
3. 트랜잭션을 지원하는 쓰기 지연
4. 변경 감지
5. 지연 로딩
이 장점들을 자세히 이해하려면 엔티티의 생명주기를 알아야 할 필요가 있다.
엔티티에는 4가지 상태가 있다.
1. 비영속 (new/transient): 영속성 컨텍스트와 전혀 관계가 없는 상태
2. 영속 (managed) : 영속성 컨텍스트에 저장된 상태
3. 준영속 (detached) : 영속성 컨텍스트에 저장되었다가 분리된 상태
4. 삭제(removed) :삭제된 상태
비영속
Member member = new Member();
위와 같이 엔티티 객체를 생성을 한 상태이다.
순수 객체 상태이며, 영속성 컨텍스트나 데이터베이스와는 전혀 관련 없는 상태이다.
영속
entityManage.persist(member);
위와 같이 엔티티를 저장하게 되면 영속 상태가 된다.
엔티티 매니저를 통해 엔티티를 영속성 컨텍스트에 저장되어 관리받는 상태이다.
준영속
entityManage.detach(member)
위처럼 하면 회원 엔티티를 영속성 컨텍스트에 분리하게 되어 준영속 상태가 된다.
준영속은 영속성 컨텍스트가 관리하던 영속 상태의 엔티티를 더 이상 관리하지 않게 되는 상태이다.
삭제
em.remove(member);
위는 데이터베이스에서 객체를 삭제하는 메소드이다.
데이터베이스에도 삭제를 하였으니 영속성 컨텍스트가 해당 객체를 저장하고 있을 필요가 없다. 따라서 DB와 영속성 컨텍스트에서 모두 삭제처리가 된다.
다시 영속성 컨텍스트로 돌아가, 장점들을 하나씩 자세히 살펴보겠다.
1. 1차 캐시
영속성 컨텍스트 내부에는 캐시를 (1차캐시)를 가지고 있고 엔티티는 모두 이곳에 저장된다. 이것의 장점은 위에서 들었던 예시처럼, 객체를 조회할 때 엔티티 메니저는 1차 캐시에 먼저 확인하고, 없으면 DB에 조회한 다음 1차 캐시에 저장하기하고 그것을 반환하기 때문에 1차 캐시에 있는 엔티티를 또 다시 조회할 때는 DB까지 가지 않아도 된다.
2. 동일성 보장
위의 개념을 이해했다면 굳이 설명하지 않아도 이해가 될 장점이다.
같은 식별자로 entity를 조회할 때, 영속성 컨텍스트는 1차 캐시에 저장되어 있는 인스턴스를 반환하기 때문에,
Member a = em.find(Member.class, "member1");
Member b = em.find(Member.class, "member2");
다음의 경우 a==b는 참이 되게 된다.
3. 트랜잭션을 지원하는 쓰기 지연
엔티티를 DB에 저장할 때, 엔티티 매니저는 트랜잭션을 커밋하기 직전까지 데이터베이스에 엔티티를 저장하지 않고, 내부 쿼리 저장소에 INSERT SQL을 차곡 차곡 모아둔다. 그리고 트랜잭션을 커밋할 때 모아둔 쿼리를 데이터베이스에 보내는데 이것을 트랜잭션을 지원하는 쓰기 지연이라고 한다.
그래서 트랜잭션에 커밋하는 순간 모아둔 등록 쿼리를 데이터베이스에 한 번에 전달하기 때문에 그때, 그때 쿼리를 날리는 것보다도 성능을 최적화 할 수 있게 된다.
4. 변경 감지
영속성 컨텍스트는 엔티티의 변경사항을 데이터베이스에 자동으로 반영하는 변경 감지를 지원한다.
JPA는 엔티티를 영속성 컨텍스트에 보관할 때 최초 상태를 복사해서 저장 (=스냅샷)을 한다. 그리고 플러시 시점에 스냅샷과 엔티티를 비교하여 변경된 엔티티를 찾는다.
플러시는 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영(동기화)하는 것이라 보면 된다.
플러시를 실행하면, 앞서 이야기 했던, 영속성 컨텍스트에 있는 모든 엔티티를 스냅샷과 비교하여 수정된 엔티티에 대해 수정 쿼리를 만들어 쓰기 지연 SQL 저장소에 등록하고, 쓰기 지연 SQL 쿼리를 데이터베이스에 전송하게 된다.
따라서
Member member = entityManager.find(Member.class , "member");
member.setAge(10);
transaction.commit();
다음과 같이 코드를 짠 후, 트랜잭션을 커밋하게 되면 굳이 수정 쿼리를 짜거나, 수정 메소드를 사용하지 않아도 자동으로 DB에 수정 내용이 반영되게 된다.
이전시간에 SQL mapper 와 ORM 의 차이에서 보았듯이,
컬럼이 하나 더 추가되고, 이를 수정해야 한다면 SQL mapper는 수정 쿼리문을 추가로 작성해야 한다면, JPA는 하지 않아도 되는 것이 이 변경 감지에가 제공하는 이점이다.
그런데 JPA는 수정 쿼리를 어떤식으로 작성해서 날리는 것일까?
기본적으로 모든 필드를 업데이트하는 방식을 사용한다.
이렇게 하면 데이터베이스에 보내는 데이터 전송량이 증가한다는 단점이 있지만, 모든 필드를 사용하면 수정 쿼리가 항상 같고, 데이터 베이스에 동일한 쿼리를 보내면 데이터베이스는 이전에 한 번 파싱된 쿼리를 재사용할 수 있다는 이점이 있다.
다만, 컬럼이 대략 30개 이상이 되면@org.hibernate.annotaions.DynamicUpdate 을 이용하여 수정된 데이터만을 사용하여 UPDATE SQL을 생성하도록 하는 것이 좋은데, 컬럼이 30개 이상이 된다는 것은 테이블 설계가 잘못되었을 확률이 높기는 하다.
더 알아보기
플러시는 em.flush()를 직접 호출 할 때, 트랜잭션 커밋 시, JPQL 쿼리 실행 시에 호출되게 된다.entityManager.persist(memberA); entityManager.persist(memberB); query = entityManager.createQuery("select m from Member m", Member.class); List<Member> members = query.getResultList();
위의 코드에서 memberA, memberB 엔티티들은 영속성 컨텍스트에는 있지만, 아직 DB에는 반영되지 못하였다.
이때 위의 JPQL 을 실행하면, memberA,memberB는 아직 DB에 없기 때문에 조회되지 않을 것이다. 따라서 이러한 현상을 방지하기 위해 쿼리를 실행하기 직전에 영속성 컨텍스트를 플러시하여 변경 내용을 데이터베이스에 반영하게 된다.
5. 지연 로딩
지연 로딩의 경우 1장에서 살펴보았듯, 객체가 참조된 객체를 사용할 때 그, 연관된 객체까지 조회하는 쿼리를 날려 정보를 가져오는 것이다.
예를 들어, Member 객체 안에 Team 객체가 있고 Team 객체에는 teamName 필드가 있을 때 member 객체를 조회할 때 연관된 모든 정보들을 조회 해 오는 것이 아닌,
member.getTeam().getTeamName(); 처럼 참조된 관계를 사용할 때 정보를 가져오는 것이다.
자세한 이점은 1장 을 참조하면 좋을 듯 하다.
위에서 이점들을 살펴보며 비영속 -> 영속 -> 삭제되는 상태 변화들을 살펴보았다.
이번에는 영속 -> 준영속이 되는 상태 변화를 중점적으로 살펴볼 것이다.
영속 상태의 엔티티를 준영속 상태로 만드는 방법은 크게 3가지가 있다.
detach()
Member member = new Member(); // 비영속 상태
member.setId("memberId");
entityManager.persist(member); //영속 상태
entityManager.detach(member); // 준영속 상태
transaction.commit();
위의 코드에서 엔티티 상태에 따라 영속성 컨텍스트가 어떻게 변화되고 있을까.
member이 영속 상태가 되었을 때, 1차 캐시와 쓰기 지연 SQL 저장소에 member entity가 담기게 된다. 그런데 detach를 통해 준영속 상태가 되었을 때, 1차 캐시에 제거되고 관련 SQL 또한 쓰기 지연 SQL 저장소에서 삭제가 된다. 따라서 위의 코드를 실행하면, member entity는 1차 캐시에도 없고, 데이터베이스에 저장도 되지 않게 된다.
clear(), close()를 하게 되면 특정 엔티티만이 아닌 영속성 엔티티가 관리하고 있는 모든 요소들이 초기화되고 종료가 되게 된다.
준영속 상태가 된 엔티티들은 1차 캐시, 쓰기 지연, 변경 감지, 지연 로딩 등 영속성 컨텍스트가 제공하는 어떠한 기능도 동작하지 않지만, 이미 한 번 영속 상태였기 때문에 식별자 값을 가지고 있다는 특징이 있다.
그렇다면 준영속 상태의 엔티티를 어떻게 다시 영속 상태로 변경할까? 바로 병합을 사용하면 된다.
병합 : merge()
merge() 메소드는 준영속 상태의 엔티티를 받아서 새로운 영속 상태의 엔티티를 반환한다.
다음 코드를 보며 영속 -> 준영속 -> 영속 상태가 되는 변화 과정을 살펴보자.
EntityManager em1 = emf.createEntityManager();
EntityTransaction tx1 = em1.getTransaction();
tx1.begin() ; //트랜잭션 시작
Member member = new Member();
member.setId("id");
em1.persist(member); //영속 상태
tx1.commit(); //트랜잭션 커밋
em1close(); //영속성 컨텍스트가 종료되며 준영속 상태가 됨
member.setName("새 이름"); //준영속 상태에서 변경
EntityManager em2 = emf.createEntityManager();
EntityTransaction tx2 = em1.getTransaction();
tx2.begin(); //트랜잭션 시작
Member mergeMember = em2.merge(member); //새로운 mergeMember로 영속 상태가 됨
tx2.commit(); //트랜잭션 커밋
System.out.println(em2.contains(member));
System.out.println(em2.contains(mergeMember));
---
출력결과
false
true
위의 코드에서, 준영속 상태에서 이름을 변경하였으니 수정 사항을 데이터베이스에 반영할 수 없다. 그래서 병합을 이용하여 영속 상태로 변경해주는데, 실질적으로 member 가 영속 상태가 되는 것이 아니라 mergeMember 라는 새로운 영속 상태의 엔티티가 생기는 것이다.
이 과정을 더 풀어서 설명 해 보겠다.
merge()가 실행되면,
(1) 파라미터로 넘어온 준영속 엔티티의 식별자 값으로 1차 캐시에서 엔티티를 조회하고,
(2) 만약 1차 캐시에 엔티티가 없으면 데이터베이스에서 엔티티를 조회하여 1차 캐시에 저장한다.
(3) 조회한 영속 엔티티 (mergeMember)에 member 엔티티의 값을 채워 넣고
(4) mergeMember를 반환한다.
따라서 사실상 member는 여전히 준영속 상태이다.
그래서 준영속 -> 영속 상태로 만들 때 혼동을 방지하기 위해
member = entityManager.merge(member);
이처럼 작성하는 것이 좋다.
참고로 비영속 엔티티 또한 merge()를 통해 영속 상태로 만들 수 있다.
1차 캐시, 데이터베이스에서도 파라미터로 넘어온 엔티티의 식별자 값으로 엔티티를 찾을 수 없다면 새로운 엔티티를 생성해서 병합하게 된다.
따라서 병합은 save or update 기능을 수행한다고 보아도 된다.
이렇게 영속성 관리에 대해 살펴보았다.
다음에는 엔티티 매핑에 대해 살펴보겠다.
참조 : 자바 ORM 표준 JPA 프로그래밍 - 김영한