Study/Java

[Java] 59. Spring boot의 JPA에서 EntityManager를 사용하는 방법

v명월v 2022. 2. 25. 19:15

안녕하세요. 명월입니다.


이 글은 Spring boot의 JPA에서 EntityManager를 사용하는 방법에 대한 글입니다.


이전 글에서 Spring boot framework 환경에서 JPA를 설정하여 사용하는 방법에 대해 설명했습니다.

링크 - [Java] 58. Eclipse에서 Spring boot의 JPA를 설정하는 방법


그런데 문제가 JpaRepository 인터페이스를 상속받아서 쓴다는 것입니다. JpaRepository 인터페이스 자체가 문제가 있는 것은 아닙니다. 데이터베이스 커넥션을 보다 쉽게 접근하고 트랜잭션을 자동으로 처리해 준다는 점은 매우 편한 부분입니다. 그러나 이 자동으로 처리하는 게 문제가 있습니다.

자동이라는 점은 초기 접근에 매우 좋지만 결국에는 트랜젹선을 컨트럴하는데 한계가 존재한다는 점입니다. 예를 들면, 여러 개의 테이블을 동시에 입력하는데 처리 도중에 에러가 발생했습니다. 그럴 경우 하나의 테이블이 아닌 여러 개의 테이블을 전부 롤백을 해야 하는데, 트랜젝션을 제어하기가 까다로워서 프로그램이 복잡해 질 수 있습니다.

즉, JpaRepository도 트랜잭션을 따로 취득해서 제어가 가능한데 통합적(?)인 관리가 되지 않기 때문에 역으로 소스가 복잡해지고 리소스 관리등의 명확한 흐름이 제어가 되지 않습니다.


먼저, AbstractDao와 FactoryDao를 만들겠습니다.

링크 - [Java] 50. JPA 프로젝트에서 DAO 클래스 작성하기

링크 - [Java] 52. Spring 프레임워크에서 DAO를 Factory Pattern으로 의존성 주입하는 방법


먼저 AbstractDao의 추상클래스를 만들어봅시다.

package com.example.demo.dao;
 
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
 
