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


Study/Java  2021. 6. 24. 16:40

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


이 글은 Spring 프레임워크에서 DAO를 Factory method Pattern으로 의존성 주입하는 방법에 대한 글입니다.


이전 글에서 Spring 프레임워크에서 JPA ORM의 DAO를 @Autowired의 어트리뷰트를 사용해서 의존성 주입하는 방법에 대해서 설명했습니다.

link - [Java] 51. Spring 프레임워크에서 JPA 사용법(의존성 주입 @Autowired)


Spring Controller에서는 DAO를 취득해서 JPA ORM을 이용해서 데이터를 취득하는 부분에 대해서는 문제가 없습니다.

그런데 문제는 Spring Controller이 아닌 클래스에서 사용하는 것입니다. 물론 일반 클래스에서 그냥 DAO 클래스의 인스턴스를 생성(new)해서 사용해도 문제는 없습니다.


그러나 Spring에서 의존성 주입으로 Singleton 형식으로 사용하는데, 다른 일반 클래스에서도 일반 인스턴스 생성으로 데이터베이스의 접속해서 사용하는 것이 아닌 같이 Singleton 형식으로 사용하고 싶습니다.

그러기 위해서 DAO 클래스를 제어하는 Factory method pattern를 먼저 만드는 게 필요합니다.

package 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 클래스 자체를 Singleton 타입으로 만들었습니다. 즉, 프로그램이 시작되서 FactoryDao의 인스턴스는 단 한개만 생성됩니다.

그리고 getDao는 Factory method patter입니다. 즉, 파라미터의 클래스 타입에 의해 인스턴스를 취득합니다.

그러나 우리가 DAO를 생성할 때마다 FactoryDao에 if나 switch 분기문을 만들기 귀찮기 때문에 파라미터로 받은 클래스 타입으로 Reflection으로 인스턴스를 생성하는 flyweight pattern을 적용했습니다.

다시 정리하면 Singleton + Factory method + flyweight 패턴의 결과입니다.


이제 Spring의 Controller 부분이 아닌 일반 함수 부분에서 DAO를 취득해서 사용해 봅시다.

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에서 private으로 바꾸고 파라미터를 재설정한다.
  private UserDao() {
    // protected 생성자 호출
    super(User.class);
  }

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

import dao.FactoryDao;
import dao.UserDao;
import model.User;
// 일반 클래스
public class Common {
  // id로 받아서 User 이름을 취득하는 함수
  public String getUserNameById(String id) {
    // FactoryDao에서 UserDao의 인스턴스를 취득합니다.
    UserDao userdao = FactoryDao.getDao(UserDao.class);
    // UserDao 클래스의 selectById 함수를 이용해서 User Entity를 취득합니다.
    User user = userdao.selectById(id);
    // 이름을 리턴합니다.
    return user.getName();
  }
}

이제 Controller에서 Common 클래스의 GetUserNameById 함수를 이용해봅시다.

package controller;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.RequestMapping;
import common.Common;

@Controller
public class Home {
  // 요청 url 패턴
  @RequestMapping(value = "/index.html")
  public String index(ModelMap modelmap, HttpSession session, HttpServletRequest req, HttpServletResponse res) {
    // Common 클래스 인스턴스 생성
    Common common = new Common();
    // modelmap에 Common 클래스의 getUserNameById 함수를 이용해서 Name을 취득한다.
    modelmap.addAttribute("Data", common.getUserNameById("nowonbun"));
    // view의 파일명
    return "index";
  }
}

위 결과를 보니 Common 클래스 안에서 UserDao를 이용해서 데이터베이스에 접속하여 데이터를 가져오고 화면에 표시하는게 확인이 됩니다.

여기까지 Spring에서 일반 클래스에서 DAO 인스턴스를 Factory pattern으로 가져오는 것을 확인했습니다.


이제 이 FactoryDao에 있는 DAO 인스턴스를 @Autowired를 통해서 Controller에서 의존성 주입으로 인스턴스를 받아야 합니다.

이전에는 우리가 bean등록을 mvc-config.xml에 등록을 했습니다만, 여기서는 xml에 하는 것이 아니고 클래스로 설정합니다.

package controller;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import dao.FactoryDao;
import dao.UserDao;

// 설정 어트리뷰트
@Configuration
public class ApplicationConfig {
  // Bean 설정, 이름은 UserDao
  @Bean(name = "UserDao")
  public UserDao getUserDao() {
    // FactoryDao에서 UserDao의 인스턴스를 취득한다.
    return FactoryDao.getDao(UserDao.class);
  }
}

위 ApplicationConfig 클래스는 xml에서 설정된 Controller 패키지에 작성합니다.

그리고 @Configuration 어트리뷰트를 설정하고 xml에서 사용된 bean-id를 Bean 어트리뷰트로 설정합니다.


이제 다시 Controller에서 의존성 주입으로 DAO를 받아 보겠습니다.

package controller;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.RequestMapping;
import dao.FactoryDao;
import dao.UserDao;

@Controller
public class Home {
  // 의존성 주입
  @Autowired
  // ApplicationConfig 클래스에서 설정한 bean-id
  @Qualifier("UserDao")
  private UserDao userdao;
  
  // 요청 url 패턴
  @RequestMapping(value = "/index.html")
  public String index(ModelMap modelmap, HttpSession session, HttpServletRequest req, HttpServletResponse res) {
    // 의존성 주입으로 받은 UserDao 인스턴스의 메모리 주소
    System.out.println(userdao);
    // FactoryDao로 받은 UserDao 인스턴스의 메모리 주소
    System.out.println(FactoryDao.getDao(UserDao.class));
    
    // 의존성 주입으로 받은 UserDao 인스턴스로 데이터 취득
    modelmap.addAttribute("Data", userdao.selectById("nowonbun").getName());
    // view의 파일명
    return "index";
  }
}

의존성 주입으로도 UserDao를 이용해 데이터베이스의 값을 제대로 가져옵니다.

콘솔에 찍힌 FactoryDao로 받은 UserDao 인스턴스와 의존성 주입으로 받은 UserDao 인스턴스의 메모리 주소가 같습니다.

그 뜻은 같은 인스턴스라는 뜻입니다.


이렇게 되면 FactoryDao를 통해서 Spring의 의존성 주입 DAO와 일반 클래스에서 사용하는 DAO는 하나로 통일이 가능하네요.

그리고 사실 여기서 하나의 작업을 더해야 하는데, 그것은 DAO의 생성자들을 모두 private로 바꾸는 것입니다.


FactoryDao 클래스를 보면 접근 제한자와 관계없이 인스턴스를 생성할 수 있게 하는 구문(constructor.setAccessible(true))이 있습니다.

즉, DAO 클래스의 생성자를 private로 해도 FactoryDao에서는 문제없이 인스턴스 생성이 가능하다는 뜻입니다.

그리고 DAO의 생성자를 private로 하게 되면 다른 클래스에서는 DAO의 인스턴스를 생성(new) 할 수 없게 됩니다.

이렇게까지 해야 프로젝트 작성 준비가 끝나게 됩니다.


여기까지 Spring 프레임워크에서 DAO를 Factory Pattern으로 의존성 주입하는 방법에 대한 글이었습니다.


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