Junhyunny's Devlogs

글 검색

기술 글 제목, 본문, 카테고리에서 검색합니다.

    이전 화면으로

    @PersistenceContext 애너테이션

    • spring-boot
    • jpa
    이 글의 목차

    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 사용 방법이다.

    KEEP READING

    DISCUSSION

    댓글