// Dao 추상클래스
public abstract class AbstractDao<T> {
  // Spring boot에서는 persistence.xml 파일이 없어서 아래의 소스가 에러가 발생한다.
  private static EntityManagerFactory emf = Persistence.createEntityManagerFactory("JpaExample");
  private Class<T> clazz;
  // 람다식을 위한 interface
  protected interface EntityManagerRunable {
    void run(EntityManager em);
  }
  // 람다식을 위한 interface
  protected interface EntityManagerCallable<V> {
    V run(EntityManager em);
  }
  // 생성자를 protected로 설정
  protected AbstractDao(Class<T> clazz) {
    this.clazz = clazz;
  }
  // 클래스 타입을 취득하는 함수
  protected final Class<T> getClazz() {
    return clazz;
  }
  // 테이블에서 key의 조건으로 데이터를 취득한다.
  public T findOne(Object id) {
    return transaction((em) -> {
      return em.find(clazz, id);
    });
  }
  // Entity를 데이터 베이스에 Insert한다.
  public T create(T entity) {
    return transaction((em) -> {
      em.persist(entity);
      return entity;
    });
  }
  // Entity를 데이터 베이스에 Update한다.
  public T update(T entity) {
    return transaction((em) -> {
      // 클래스를 데이터베이스 데이터와 매핑시킨다.
      em.detach(entity);
      // update
      return em.merge(entity);
    });
  }
  // Entity를 데이터 베이스에 Delete한다.
  public void delete(T entity) {
    transaction((em) -> {
      // 클래스를 데이터베이스와 매핑시킨다.
      em.detach(entity);
      // 그리고 update를 하고 삭제한다.
      em.remove(em.merge(entity));
    });
  }
  // 반환 값이 있는 트랜젝션 (일반 트랜젹션으로 데이터가 변화한다.)
  public <V> V transaction(EntityManagerCallable<V> callable) {
    return transaction(callable, false);
  }
  // 반환 값이 있는 트랜젝션 (readonly를 true 설정하면 함수를 호출하는 가운데서는 commit이 발생하지 않는다.)
  public <V> V transaction(EntityManagerCallable<V> callable, boolean readonly) {
    // Manager를 생성한다. EntityManagerFactory를 persistence.xml에서 취득을 못했기 때문에 EntityManager를 취득하지 못한다.
    EntityManager em = emf.createEntityManager();
    // transaction을 가져온다.
    EntityTransaction transaction = em.getTransaction();
    // 트랜젝션을 시작한다.
    transaction.begin();
    try {
      // 람다식을 실행한다.
      V ret = callable.run(em);
      // readonly가 true면 rollback한다.
      if (readonly) {
        transaction.rollback();
      } else {
        // 트랜젝션을 데이터베이스에 입력한다.
        transaction.commit();
      }
      // 결과를 리턴한다.
      return ret;
      // 에러가 발생할 경우
    } catch (Throwable e) {
      // transaction이 활성 중이라면
      if (transaction.isActive()) {
        // rollback
        transaction.rollback();
      }
      // RuntimeException로 변환
      throw new RuntimeException(e);
    } finally {
      // Manager를 닫는다.
      em.close();
    }
  }
  // 반환 값이 없는 transaction (일반 트랜젹션으로 데이터가 변화한다.)
  public void transaction(EntityManagerRunable runnable) {
    transaction(runnable, false);
  }
  // 반환 값이 없는 트랜젝션 (readonly를 true 설정하면 함수를 호출하는 가운데서는 commit이 발생하지 않는다.)
  public void transaction(EntityManagerRunable runnable, boolean readonly) {
    // Manager를 생성한다. EntityManagerFactory를 persistence.xml에서 취득을 못했기 때문에 EntityManager를 취득하지 못한다.
    EntityManager em = emf.createEntityManager();
    // transaction을 가져온다.
    EntityTransaction transaction = em.getTransaction();
    // 트랜젝션을 시작한다.
    transaction.begin();
    try {
      // 람다식을 실행한다.
      runnable.run(em);
      // readonly가 true면 rollback한다.
      if (readonly) {
        transaction.rollback();
      } else {
        // 트랜젝션을 데이터베이스에 입력한다.
        transaction.commit();
      }
      // 에러가 발생할 경우
    } catch (Throwable e) {
      // transaction이 활성 중이라면
      if (transaction.isActive()) {
        // rollback
        transaction.rollback();
      }
      // RuntimeException로 변환
      throw new RuntimeException(e);
    } finally {
      // Manager를 닫는다.
      em.close();
    }
  }
}

위 소스를 그대로 상속해서 Dao를 만들고 실행하면 에러가 발생합니다.

즉, AbstractDao에서 11번째 라인 내용을 보면 Spring boot에서는 persistence.xml 파일이 없기 때문에 EntityManagerFactory를 취득하지 못합니다.

76번째 라인과 116번째 라인을 보면 EntityManagerFactory에서 EntityManager를 취득하고 transaction를 받아와서 실행하는데 역시 EntityManagerFactory를 취득을 못해 에러가 발생했기 때문에 전부 에러가 발생하는 것입니다.


그러면 EntityManagerFactory를 취득하면 해결이 되는데.. EntityManagerFactory를 어디서 취득할까 하면 ApplicationConfig 클래스, 즉 @Configuration 어노테이션이 설정되어 있는 클래스에서 의존성 주입으로 취득할 수 있습니다.

근데 순서가 FactoryDao가 ApplicationConfig보다 먼저 생성되는 부분이기 때문에 Singleton 패턴으로 해결하면 됩니다.

package com.example.demo.Controller;

import javax.persistence.EntityManagerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.example.demo.dao.FactoryDao;
import com.example.demo.dao.UserDao;

