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


Study/Java  2021. 6. 22. 12:09

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


이 글은 JPA 프로젝트에서 DAO 클래스 작성하기에 대한 글입니다.


이전 글까지 JPA 프레임워크를 사용하는 방법에 대해서 설명헀습니다. 이번 글에서는 JPA를 실제 프로젝트에서 어떻게 사용할까에 대한 내용입니다.

JPA 프레임워크에서 transaction 공통 함수를 작성하고 그대로 웹 프로젝트(Servlet이나 Spring 프레임워크)의 Controller에서 사용해도 문제는 없습니다.

예를 들면 Controller의 요청이 올 때, 해당 데이터를 데이터베이스에서 취득하고 그 데이터를 Entity 클래스에 넣어서 클라이언트(브라우져)에 응답해도 됩니다.


그러나 실무에서 웹 프로젝트를 작성할 때는 몇가지 룰이 있습니다.

먼저, 웹 요청이 올 때는 해당 요청에서 사용될 데이터베이스의 데이터는 한번에 요청한다. 그런 다음, 세션에 저장할 데이터 또는 취득할 데이터를 취득하고 최대한 간결한 로직을 구현한 후에 응답한다.

왜 이런 룰를 만들게 되냐면 웹이라는 것을 Controller에서 처리가 너무 많아지면 응답 속도가 엄청 느려집니다. 사용자의 특성상 페이지를 요청했는데 1초 이상만 걸려도 무슨 문제가 있다고 인지를 하기 때문에 처리 속도가 느려지게 되면 안됩니다.

그러다 보니 가장 시간이 많이 걸리는 처리(Database connection)을 가장 앞에다 배치를 하고 그 다음은 처리 로직을 넣는 것인데.. 그렇게 해야 디버그를 하던 프로파일링을 해서 성능 측정이 간편하기 때문입니다.

데이터베이스에서 데이터를 취득하고 Controller에서 사용하는 데이터로 변환해 주는 작업이 DAO(Database access object) 클래스 작성하기 입니다.


웹에서는 두가지 데이터 변환 오브젝트가 있습니다. 그것이 DTO와 DAO입니다. DTO는 클라이언트(브라우저)에서 요청되는 데이터 값을 클래스 인스턴스 형태로 바꾸어 주는 오브젝트로 이 부분은 Spring에서 자동으로 처리해 줍니다.

링크 - [Java] 40. Web Spring framework에서 Controller를 다루는 방법


DAO는 데이터베이스의 데이터를 클래스 인스턴스 값으로 변환해 주는 것입니다. 이것도 일부 JPA에서 처리를 해줍니다. 그러나 데이터를 여러가지 Join하거나 복합적으로 취득해야 할 경우를 위해서 DAO 클래스를 만듭니다.

package dao;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;

// Dao 추상클래스
public abstract class AbstractDao<T> {
  // FactoryManager를 생성합니다. "JpaExample"은 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를 생성한다.
    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를 생성한다.
    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 클래스를 만들기 위해서 공통적으로 사용되는 추상 클래스를 만들었습니다.

먼저 이 추상 클래스에는 DAO에서 데이터베이스로부터 데이터를 받기 위해 자주 사용되는 transaction 공통 함수를 만들었습니다. 이 transaction 공통 함수는 이전 글에서 설명했습니다.

링크 - [Java] 49. JPA에서 트랜잭션(transaction) 다루기와 공통 함수 만들기(옵서버 패턴)


그리고 Controller등에서 자주 사용되는 데이터 삽입(create), 수정(update), 삭제(delete) 함수를 공통으로 만들었습니다.

생성자는 protected로 상속받는 클래스에서 재정의하게 끔 작성하였고, 클래스에 재네릭 타입을 넣어서 이 함수의 반환 값과 파라미터의 데이터 타입을 일치시켰습니다.


이제 UserDao 클래스와 InfoDao 클래스, Info2Dao 클래스, PermissionDao 클래스를 만들어 봅시다.

package dao;

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

// User 데이터 Dao 클래스, AbstractDao를 상속받고 제네릭은 User 클래스를 설정한다.
public class UserDao extends AbstractDao<User> {
  // 생성자 재정의 protected에서 public으로 바꾸고 파라미터를 재설정한다.
  public UserDao() {
    // protected 생성자 호출
    super(User.class);
  }

  // 모든 데이터 취득
  @SuppressWarnings("unchecked")
  public List<User> selectAll() {
    // AbstractDao 추상 클래스의 transaction 함수를 사용한다.
    return super.transaction((em) -> {
      // 쿼리를 만든다. (실무에서는 createQuery가 아닌 createNamedQuery를 사용해서 Entity에서 쿼리를 일괄 관리한다.)
      Query query = em.createQuery("SELECT u FROM User u");
      // 결과 리턴
      return (List<User>) query.getResultList();
    });
  }

  // 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;
      }
    });
  }
}

위 예제는 UserDao 클래스 예제입니다. 즉, Dao에서 User 데이터를 취득해서 User 클래스로 반환하는 형태의 클래스입니다.

