EntityManagerFactory 클래스
RECOMMEND POSTS BEFORE THIS
0. 들어가면서
엔티티 매니저(EntityManager)는 스레드 안전(thread safety)하지 않습니다.
내부 영속성 컨텍스트(persistence context)에 1차 캐시를 관리하기 때문에 하나의 엔티티 매니저에 다중 스레드가 접근하면 동시성 문제가 발생할 수 있습니다.
동시성 문제를 해결하려면 각 스레드 별로 고유한 엔티티 매니저를 사용해야 합니다.
이번 포스트에선 동시성 문제가 발생하지 않도록 엔티티 매니저를 사용하는 방법 중 하나인 EntityManagerFactory에 대해 살펴보겠습니다.
1. EntityManagerFactory 클래스
스레드에 안전하지 않은 엔티티 매니저는 매번 만들어 사용해야 합니다.
EntityManagerFactory 클래스를 사용하면 엔티티 매니저를 만들 수 있습니다.
EntityManagerFactory 클래스는 생성할 때 비용이 비싸기 때문에 어플리케이션 전역에 하나만 만들어 사용하는 것이 좋습니다.
@PersistenceUnit 애너테이션을 사용하면 EntityManagerFactory 빈(bean)을 주입 받을 수 있습니다.
@PersistenceUnit
private EntityManagerFactory factory;
2. Create EntityManager
createEntityManager 메소드를 통해 엔티티 매니저를 생성할 수 있습니다.
엔티티 매니저는 매번 새로운 객체를 생성합니다.
새로 생성될 때마다 세션을 매번 새롭게 맺기 때문에 비즈니스 로직을 하나의 트랜잭션으로 관리하고 싶다면 하나의 엔티티 매니저를 같이 사용해야 합니다.
@Test
@DisplayName("EntityManagerFactory는 EntityManager를 매번 새롭게 만든다.")
void create_entity_manager() {
EntityManager firstEntityManager = factory.createEntityManager();
EntityManager secondEntityManager = factory.createEntityManager();
assertThat(firstEntityManager == secondEntityManager, equalTo(false));
assertThat(firstEntityManager.equals(secondEntityManager), equalTo(false));
}
3. Example
EntityManagerFactory를 사용하면 @Transactional 애너테이션을 통해 트랜잭션 관리가 불가능합니다.
트랜잭션 관리 프로세스를 직접 만들어야 합니다.
간단하게 트랜잭션 관리 프로세스를 구현한 코드를 적용한 예시 코드를 살펴보겠습니다.
3.1. AbstractFactoryService 클래스
- 외부에서 전달한 함수를 트랜잭션 내부에서 실행합니다.
readonly여부에 따라 비즈니스 로직 처리 후 롤백(rollback) 혹은 커밋(commit)을 수행합니다.- 예외(exception)가 발생하면 롤백 처리합니다.
package action.in.blog.factory;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.PersistenceUnit;
public abstract class AbstractFactoryService {
@PersistenceUnit
private EntityManagerFactory factory;
protected <V> V transaction(EntityManagerCallable<V> callable) {
return transaction(callable, false);
}
protected <V> V transaction(EntityManagerCallable<V> callable, boolean readonly) {
EntityManager em = factory.createEntityManager();
EntityTransaction transaction = em.getTransaction();
transaction.begin();
try {
V ret = callable.run(em);
if (readonly) {
transaction.rollback();
} else {
transaction.commit();
}
return ret;
} catch (Throwable e) {
if (transaction.isActive()) {
transaction.rollback();
}
throw new RuntimeException(e);
} finally {
em.close();
}
}
protected interface EntityManagerCallable<V> {
V run(EntityManager em);
}
}
3.2. FactoryService 클래스
- 데이터 생성 후 조회하는 간단한 비즈니스 로직을
transaction메소드에 전달합니다. - 롤백을 유도하기 위해 플래그(flag) 값으로 의도적인 예외를 던집니다.
package action.in.blog.factory;
import org.springframework.stereotype.Service;
@Service
public class FactoryService extends AbstractFactoryService {
private final FactoryStore factoryStore;
public FactoryService(FactoryStore factoryStore) {
this.factoryStore = factoryStore;
}
public FactoryEntity findEntityAfterInsert(String name, boolean intentionallyException) {
return transaction((em) -> {
factoryStore.createFactoryEntity(em, name);
if (intentionallyException) {
throw new RuntimeException("throw intentionallyException");
}
return factoryStore.findByName(em, name);
});
}
}
3.3. FactoryStore 클래스
- 데이터 추가, 조회 기능을 제공합니다.
package action.in.blog.factory;
import org.springframework.stereotype.Repository;
import javax.persistence.EntityManager;
import javax.persistence.TypedQuery;
@Repository
public class FactoryStore {
public void createFactoryEntity(EntityManager em, String name) {
em.persist(FactoryEntity.builder()
.name(name)
.build());
}
public FactoryEntity findByName(EntityManager em, String name) {
TypedQuery<FactoryEntity> query = em.createQuery("select f from FactoryEntity f where f.name = :name", FactoryEntity.class);
query.setParameter("name", name);
return query.getSingleResult();
}
}
4. Tests
- 예외가 발생하지 않는 경우
- 데이터가 정상적으로 추가됩니다.
- 추가된 데이터를 DB에서 발급받은 ID로 조회할 수 있습니다.
- 예외가 발생하는 경우
- 데이터가 정상적으로 추가되지 않습니다.
- 데이터 검색 시
NoResultException예외가 발생합니다.
package action.in.blog.factory;
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.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.NoResultException;
import javax.persistence.PersistenceUnit;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.jupiter.api.Assertions.assertThrows;
@SpringBootTest
public class FactoryServiceIT {
@PersistenceUnit
EntityManagerFactory factory;
@Autowired
FactoryService sut;
@Test
@DisplayName("정상 처리되면 데이터베이스에 데이터가 저장된다.")
void find_entity_with_exception_after_insert() {
EntityManager entityManager = factory.createEntityManager();
FactoryEntity factoryEntity = sut.findEntityAfterInsert("Hello Word", false);
FactoryEntity result = entityManager.find(FactoryEntity.class, factoryEntity.getId());
assertThat(result.getName(), equalTo(factoryEntity.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("java.lang.RuntimeException: throw intentionallyException"));
assertThrows(NoResultException.class, () -> {
FactoryStore store = new FactoryStore();
store.findByName(entityManager, "Hello World");
});
}
}
댓글남기기