// 설정 어트리뷰트
@Configuration
public class ApplicationConfig {
  // EntityManagerFactory 인스턴스를 의존성 주입으로 받는다.
  @Autowired
  private EntityManagerFactory emf;
  // 싱글톤 설정
  private static ApplicationConfig instance;
  // 생성자
  public ApplicationConfig() {
    // 싱글톤 인스턴스 설정
    ApplicationConfig.instance = this;
  }
  // 싱글톤 인스턴스 취득 함수
  public static ApplicationConfig getInstance() {
    // 싱글톤 인스턴스 리턴
    return ApplicationConfig.instance;
  }
  // EntityManagerFactory 인스턴스 취득 함수
  public EntityManagerFactory getEntityManagerFactory() {
    // EntityManagerFactory 인스턴스 리턴
    return this.emf;
  }
  // Bean 설정, 이름은 UserDao
  @Bean(name = "UserDao")
  public UserDao getUserDao() {
    // FactoryDao에서 UserDao의 인스턴스를 취득한다.
    return FactoryDao.getDao(UserDao.class);
  }
}

다시 AbstractDao를 수정하고 AbstractDao를 상속받은 UserDao 클래스와 Dao 클래스를 관리하는 FactoryDao 클래스를 만듭니다.

package com.example.demo.dao;

import javax.persistence.EntityManager;
import javax.persistence.EntityTransaction;
import com.example.demo.Controller.ApplicationConfig;

// Dao 추상클래스
public abstract class AbstractDao<T> {
  private Class<T> clazz;
  // 람다식을 위한 interface
  protected interface EntityManagerRunable {
    void run(EntityManager em);
  }
  // 람다식을 위한 interface
  protected interface EntityManagerCallable<V> {
    V run(EntityManager em);
  }
  // 생성자를 protected로 설정
  protected AbstractDao(Class<T> clazz) {
    this.clazz = clazz;
  }
  // 클래스 타입을 취득하는 함수
  protected final Class<T> getClazz() {
    return clazz;
  }
  // 테이블에서 key의 조건으로 데이터를 취득한다.
  public T findOne(Object id) {
    return transaction((em) -> {
      return em.find(clazz, id);
    });
  }
  // Entity를 데이터 베이스에 Insert한다.
  public T create(T entity) {
    return transaction((em) -> {
      em.persist(entity);
      return entity;
    });
  }
  // Entity를 데이터 베이스에 Update한다.
  public T update(T entity) {
    return transaction((em) -> {
      // 클래스를 데이터베이스 데이터와 매핑시킨다.
      em.detach(entity);
      // update
      return em.merge(entity);
    });
  }
  // Entity를 데이터 베이스에 Delete한다.
  public void delete(T entity) {
    transaction((em) -> {
      // 클래스를 데이터베이스와 매핑시킨다.
      em.detach(entity);
      // 그리고 update를 하고 삭제한다.
      em.remove(em.merge(entity));
    });
  }
  // 반환 값이 있는 트랜젝션 (일반 트랜젹션으로 데이터가 변화한다.)
  public <V> V transaction(EntityManagerCallable<V> callable) {
    return transaction(callable, false);
  }
  // 반환 값이 있는 트랜젝션 (readonly를 true 설정하면 함수를 호출하는 가운데서는 commit이 발생하지 않는다.)
  public <V> V transaction(EntityManagerCallable<V> callable, boolean readonly) {
    // ApplicationConfig 인스턴스에서 EntityManagerFactory를 취득하여 Manager를 생성한다.
    EntityManager em = ApplicationConfig.getInstance().getEntityManagerFactory().createEntityManager();
    // transaction을 가져온다.
    EntityTransaction transaction = em.getTransaction();
    // 트랜젝션을 시작한다.
    transaction.begin();
    try {
      // 람다식을 실행한다.
      V ret = callable.run(em);
      // readonly가 true면 rollback한다.
      if (readonly) {
        transaction.rollback();
      } else {
        // 트랜젝션을 데이터베이스에 입력한다.
        transaction.commit();
      }
      // 결과를 리턴한다.
      return ret;
      // 에러가 발생할 경우
    } catch (Throwable e) {
      // transaction이 활성 중이라면
      if (transaction.isActive()) {
        // rollback
        transaction.rollback();
      }
      // RuntimeException로 변환
      throw new RuntimeException(e);
    } finally {
      // Manager를 닫는다.
      em.close();
    }
  }
  // 반환 값이 없는 transaction (일반 트랜젹션으로 데이터가 변화한다.)
  public void transaction(EntityManagerRunable runnable) {
    transaction(runnable, false);
  }
  // 반환 값이 없는 트랜젝션 (readonly를 true 설정하면 함수를 호출하는 가운데서는 commit이 발생하지 않는다.)
  public void transaction(EntityManagerRunable runnable, boolean readonly) {
    // ApplicationConfig 인스턴스에서 EntityManagerFactory를 취득하여 Manager를 생성한다.
    EntityManager em = ApplicationConfig.getInstance().getEntityManagerFactory().createEntityManager();
    // transaction을 가져온다.
    EntityTransaction transaction = em.getTransaction();
    // 트랜젝션을 시작한다.
    transaction.begin();
    try {
      // 람다식을 실행한다.
      runnable.run(em);
      // readonly가 true면 rollback한다.
      if (readonly) {
        transaction.rollback();
      } else {
        // 트랜젝션을 데이터베이스에 입력한다.
        transaction.commit();
      }
      // 에러가 발생할 경우
    } catch (Throwable e) {
      // transaction이 활성 중이라면
      if (transaction.isActive()) {
        // rollback
        transaction.rollback();
      }
      // RuntimeException로 변환
      throw new RuntimeException(e);
    } finally {
      // Manager를 닫는다.
      em.close();
    }
  }
}

