레이블이 IoC인 게시물을 표시합니다. 모든 게시물 표시
레이블이 IoC인 게시물을 표시합니다. 모든 게시물 표시

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 과 조합하면 제대로 표현이 가능할 것입니다^^

2011년 10월 31일 월요일

Castle.Windsor 를 이용한 Decorator 패턴

Castle.Windsor 는 IoC/DI 라이브러리 중에 .NET 계열에서는 유명하고, 많이 사용하는 라이브러리 중에 한가지입니다. NHibernate 초기에 DynamicProxy 를 Castle.DynamicProxy 를 사용하여, 많이 유명해졌지요.
지금은 NHibernate도 독립적으로 ProxyFactory를 운용하고, Microsoft 사까지 Unity 라는 IoC/DI 라이브러리를 제공하고 있어, 어떤 것이 더 좋냐? 이런 건 무의미하고, 각자 자신에게 맞는 걸 사용하면 되겠습니다.



 

  
   
    ${DataService.Northwind}
    ${RequestSerializer.Northwind}
    ${ResponseSerializer.Northwind}
   
  

  
   
    ${DataService.Pubs}
    ${RequestSerializer.Pubs}
    ${ResponseSerializer.Pubs}
   
  
  
  

  
   
    ${AdoRepository.Northwind}
   
  

  
   
    ${AdoRepository.Pubs}
   
  

  
  

  
   
    ${JsonSerializer.Request}
    ${Compressor.SharpBZip2}
   
  

  
   
    ${JsonSerializer.Response}
    ${Compressor.SharpBZip2}
   
  

  
   
    ${RequestSerializer.Compress}
    ${Encryptor.Aria}
   
  

  
   
    ${ResponseSerializer.Compress}
    ${Encryptor.Aria}
   
  


  
   
    ${BsonSerializer.Request}
    ${Compressor.SevenZip}
   
  

  
   
    ${BsonSerializer.Response}
    ${Compressor.SevenZip}
   
  
  
  

  
  

  
  

  
  

  
  
  
  
  

  
  

 
 

위의 코드는 Castle.Windsor 를 이용하여, Dependency Injection을 좀 복잡하게 사용한 예입니다. 뭐 이렇게 할 필요가 있나 싶겠지만, 제품을 만들다 보면, Customizing을 해야 할 게 아니라, 위와 같이 Configuration을 변경하도록 하는 것이 제품 완성도를 높인다고 믿고 있는 터라 꼭 이런 방식을 사용합니다.

아니 이런 Decorator 패턴에 대해 쓴다는 걸 서론만 쓰고, 그만 뒀네요... 코드 처리하는데 정신이 팔려서리... 쩝...
자 그럼 코드를 좀 볼까요? 상위에 있는 놈들은 모두 Component의 속성이나 인자값으로 다른 Component 를 주입하는 Dependency Injection이라 합니다.

자 여기서 Decorator 패턴을 사용한 예는?
Component 중에 "RequestSerializer.Pubs" 를 보십시요. 이 놈은 Serializer 로 "RequestSerializer.Compress" 를 Wrapping 하여 ARIA 암호화를 수행하는 Decorator 입니다.
"RequestSerializer.Compress" 는 "BsonSerializer.Request" 를 Wrapping 하고, SevenZip 알고리즘으로 압축을 수행합니다.
자 이제 정리하면 BSON->Compress->암호화 로 래핑되도록 되었습니다. 물론 순서는 바꿀 수 있고, Decorator를 더 추가할 수도 있습니다.

이렇게 환경설정에서 IoC/DI 기능 중에 Decorator 패턴을 활용하게 되면, 상당히 복잡한 구성도 쉽게 구성할 수 있고, 다양한 조합의 Component를 제공할 수 있습니다.

2009년 4월 25일 토요일

ASP.NET MVC 와 Castle Windsor

ASP.NET MVC 의 Controller를 Castle Windsor (IoC) 컨테이너를 활용하여 확장할 수 있다는 예제

http://blog.andreloker.de/post/2009/03/28/ASPNET-MVC-with-Windsor-programmatic-controller-registration.aspx

근데... 별로... Windsor를 잘 못하는거 같네...

내가 만든 IoC, IoCHttpApplication, IoCHttpModule 을 사용하는 것이 더 낳을 듯...

2008년 7월 11일 금요일

NHibernate, Castle.NET을 이용한 Service 만들기

NHibernate의 Entity별 Data Access Object (또는 Repository)를 제작하는 것은 상당한 노력이 필요하다.
더 좋은 방법은 UseCase 별로 그룹을 정해서 Facade 패턴을 이용하여 Repository 수를 줄이게 되면 좋다. 그럼 어떻게 하는게 좋은가?

Generic을 이용하여 IRepository 와 Repository 하나만 만들면, Entity의 수에 상관없이 사용할 수 있고, UseCase 별로 그룹핑해서 DataService 를 제작하면 Busiess Layer에서 효과적으로 작업량을 줄이고, 유연성을 확보할 수 있습니다.

또한 Busiess Logic 변경에 따른 유연성 확보를 위해 Castle의 IoC 기능을 이용하면, 전체 틀이 변경되지 않는 한, 환경설정에 따른 다른 Logic을 적용할 수 있습니다.

또한 Decorator Pattern을 이용하게 되면, 기존 작업에 Decoration 작업을 덪 붙일 수도 있습니다.

RCL.Data에서 이 모든 것을 가능하게 했고, 이를 바탕으로 만들어진 RealAdmin 제품이 있습니다.

향후 리얼웹 제품은 모두 위의 두 라이브러리를 이용하여 더욱 확장성과 유연성을 가진 제품으로 Upgrade될 것입니다.