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