2012년 12월 7일 금요일

Spring DI를 내 맘대로 사용하기

Spring Framework는 워낙 잘 만들어져, 흠잡을데가 없다고 봅니다. ㅋ
그만큼 탄탄하게 만들어졌고, 최대한 슬림하게 만드려고 한 노력이 돋보이기도 합니다.

그럼 우선 IoC/DI 분야에서는 Spring이 거의 산업표준이고, Java 개발자라면 필수적으로 사용하는 것이지만, 요즘은 더 가볍고, 코드로 DI를 정의하는 google guice 의 도전을 받고 있습니다.
이에 Spring 또한 Java 표준을 준수하고, 코드로 DI를 정의할 수 있는 기능을 3.0 이후 버전에서 지원하므로, 굳이 guice 를 쓸 필요가 있을까 싶습니다.
guice 와의 비교는 googling 해 보시면 많이 나와 있으니 생략하기로 하고...

이번 글의 주제인 Spring ApplicationContext 를 수동적으로 사용하지 않고, 좀 더 적극적으로 사용하는 방식에 대해 설명하겠습니다.

우선 전체 소스를 보면 (제가 소스 별로 나눠서 쓰게 되면, 편집하는 게 어려워서 포기했습니다. 양해를 구합니다.)


package kr.kth.commons.spring3;

import kr.kth.commons.base.Action0;
import kr.kth.commons.base.AutoCloseableAction;
import kr.kth.commons.base.Guard;
import kr.kth.commons.tools.StringTool;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionValidationException;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.context.support.GenericXmlApplicationContext;

import javax.annotation.concurrent.ThreadSafe;
import java.util.Map;
import java.util.Stack;

import static kr.kth.commons.base.Guard.shouldNotBeNull;

/**
 * Spring Framework 의 Dependency Injection을 담당하는 클래스입니다.
 * User: sunghyouk.bae@gmail.com
 * Date: 12. 11. 23.
 */
@Slf4j
@ThreadSafe
public final class Spring {

 public static final String DEFAULT_APPLICATION_CONTEXT_XML = "applicationContext.xml";

 private static final String LOCAL_SPRING_CONTEXT = "kr.kth.commons.spring3.Spring.globalContext";
 private static final String NOT_INITIALIZED_MSG =
  "Spring ApplicationContext가 초기화되지 않았습니다. 사용하기 전에  Spring.init() 을 호출해주기시 바랍니다.";

 private static final Object syncLock = new Object();
 private static volatile GenericApplicationContext globalContext;
 static ThreadLocal> localContextStack = new ThreadLocal<>();


 public static synchronized boolean isInitialized() {
  return (globalContext != null);
 }

 public static synchronized boolean isNotInitialized() {
  return (globalContext == null);
 }

 private static synchronized void assertInitialized() {
  Guard.assertTrue(isInitialized(), NOT_INITIALIZED_MSG);
 }

 public static synchronized GenericApplicationContext getContext() {
  GenericApplicationContext context = getLocalContext();
  if (context == null)
   context = globalContext;
  Guard.assertTrue(context != null, NOT_INITIALIZED_MSG);
  return context;
 }

 private static synchronized GenericApplicationContext getLocalContext() {
  if (getLocalContextStack().size() == 0)
   return null;
  return getLocalContextStack().peek();
 }

 private static synchronized Stack getLocalContextStack() {
  if (localContextStack.get() == null) {
   localContextStack.set(new Stack());
  }
  return localContextStack.get();
 }

 public static synchronized void init() {
  init(DEFAULT_APPLICATION_CONTEXT_XML);
 }

 public static synchronized void init(String... resourceLocations) {
  if (log.isDebugEnabled())
   log.debug("Spring Context 를 초기화합니다. resourceLocations=[{}]",
             StringTool.listToString(resourceLocations));
  init(new GenericXmlApplicationContext(resourceLocations));
 }

 public static synchronized void init(GenericApplicationContext applicationContext) {
  shouldNotBeNull(applicationContext, "applicationContext");

  if (globalContext == null) {
   if (log.isInfoEnabled())
    log.info("Spring ApplicationContext 를 초기화 작업을 시작합니다...");

   globalContext = applicationContext;

   if (log.isInfoEnabled())
    log.info("Spring ApplicationContext를 초기화 작업을 완료했습니다.");
  } else {
   if (log.isWarnEnabled())
    log.warn("이미 Spring ApplicationContext를 초기화 했으므로, 무시합니다.");
  }
 }

 public static synchronized void initByAnnotatedClasses(Class... annotatedClasses) {
  init(new AnnotationConfigApplicationContext(annotatedClasses));
 }

 public static synchronized void initByPackages(String... basePackages) {
  init(new AnnotationConfigApplicationContext(basePackages));
 }


 public static AutoCloseableAction useLocalContext(final GenericApplicationContext localContext) {
  shouldNotBeNull(localContext, "localContext");

  if (log.isDebugEnabled())
   log.debug("로컬 컨텍스트를 사용하려고 합니다... localContext=[{}]", localContext);

  synchronized (syncLock) {
   getLocalContextStack().push(localContext);

   return new AutoCloseableAction(new Action0() {
    @Override
    public void perform() {
     reset(localContext);
    }
   });
  }
 }

