@PersistenceContext 애너테이션
RECOMMEND POSTS BEFORE THIS
- Proxy Pattern
- PersistenceContext and Entity Lifecycle
- Features of EntityManager
- EntityManagerFactory 클래스
0. 들어가면서
EntityManagerFactory 클래스 포스트에선 스레드 안전(thread safe)하지 않은 엔티티 매니저(EntityManager)를 안전하게 사용하는 방법들 중 한 가지인 EntityManagerFactory
클래스에 대해 정리하였습니다.
이번엔 @PersistenceContext
애너테이션을 통해 엔티티 매니저의 스레드 안전성을 도모하는 방법에 대해 정리해보았습니다.
1. @PersistenceContext 애너테이션
@Autowired
애너테이션을 통해 빈(bean)을 주입 받듯이 @PersistenceContext
애너테이션을 사용하면 EntityManager
빈을 주입 받을 수 있습니다.
EntityManager
의 실제 구현 클래스의 객체가 아닌 프록시(proxy) 객체를 주입 받습니다.
EntityManagerProxy 인터페이스
package org.springframework.orm.jpa;
import javax.persistence.EntityManager;
public interface EntityManagerProxy extends EntityManager {
EntityManager getTargetEntityManager() throws IllegalStateException;
}
@PersistenceContext 애너테이션을 통한 빈 주입
2. SharedEntityManagerInvocationHandler 클래스
주입 받은 엔티티 매니저를 호출하면 프록시 내부에 invocationHandler
객체로 등록된 SharedEntityManagerInvocationHandler
인스턴스를 사용합니다.
SharedEntityManagerInvocationHandler
인스턴스의 invoke
메소드를 살펴보면 다음과 같은 내용을 확인할 수 있습니다.
EntityManagerFactoryUtils.doGetTransactionalEntityManager
호출 라인- 해당 스레드에 트랜잭션이 시작된 엔티티 매니저가 있다면 이를 반환합니다.
- 해당 스레드에 엔티티 매니저가 없다면 새로운 엔티티 매니저를 생성 후 반환합니다.
Object result = method.invoke(target, args)
호출 라인- 획득한 엔티티 매니저(target)로 프록시 외부에서 호출한 메소드를 실행합니다.
@Override
@Nullable
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// ...
// Determine current EntityManager: either the transactional one managed by the factory or a temporary one for the given invocation.
EntityManager target = EntityManagerFactoryUtils.doGetTransactionalEntityManager(
this.targetFactory, this.properties, this.synchronizedWithTransaction);
switch (method.getName()) {
case "getTargetEntityManager":
if (target == null) {
throw new IllegalStateException("No transactional EntityManager available");
}
return target;
case "unwrap":
Class<?> targetClass = (Class<?>) args[0];
if (targetClass == null) {
return (target != null ? target : proxy);
}
if (target == null) {
throw new IllegalStateException("No transactional EntityManager available");
}
break;
}
// ...
// Invoke method on current EntityManager.
try {
Object result = method.invoke(target, args);
if (result instanceof Query) {
Query query = (Query) result;
if (isNewEm) {
Class<?>[] ifcs = cachedQueryInterfaces.computeIfAbsent(query.getClass(), key ->
ClassUtils.getAllInterfacesForClass(key, this.proxyClassLoader));
result = Proxy.newProxyInstance(this.proxyClassLoader, ifcs,
new DeferredQueryInvocationHandler(query, target));
isNewEm = false;
}
else {
EntityManagerFactoryUtils.applyTransactionTimeout(query, this.targetFactory);
}
}
return result;
} catch (InvocationTargetException ex) {
throw ex.getTargetException();
} finally {
if (isNewEm) {
EntityManagerFactoryUtils.closeEntityManager(target);
}
}
}
3. Example
EntityManagerFactory
클래스를 직접 사용하면 @Transactional
애너테이션을 통한 트랜잭션 처리가 불가능합니다.
반면에 @PersistenceContext
애너테이션을 통해 주입 받은 엔티티 매니저를 사용하면 @Transactional
애너테이션을 통한 트랜잭션 처리가 가능합니다.
간단한 예시 코드를 통해 확인해보겠습니다.
3.1. PcAnnotationService 클래스
- 데이터 생성 후 조회하는 간단한 비즈니스 로직을 트랜잭션 처리합니다.
- 롤백을 유도하기 위해 플래그(flag) 값으로 의도적인 예외를 던집니다.
package action.in.blog.annotation;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class PcAnnotationService {
private final PcAnnotationStore pcAnnotationStore;
public PcAnnotationService(PcAnnotationStore pcAnnotationStore) {
this.pcAnnotationStore = pcAnnotationStore;
}
@Transactional
public PcAnnotationEntity findEntityAfterInsert(String name, boolean intentionallyException) {
pcAnnotationStore.createFactoryEntity(name);
if (intentionallyException) {
throw new RuntimeException("throw intentionallyException");
}
return pcAnnotationStore.findByName(name);
}
}
3.2. PcAnnotationStore 클래스
- 엔티티 매니저를
@PersistenceContext
애너테이션을 통해 주입 받습니다. - 데이터 추가, 조회 기능을 제공합니다.
package action.in.blog.annotation;
import org.springframework.stereotype.Repository;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.TypedQuery;
@Repository
public class PcAnnotationStore {
@PersistenceContext
private EntityManager em;
public void createFactoryEntity(String name) {
em.persist(PcAnnotationEntity.builder()
.name(name)
.build());
}
public PcAnnotationEntity findByName(String name) {
TypedQuery<PcAnnotationEntity> query = em.createQuery("select pc from PcAnnotationEntity pc where pc.name = :name", PcAnnotationEntity.class);
query.setParameter("name", name);
return query.getSingleResult();
}
}
4. Tests
- 예외가 발생하지 않는 경우
- 데이터가 정상적으로 추가됩니다.
- 추가된 데이터를 DB에서 발급받은 ID로 조회할 수 있습니다.
- 예외가 발생하는 경우
- 데이터가 정상적으로 추가되지 않습니다.
- 데이터 검색 시 NoResultException 예외가 발생합니다.
package action.in.blog.annotation;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import javax.persistence.*;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.jupiter.api.Assertions.assertThrows;
@SpringBootTest
public class PcAnnotationServiceIT {
@PersistenceUnit
EntityManagerFactory factory;
@Autowired
PcAnnotationService sut;
@Test
@DisplayName("정상 처리되면 데이터베이스에 데이터가 저장된다.")
void find_entity_with_exception_after_insert() {
EntityManager entityManager = factory.createEntityManager();
PcAnnotationEntity pcAnnotationEntity = sut.findEntityAfterInsert("Hello Word", false);
PcAnnotationEntity result = entityManager.find(PcAnnotationEntity.class, pcAnnotationEntity.getId());
assertThat(result.getName(), equalTo(pcAnnotationEntity.getName()));
}
@Test
@DisplayName("비즈니스 로직 중간에 예외가 발생하는 경우 데이터가 롤백된다.")
void rollback_data_with_exception_after_insert() {
EntityManager entityManager = factory.createEntityManager();
RuntimeException exception = assertThrows(RuntimeException.class, () -> {
sut.findEntityAfterInsert("Hello World", true);
});
assertThat(exception.getMessage(), equalTo("throw intentionallyException"));
assertThrows(NoResultException.class, () -> {
TypedQuery<PcAnnotationEntity> query = entityManager.createQuery("select pc from PcAnnotationEntity pc where pc.name = :name", PcAnnotationEntity.class);
query.setParameter("name", "Hello World");
query.getSingleResult();
});
}
}
5. Dependency Injection with @Autowired
위에서 다룬 내용들을 요약하면 다음과 같습니다.
@PersistenceContext
애너테이션을 사용하면 엔티티 매니저 프록시 객체를 주입 받아서 사용합니다.- 프록시 객체를 통해 스레드 별로 엔티티 매니저를 생성하고 사용하기 때문에 스레드 안정성을 보장합니다.
일반적으로 @Autowired
애너테이션을 통해 빈을 주입 받으면 어플리케이션 전역에서 사용 중인 객체를 전달받습니다.
때문에 엔티티 매니저를 @Autowired
애너테이션을 통해 주입 받는 것은 위험해보입니다.
하지만, 실제로 @Autowired
애너테이션으로 엔티티 매니저를 주입 받으면 @PersistenceContext
애너테이션과 동일하게 프록시 객체를 주입받습니다.
생성자 주입(constructor injection)을 통해 전달 받는 엔티티 매니저도 동일합니다.
@Autowired 애너테이션을 통한 빈 주입
5.1. @PersistenceContext is same with @Autowired
예전엔 반드시 @PersistenceContext
애너테이션을 사용해야 했지만, 특정 버전 이상부터는 @Autowired
애너테이션도 동일한 기능을 제공하는 것으로 보입니다.
공식 문서에선 확인하지 못 했지만, 관련된 내용을 StackOverflow
에 문의한 결과 다음과 같은 답변을 받을 수 있었습니다.
- 두 방법은 동일하지만,
@PersistenceContext
애너테이션을 사용하는 것이 명시적이고, 표준 JPA 사용 방법이다.
댓글남기기