transaction 함수에서 ApplicationConfig의 싱글톤 인스턴스를 취득해서 EntityManagerFactory를 취득해옵니다. 그리고 EntityManager 인스턴스를 취득합니다.

그외는 transaction의 내용은 같습니다. 람다식의 인터페이스를 파라미터로 받아서 함수를 실행합니다.

package com.example.demo.dao;

import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;

public class FactoryDao {
  // FactoryDao 클래스의 singleton 패턴의 인스턴스 변수
  private static FactoryDao instance = null;
  // Dao 클래스의 인스턴스를 가지고 있을 flyweight 패턴의 맵
  private final Map<Class<?>, AbstractDao<?>> flyweight;

  // Singleton 패턴을 유지하기 위해 생성자를 private 타입으로 선언한다.
  private FactoryDao() {
    // flyweight 패턴의 맵의 인스턴스 생성
    flyweight = new HashMap<Class<?>, AbstractDao<?>>();
  }
  @SuppressWarnings("unchecked")
  // DAO 인스턴스를 취득하기 위한 Singleton 패턴의 함수
  public static <T> T getDao(Class<T> clz) {
    try {
      // FactoryDao의 인스턴스가 없으면 생성한다.
      if (instance == null) {
        // 인스턴스 생성
        instance = new FactoryDao();
      }
      // FactoryDao의 flyweight 맵에서 파라미터의 클래스 타입의 DAO가 존재하지 않을 경우
      if (!instance.flyweight.containsKey(clz)) {
        // Reflection 기능으로 생성자를 찾는다.
        Constructor<T> constructor = clz.getDeclaredConstructor();
        // 접근 제한자와 관계없이 접근 가능하게 설정
        constructor.setAccessible(true);
        // flyweight에 클래스 타입을 키로 설정하고 인스턴스를 저장한다.
        instance.flyweight.put(clz, (AbstractDao<?>) constructor.newInstance());
      }
      // flyweight에 저장된 DAO 인스턴스를 리턴한다.
      return (T) instance.flyweight.get(clz);
    } catch (Throwable e) {
      // 에러가 발생
      throw new RuntimeException(e);
    }
  }
}