 public static synchronized void reset(final GenericApplicationContext contextToReset) {
  if (contextToReset == null) {
   globalContext = null;

   if (log.isInfoEnabled())
    log.info("Global Spring Context 를 Reset 했습니다!!!");

   return;
  }

  if (log.isDebugEnabled())
   log.debug("ApplicationContext=[{}] 을 Reset 합니다...", contextToReset);

  synchronized (syncLock) {
   if (getLocalContext() == contextToReset) {
    getLocalContextStack().pop();

//    if (getLocalContextStack().size() == 0)
//     Local.put(LOCAL_SPRING_CONTEXT, null);

    if (log.isDebugEnabled())
     log.debug("Local Application Context 를 Reset 했습니다.");
    return;
   }

   if (globalContext == contextToReset) {
    globalContext = null;

    if (log.isInfoEnabled())
     log.info("Global Application Context 를 리셋했습니다!!!");
   }
  }
 }

 public static synchronized void reset() {
  if (getLocalContext() != null)
   reset(getLocalContext());
  else
   reset(globalContext);
 }


 public static synchronized Object getBean(String name) {
  if (log.isDebugEnabled())
   log.debug("ApplicationContext로부터 Bean을 가져옵니다. beanName=[{}]", name);

  return getContext().getBean(name);
 }

 public static synchronized Object getBean(String name, Object... args) {
  if (log.isDebugEnabled())
   log.debug("ApplicationContext로부터 Bean을 가져옵니다. beanName=[{}], args=[{}]", name, StringTool.listToString(args));

  return getContext().getBean(name, args);
 }

 public static synchronized  T getBean(Class beanClass) {
  if (log.isDebugEnabled())
   log.debug("ApplicationContext로부터 Bean을 가져옵니다. beanClass=[{}]", beanClass.getName());

  return getContext().getBean(beanClass);
 }

 public static synchronized  T getBean(String name, Class beanClass) {
  if (log.isDebugEnabled())
   log.debug("ApplicationContext로부터 Bean을 가져옵니다. beanName=[{}], beanClass=[{}]", name, beanClass);

  return getContext().getBean(name, beanClass);
 }

 public static synchronized  String[] getBeanNamesForType(Class beanClass) {
  if (log.isDebugEnabled())
   log.debug("해당 수형의 모든 Bean의 이름을 조회합니다. beanClass=[{}]", beanClass.getName());

  return getContext().getBeanNamesForType(beanClass);
 }

 public static synchronized  String[] getBeanNamesForType(Class beanClass,
                                                             boolean includeNonSingletons,
                                                             boolean allowEagerInit) {
  if (log.isDebugEnabled())
   log.debug("해당 수형의 모든 Bean의 이름을 조회합니다. beanClass=[{}], includeNonSingletons=[{}], allowEagerInit=[{}]",
             beanClass.getName(), includeNonSingletons, allowEagerInit);

  return getContext().getBeanNamesForType(beanClass, includeNonSingletons, allowEagerInit);
 }

 public static synchronized  Map getBeansOfType(Class beanClass) {
  if (log.isDebugEnabled())
   log.debug("해당 수형의 모든 Bean을 조회합니다. beanClass=[{}]", beanClass.getName());

  return getContext().getBeansOfType(beanClass);
 }

 public static synchronized  Map getBeansOfType(Class beanClass,
                                                              boolean includeNonSingletons,
                                                              boolean allowEagerInit) {
  if (log.isDebugEnabled())
   log.debug("해당 수형의 모든 Bean을 조회합니다. beanClass=[{}], includeNonSingletons=[{}], allowEagerInit=[{}]",
             beanClass.getName(), includeNonSingletons, allowEagerInit);

  return getContext().getBeansOfType(beanClass,
                                     includeNonSingletons,
                                     allowEagerInit);
 }

 public static synchronized  T getOrRegisterBean(Class beanClass) {
  return getOrRegisterBean(beanClass, ConfigurableBeanFactory.SCOPE_SINGLETON);
 }

 public static synchronized  T getOrRegisterBean(Class beanClass, String scope) {
  Map beans = getBeansOfType(beanClass, true, true);
  if (beans.size() > 0)
   return beans.values().iterator().next();

  registerBean(beanClass.getName(), beanClass, scope);
  return getContext().getBean(beanClass);
 }

 public static synchronized boolean isBeanNameInUse(String beanName) {
  return getContext().isBeanNameInUse(beanName);
 }

 public static synchronized boolean isRegisteredBean(String beanName) {
  return getContext().isBeanNameInUse(beanName);
 }

 public static synchronized  boolean isRegisteredBean(Class beanClazz) {
  try {
   return (getContext().getBean(beanClazz) != null);
  } catch (Exception e) {
   return false;
  }
 }