먼저 extends로 상속할 때, 제네릭을 User 클래스로 설정합니다. 그래야 create 함수와 update 함수, delete 함수의 파라미터와 리턴 타입이 User 클래스 타입으로 일치됩니다.

생성자는 추상 클래스가 protected 타입이였기 때문에 public으로 바꾸고, 추상 클래스의 생성자를 호출합니다.

그러면 최종적으로 위 이미지와 같은 구조가 작성됩니다.

Main에서 사용해 보겠습니다.

import dao.UserDao;
import model.User;

public class Main {
  // 실행 함수
  public static void main(String... args) {
    // User Dao 인스턴스 생성
    UserDao userdao = new UserDao();
    // User 인스턴스 생성
    User user = new User();
    // 데이터 입력
    user.setId("test111");
    user.setName("test111");
    user.setIsDeleted(false);
    
    // 데이터 생성
    userdao.create(user);
    // user 변수를 null 한다.
    user = null;
    
    // 데이터베이스에서 데이터를 취득한다.
    user = userdao.selectById("test111");
    // 콘솔 출력
    System.out.println(user.getName());
    // 데이터를 삭제한다.
    userdao.delete(user);
    // 다시 검색한다.
    user = userdao.selectById("test111");
    // null 이면
    if(user == null) {
      // 콘솔 출력
      System.out.println("User == null");
    }
  }
}

실행 함수에서 UserDao 클래스의 인스턴스를 생성하고 먼저 데이터베이스에 데이터를 추가할 User 인스턴스를 만들었습니다.

그리고 데이터를 insert하고 다시 select로 취득했습니다. 그리고 삭제까지 처리하는데 아무런 문제없이 됩니다.


위 DAO는 Transaction 테이블일 때의 형태입니다. Transaction 테이블이란 Transaction 이 빈번하게 일어나는 테이블을 뜻합니다. 즉, 데이터의 추가, 수정, 삭제가 자주 발생하는 테이블입니다.

Master 테이블의 DAO 구조는 조금 틀립니다. Master 테이블은 보통 SA(System admin)이 직접 데이터베이스로 추가, 수정, 삭제가 일어나고 그 빈도가 매우 적습니다.

package dao;

import java.util.List;
import javax.persistence.Query;
import model.Permission;

// Permission 데이터 Dao 클래스, AbstractDao를 상속받고 제네릭은 Permission 클래스를 설정한다.
public class PermissionDao extends AbstractDao<Permission> {
  // 마스터 데이터
  private static List<Permission> singleton = null;

  // 생성자 재정의 protected에서 public으로 바꾸고 파라미터를 재설정한다.
  public PermissionDao() {
    // protected 생성자 호출
    super(Permission.class);
    // singleton에 데이터가 없으면 재설정한다.
    if(singleton == null) {
      reflesh();  
    }
  }

  // 리스트 재갱신
  @SuppressWarnings("unchecked")
  public void reflesh() {
    // AbstractDao 추상 클래스의 transaction 함수를 사용한다.
    super.transaction((em) -> {
      // 쿼리를 만든다.
      Query query = em.createNamedQuery("Permission.findAll");
      // 결과 리턴
      singleton = (List<Permission>) query.getResultList();
    });
  }
  // Basic 권한 마스터 데이터 취득
  public Permission BasicPermission() {
    // 취득
    return singleton.stream().filter(x -> "BASC".equals(x.getCode())).findAny().get();
  }
}

Master 데이터는 데이터 변경이 적기 때문에 사용할 때마다 connection으로 데이터를 취득하는 것보다는 위처럼 처음 한번만 취득해 놓고 메모리상에 올려놓고 사용하는 편이 훨씬 성능상의 이점이 있습니다.

import java.util.ArrayList;
import java.util.List;
import dao.PermissionDao;
import dao.UserDao;
import model.Permission;
import model.User;

public class Main {
  // 실행 함수
  public static void main(String... args) {
    // User Dao 인스턴스 생성
    UserDao userdao = new UserDao();
    // Permission Dao 인스턴스 생성
    PermissionDao permissiondao = new PermissionDao();
    // User 인스턴스 생성
    User user = new User();
    // 데이터 입력
    user.setId("test111");
    user.setName("test111");
    user.setIsDeleted(false);
    // Permission 리스트 생성
    List<Permission> permissions = new ArrayList<>();
    // basic 퍼미션 추가
    permissions.add(permissiondao.BasicPermission());
    // 설정
    user.setPermissions(permissions);

    // 데이터 생성
    userdao.create(user);
  }
}

위 처럼 permissiondao.BasicPermission()를 통해서 간단하게 permission 인스턴스를 취득해서 사용할 수 있습니다.

만약, SA에 의해 데이터가 변경되었을 경우 dao의 reflesh 함수를 호출하게 되면 프로그램상에서 다시 Master 테이블이 재정의가 될 것입니다.

근데 사실 Master 테이블이 변경될 정도면 프로그램 내용도 바뀌였을 확율이 높으니 보통은 재기동으로 할 듯 싶네요.


여기까지 JPA 프로젝트에서 Dao 클래스 작성하기에 대한 글이었습니다.


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