티스토리 뷰
오늘은 영속성 컨텍스트의 스코프와 준영속 상태에 대해서 공부하다 생긴 의문점에 대한 것과 이를 해결해 나갔던 과정에 대해서 작성한다.
김영한님의 JPA강의를 듣고 자바 ORM 표준 JPA 프로그래밍을 다시 정독하던 중 한 코드에서 의문이 생겼다.
public class ExamMergeMain {
static EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpabook");
public static void main(String[] args) {
Member member = createMember("memberA", "회원1");
member.setName("updateName");
mergeMember(member);
}
static Member createMember(String id, String username) {
// 영속성 컨텍스트1 시작
EntityManager em1 = emf.createEntityManager();
EntityTransaction tx1 = em1.getTransaction();
tx1.begin();
Member member = new Member();
member.setId(id);
member.setName(username);
em1.persist(member);
tx1.commit();
em1.close(); // 영속성 컨텍스트1 종료
return member;
}
static void mergeMember(Member member) {
// 영속성 컨텍스트2 시작
EntityManager em2 = emf.createEntityManager();
EntityTransaction tx2 = em1.getTransaction();
tx2.begin();
Member mergeMember = em2.merge(member);
tx2.commit();
mergeMember.setName("update")
// 준영속 상태
System.out.println("member = " + member.getName());
// 영속 상태
System.out.println("mergeMember = " + mergeMember.getName());
System.out.println("em2 contains member = " + en2.contains(member)); // false
System.out.println("em2 contains mregeMember = " + en2.contains(mregeMember)); // true
em2.close();
}
}
merge()
메소드를 통해 준영속 상태의 member를 다시 영속 상태로 만드는 부분이다. 여기서 mergeMember
가 트랜잭션이 커밋된 후 close()
가 호출되기 전까지 영속화 되어 있는 것을 알 수 있다. 트랜잭션이 커밋된다고 하더라도 1차 캐시가 초기화되거나 하지 않기 때문이다.
나는 보통 Repository나 Service 클래스를 작성할 때 @Transactional
어노테이션을 기본적으로 모든 메서드에 적용하고 readOnly
옵션만 다르게 설정한다.
@Transactional(readOnly=true)
@Repository
public class UserRepository {
@PersistenceContext
private final EntityManager em;
public UserRepository(EntityManager em) {
this.em = em;
}
@Transactional
public User save(User user) {
em.persist(user);
return user;
}
}
만약 @Transactional
어노테이션이 적용되지 않는 부분에서 엔티티를 직접 사용한다면, 그 엔티티는 준영속 상태일 것이다.
여기서 의문점이 하나 생긴다. 김영한님의 코드에는 트랜잭션을 수동으로 시작하고 커밋한 것이고, 내가 작성한 코드에서는 어노테이션만 사용한 것일 뿐이다. 그런데 예제 코드에서는 close()
가 호출되기 전까지 엔티티가 영속상태이지만 내가 작성한 코드는 트랜잭션만 벗어나면 엔티티가 준영속 상태가 되어버린다. 왜 그런 것일까?
원인 분석
먼저 실제로 엔티티를 수정하는 코드를 실행하고 영속성 컨텍스트에 반영되는 지 살펴본다.
@Transactional을 사용했을 때
실제로 업데이트 쿼리가 정상적으로 발생한 것을 확인할 수 있다.
@Transactional을 사용하지 않았을 때
@Transactional
을 사용하지 않으면 테스트도 실패하고 업데이트 쿼리도 발생하지 않은 것을 확인할 수 있다. 즉, user는 준영속 상태의 인스턴스인 것이다.
이렇게 트랜잭션을 수동으로 시작 및 커밋하는 것과 @Transactional
을 사용한 코드의 차이점은 영속성 컨텍스트를 직접 close()
를 호출하여 release하는 것과 자동으로 release하는 것에서 차이가 발생한다.
스프링에서는 영속성 컨텍스트를 수동으로 close()
를 호출하지 않는 이상 트랜잭션의 라이프사이클에 따라 영속성 컨텍스트를 생성하고 종료한다. 이러한 개념을 Transaction Scoped Persistence Context라고 한다.
영속성 컨텍스트 스코프
영속성 컨텍스트 스코프에는 Transaction Scope와 Extended Scope가 있다.
Transaction Scope
Transaction Scope은 영속성 컨텍스트가 트랜잭션에 바인딩된 것이다. 트랜잭션이 활성화되면 현재 트랜잭션과 연결된 영속성 컨텍스트가 없을 경우 새로운 영속성 컨텍스트가 생성된다.
클라이언트의 요청에 의해 활성화된 트랜잭션이 커밋 혹은 롤백된 경우 해당 트랜잭션과 연관된 영속성 컨텍스트가 종료된다. 이때 모든 엔티티가 준영속 상태가 되어버린다.
Transaction Scope의 영속성 컨텍스트에서는 서로 다른 트랜잭션은 서로 다른 영속성 컨택스트를 가진다. 즉 stateless하다. 따라서 EntityManager가 Thread-safe하게 동작할 수 있다.
Extended
Extended Persistence Context는 stateful session bean에 의해서 실행되는 영속성 컨텍스트를 의미한다. 즉, 영속성 컨텍스트가 트랜잭션의 경계를 넘어 클라이언트의 요청을 처리하기위한 비즈니스 메서드 전체에서 공유되는 것을 의미한다.
영속성 컨텍스트의 스코프를 변경하기 위해서는 @PersistenceContext(type = xxx)
와 같이 type 속성을 지정해주어야 한다. type에는 PersistenceContextType.TRANSACTION
, PersistenceContextType.EXTENDED
두 가지가 존재하며 기본 값은 트랜잭션 스코프이다.
실제로 트랜잭션 스코프의 영속성 컨텍스트에서 트랜잭션의 경계를 벗어나는 메소드에서 find()
를 호출하면 호출하는 횟수 만큼 select 쿼리가 발생할 것이다. 영속성 컨텍스트가 종료되어 1차 캐시에 엔티티가 존재하지 않기 때문에 DB를 확인하기 위해서 쿼리를 발생시킬 것이기 때문이다. 실제로 코드를 작성해서 로그를 살펴본다.
실제로 select 쿼리가 4회 발생한 것을 확인할 수 있다.
이번에는 Extended Persistence Context로 변경하여 코드를 실행한다. 영속성 컨텍스트가 트랜잭션의 경계를 벗어나 공유되기 때문에 1차 캐시에 엔티티가 존재한다. 따라서 select 쿼리가 발생하지 않을 것이다.
type=PersistenceContextType.EXTENDED
로 설정하니 select 쿼리가 발생하지 않은 것을 확인할 수 있다.
참고
'JPA' 카테고리의 다른 글
Entity의 필드가 nullable 하지 않을 때 발생할 수 있는 실수 (0) | 2021.09.30 |
---|