 public static synchronized  void registerBean(String beanName, Class beanClass) {
  registerBean(beanName, beanClass, ConfigurableBeanFactory.SCOPE_SINGLETON);
 }

 public static synchronized  void registerBean(String beanName, Class beanClass, String scope) {
  if (isBeanNameInUse(beanName))
   throw new BeanDefinitionValidationException("이미 등록된 Bean입니다. beanName=" + beanName);

  if (log.isDebugEnabled())
   log.debug("새로운 Bean을 등록합니다. beanName=[{}], beanClass=[{}], scope=[{}]", beanName, beanClass, scope);

  BeanDefinition definition = new RootBeanDefinition(beanClass);
  definition.setScope(scope);

  getContext().registerBeanDefinition(beanName, definition);
 }

 public static synchronized void registerBean(String beanName, BeanDefinition beanDefinition) {

  if (isBeanNameInUse(beanName))
   throw new BeanDefinitionValidationException("이미 등록된 Bean입니다. beanName=" + beanName);

  if (log.isDebugEnabled())
   log.debug("새로운 Bean을 등록합니다. beanName=[{}], beanDefinition=[{}]", beanName, beanDefinition);

  getContext().registerBeanDefinition(beanName, beanDefinition);
 }

 public static synchronized void removeBean(String beanName) {
  if (isBeanNameInUse(beanName)) {
   if (log.isDebugEnabled())
    log.debug("ApplicationContext에서 Name=[{}] 인 Bean을 제거합니다.", beanName);
   getContext().removeBeanDefinition(beanName);
  }
 }

 public static synchronized  void removeBean(Class beanClass) {
  if (log.isDebugEnabled())
   log.debug("Bean 형식 [{}] 의 모든 Bean을 ApplicationContext에서 제거합니다.", beanClass.getName());

  String[] beanNames = getContext().getBeanNamesForType(beanClass, true, true);
  for (String beanName : beanNames)
   removeBean(beanName);
 }

 public static synchronized Object tryGetBean(String beanName) {
  try {
   return getBean(beanName);
  } catch (Exception e) {
   if (log.isWarnEnabled())
    log.warn("bean을 찾는데 실패했습니다. null을 반환합니다.", e);
   return null;
  }
 }

 public static synchronized Object tryGetBean(String beanName, Object... args) {
  try {
   return getBean(beanName, args);
  } catch (Exception e) {
   if (log.isWarnEnabled())
    log.warn("bean을 찾는데 실패했습니다. null을 반환합니다.", e);
   return null;
  }
 }

 public static synchronized  T tryGetBean(Class beanClass) {
  try {
   return getBean(beanClass);
  } catch (Exception e) {
   if (log.isWarnEnabled())
    log.warn("bean을 찾는데 실패했습니다. null을 반환합니다.", e);
   return (T) null;
  }
 }

 public static synchronized  T tryGetBean(String beanName, Class beanClass) {
  try {
   return getBean(beanName, beanClass);
  } catch (Exception e) {
   if (log.isWarnEnabled())
    log.warn("bean을 찾는데 실패했습니다. null을 반환합니다.", e);
   return (T) null;
  }
 }
}


Spring.init() 함수를 이용해서, ApplicationContext를 초기화합니다. 아시다시피 xml 도 되고, java class로 정의한 Confiration도 됩니다. 또한 여러 개의 Configuration class 에 대한 것도 되고, 아예 사용자가 ApplicationContext 를 생성하고, 지정할 수도 있습니다.

이 후에 getBean() 이나 getBeansOfType() 은 GenericApplicationContext 에서 제공하는 것입니다.

여기서 제가 제안하는 것은 getOrRegisterBean() 처럼 만약 등록되지 않는 Bean이 있다면, 새로 등록해서 사용하게 하고, tryGetBean 처럼 bean 이 없는 경우 예외를 발생시키는 것이 아니라 null 을 반환하도록 할 수 있는 메소드를 제공합니다.

이런 시도는 IoC ( Inversion Of Control ) 의 환경을 또 한번 뒤엎어본다는 것입니다. ㅎㅎ
환경설정에서 설정한대로 움직이지만, 만약 환경설정에서 빼 먹은 것이 있다면, 코드 상에서 기본값으로 처리하도록 하는 것입니다. (논란의 여지가 있죠? ㅎㅎ )

또 하나는 Spring 의 Bean 의 Lifecycle을 Singleton, Prototype 이 주로 사용되지만, 웹 환경에서는   Session 도 가능하게 되는데, 이렇게 다양한 Lifecycle 로직을 잘 활용하게 되면, 개발자가 직접 인스턴스의 Lifecycle을 관리하는 게 아니라 ApplicationContext 의 Bean에 대한 Lifecycle 관리 기능을 사용하자는 의미가 있습니다.

아쉽게도 Spring 이나 guice 모두 Lifecycle 종류 중에 Thread 별로 Bean을 관리해주는 기능은 없네요... 이게 있으면 좋겠지만... 요건 ThreadLocal 과 조합하면 제대로 표현이 가능할 것입니다^^

댓글 없음: