Bean 은 주로 모듈화된 Logic 클래스를 의미하는데 여기서는 Dao, Bo나 1개의 instance만으로 충분한 Model을 주로 Bean이라고 부르겠다.
이런 클래스는 싱글톤으로 구현하는 경우가 있는데, 테스트 코드를 작성하기 어렵다. 그래서 싱글톤은 피하고, Instance는 하나만 생성하고자 한다. Bean은 다른 Bean을 멤버로 가질 수 있다. 이런 상황에서 Bean을 적절히 초기화하는 것이 관건이다. 이미 http://dajkim76.tistory.com/419 에서 ModuleManager라는 간단한 싱글 인스턴스 매니저를 구현한 적이 있는데 이번에는 조금 더 개선해보겠다.
InjectableBean은 Bean클래스를 위한 annotation이다.
InjectableBean.java 접기
package com.mdiwebma.base.annotation;
/**
* @author djkim
*/
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface InjectableBean {
}
접기
InjectableBean을 사용하고 싶다면 클래스 필드에 @Inject를 사용해 주면 된다.
Inject.java 접기
package com.mdiwebma.base.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author djkim
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Inject {
}
접기
테스트를 위해서 A1, A2, A3는
더보기 접기
@InjectableBean
public class A1 {
private static int count;
public A1() {
Log.e("__D", "A1 crated: " + ++count);
}
}
@InjectableBean
public class A2 {
private static int count;
public A2() {
Log.e("__D", "A2 crated: " + ++count);
}
}
@InjectableBean
public class A3 {
private static int count;
public A3() {
Log.e("__D", "A3 crated: " + ++count);
}
}
접기
N1, N2, N3는
더보기 접기
@InjectableBean
public class N1 {
@Inject
A1 a1;
@Inject
A2 a2;
public N1() {
Log.e("__D", "N1 created");
}
}
@InjectableBean
public class N2 {
@Inject
A2 a2;
@Inject
A3 a3;
public N2() {
Log.e("__D", "N2 created");
}
}
@InjectableBean
public class N3 {
@Inject
N1 n1;
@Inject
N2 n2;
public N3() {
Log.e("__D", "N3 created");
}
}
접기
N4클래스는 N3를 extend한 Bean이다.
더보기 접기
@InjectableBean
public class N4 extends N3{
@Inject
A2 a2;
}
접기
이제 사용법을 보자
더보기 접기
InjectionUtils.registerBean(EventBus.getDefault());
public class Test {
@Inject
private N1 n1;
@Inject
private N2 n2;
@Inject
private N2 n22;
@Inject
private N3 n3;
@Inject
private N4 n4;
@Inject
private Bus bus;
public Test() {
InjectionUtils.injectBean(this);
Log.e("__D", "test start");
AssertUtils.assertTrue(n1 != null);
AssertUtils.assertTrue(n2 != null);
AssertUtils.assertTrue(n22 != null);
AssertUtils.assertTrue(n2 == n22);
AssertUtils.assertTrue(n1.a2 == n2.a2);
AssertUtils.assertTrue(n3 != null);
AssertUtils.assertTrue(n1 == n3.n1);
AssertUtils.assertTrue(n2 == n3.n2);
AssertUtils.assertTrue(n4 != null);
AssertUtils.assertTrue(n4.a2 == n2.a2);
AssertUtils.assertTrue(n4.n1 == n3.n1);
AssertUtils.assertTrue(n4.n2 == n3.n2);
AssertUtils.assertTrue(bus != null);
Log.e("__D", "test done");
}
}
접기
구현시 고려한 점은 다음과 같다.
1. Bean은 싱클톤 클래스가 아니지만(유닛 테스트를 위해서), 프로세스당 하나의 인스턴스만 생성한다. (쓰레드 세이프), 생성자 구현이 없거나, 있다면 기본 생성자는 꼭 있어야 한다.
2. injectBean이 실행되는 시점 즉 @Inject가 붙은 필드를 초기화하는 시점에 필요한 Bean만 초기화한다. (Lazy init)
3. InjectableBean을 가진 클래스를 초기활 때는 super class의 멤버 필드까지 @Inject가 있으면 bean을 생성하고 초기화한다. 사용하는 클래스(예 Test) 효율을 위해서 super class의 @Inject는 초기화하지 않는다.
4. InjectableBean을 가지지 않는 클래스도 Bean으로 등록할 수 있다. 다만 injectBean을 호출하기 전에 registerBean으로 먼저 등록해야 한다.
InjectionUtils.injectBean의 구현을 보자.
더보기 접기
//
// @Inject, @InjectableBean
//
private static final HashMap<Class<?>, Object> injectableBeanMap = new HashMap<>();
public static void injectBean(@NonNull Object container) {
injectBean(container, container.getClass().getDeclaredFields());
}
private static void injectBean(@NonNull Object container, @NonNull Field[] fieldList) {
for (Field field : fieldList) {
if (!field.isAnnotationPresent(Inject.class)) {
continue;
}
final Class<?> klass = field.getType();
Object beanObject = injectableBeanMap.get(klass);
if (beanObject == null) {
// check InjectableBean class
if (!klass.isAnnotationPresent(InjectableBean.class)) {
AssertUtils.fail("@Inject failed. klass has no @InjectableBean:" + klass.getName());
continue;
}
synchronized (injectableBeanMap) {
beanObject = injectableBeanMap.get(klass);
if (beanObject == null) {
try {
beanObject = klass.newInstance();
injectableBeanMap.put(klass, beanObject);
injectBeanFromSuperClass(beanObject, klass);
} catch (InstantiationException e) {
AssertUtils.fail(e, "@Inject failed. klass.newInstance() failed:" + klass.getName());
} catch (IllegalAccessException e) {
AssertUtils.fail(e, "@Inject failed. klass.newInstance() failed:" + klass.getName());
}
}
}
}
AssertUtils.assertNotNull(beanObject);
try {
field.setAccessible(true);
field.set(container, beanObject);
field.setAccessible(false);
} catch (IllegalAccessException e) {
AssertUtils.fail(e, "@Inject failed. field.set() failed:" + klass.getName());
}
}
}
private static void injectBeanFromSuperClass(@NonNull Object beanObject, @NonNull Class<?> klass) {
Class<?> superKlass = klass.getSuperclass();
if (superKlass != null && superKlass.isAnnotationPresent(InjectableBean.class)) {
injectBeanFromSuperClass(beanObject, superKlass);
}
injectBean(beanObject, klass.getDeclaredFields());
}
public static void registerBean(@NonNull Object object) {
Class<?> klass = object.getClass();
AssertUtils.assertTrue(injectableBeanMap.get(klass) == null, "klass object already exists:" + klass.getName());
synchronized (injectableBeanMap) {
if (injectableBeanMap.get(klass) == null) {
injectableBeanMap.put(klass, object);
}
}
}
접기