그만큼 탄탄하게 만들어졌고, 최대한 슬림하게 만드려고 한 노력이 돋보이기도 합니다.
그럼 우선 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
댓글 없음:
댓글 쓰기