FactoryDao는 Reflection 기능을 사용해서 클래스의 타입을 받아서 flyweight 패턴으로 인스턴스를 저장하고 가져오는 형식의 클래스입니다.

UserDao는 데이터베이스에 접속해서 데이터를 취득하는 클래스입니다.

package com.example.demo.dao;

import java.util.List;
import javax.persistence.NoResultException;
import javax.persistence.Query;
import com.example.demo.model.User;

// User 데이터 Dao 클래스, AbstractDao를 상속받고 제네릭은 User 클래스를 설정한다.
public class UserDao extends AbstractDao<User> {
  // 생성자 재정의 protected에서 private으로 바꾸고 파라미터를 재설정한다.
  private UserDao() {
    // protected 생성자 호출
    super(User.class);
  }
  // 테이블의 전체 레코드 취득 함수
  @SuppressWarnings("unchecked")
  public List<User> findAll() {
    // AbstractDao 추상 클래스의 transaction 함수를 사용한다.
    return transaction((em) -> {
      try {
        // User 클래스의 @NamedQuery 쿼리로 취득
        Query query = em.createNamedQuery("User.findAll", User.class);
        // 결과 리턴
        return (List<User>) query.getResultList();
      } catch (NoResultException e) {
        return null;
      }
    });
  }
  // Id에 의한 데이터를 취득한다.
  public User selectById(String id) {
    // AbstractDao 추상 클래스의 transaction 함수를 사용한다.
    return super.transaction((em) -> {
      // 쿼리를 만든다. (실무에서는 createQuery가 아닌 createNamedQuery를 사용해서 Entity에서 쿼리를 일괄 관리한다.)
      Query query = em.createQuery("select u from User u where u.id = :id");
      // 파라미터 설정
      query.setParameter("id", id);
      try {
        // 결과 리턴
        return (User) query.getSingleResult();
      } catch (NoResultException e) {
        // 데이터가 없어서 에러가 발생하면 null를 리턴
        return null;
      }
    });
  }
}

이제 HomeController에서 UserDao를 의존성 주입으로 인스턴스를 취득하고 데이터베이스로부터 데이터를 취득하여 화면에 출력합니다.

package com.example.demo.Controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import com.example.demo.dao.UserDao;

// 컨트럴러 어트리뷰트
@Controller
// Controller 클래스
public class HomeController {
  // 의존성 주입
  @Autowired
  // ApplicationConfig 클래스에서 설정한 bean-id
  @Qualifier("UserDao")
  private UserDao userdao;
  // 매핑 주소
  @RequestMapping(value = { "/", "/index.html" })
  public String index(Model model) {
    // UserDao를 이용해서 nowonbun로 데이터를 취득한다.
    // 데이터를 템플릿에 전달한다.
    model.addAttribute("data", userdao.selectById("nowonbun"));
    // 템플릿 파일명
    return "Home/index";
  }
}

Repository 인터페이스가 아닌 Dao 클래스로 데이터를 취득했습니다.


전체적인 프로젝트 구조는 다음과 같습니다.

이전 Spring Web Framework에서 구성하던 방법으로 구성되었습니다. 물론 이전의 Repository 방식이 틀리다는 것은 아닙니다. 각자의 개발 스타일이 있으니 편한 방법으로 사용하면 됩니다.

저의 경우는 transaction을 관리할 수 있고 각 쿼리로 직접 데이터베이스의 데이터를 관리하는 방법이 좀 더 직관적이고 관리하기 편해서 저는 이런 방법을 선호합니다.


여기까지 Spring boot의 JPA에서 EntityManager를 사용하는 방법에 대한 글이었습니다.


궁금한 점이나 잘못된 점이 있으면 댓글 부탁드립니다.