12.1 필드와 setter 메소드에 @Inject 기능 추가
현재 생성자 주입만 가능한데, @Inject를 활용해서 필드(field), setter 메소드를 통해서도 DI를 할 수 있도록 기능을 추가한다
필드와 생성자 주입
@Service
public class MyQnaService {
private UserRepository userRepository
@Inject
private QuestionRepository questionRepository;
@Inject
public MyQnaService(UserRepository userRepository) {
this.userRepository = userRepository
}
}
setter 주입
@Controller
public class MyUserController {
private MyUserService myUserService;
@Inject
public void setUserService(MyUserService userService) {
this.myUserService = userService;
}
}
아래의 클래스 다이어그램을 참고하여 요구사항을 만족하는 소스코드를 구현해보도록 한다
p413 클래스 다이어그램
- BeanFactory 초기화시 preInstanticateBeans (빈 후보 클래스 타입) 순회하며 Injector 구현체 통해 빈 생성과 DI를 수행
- List<Injector> injectors에는 구현체 Constructor/Setter/FieldInjector를 인스턴스화하여 사용
→ Composition (합성)
- 3가지 Injector는 추상 클래스 AbstractInjector 상속 후 추상 메소드를 Overriding 하여 동작
→ Template Method Pattern (탬플릿 메소드 패턴)
12.2 필드와 setter 메소드 @Inject 구현
Inject 애노테이션
@Target({ElementType.TYPE, ElementType.CONSTRUCTOR, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Inject {
}
Injector 인터페이스
public interface Injector {
void inject(Class<?> clazz);
}
AbstractInjector 추상 클래스
탬플릿 메소드 패턴을 활용해 로직에 대한 중복을 제거하니, 하위 클래스는 자신과 관련있는 작업만 구현하면 된다
public abstract class AbstractInjector implements Injector {
private static final Logger log = LoggerFactory.getLogger(AbstractInjector.class);
private BeanFactory beanFactory;
public AbstractInjector(BeanFactory beanFactory) {
this.beanFactory = beanFactory;
}
@Override
public void inject(Class<?> clazz) {
instantiateClass(clazz);
Set<?> injectedBeans = getInjectedBeans(clazz); //*추상 메소드
for(Object injectedBean : injectedBeans) {
Class<?> beanClass = getBeanClass(injectedBean); //*추상 메소드
inject(injectedBean, instantiateClass(beanClass), beanFactory); //*추상 메소드
}
}
abstract Set<?> getInjectedBeans(Class<?> clazz);
abstract Class<?> getBeanClass(Object injectedBean);
abstract void inject(Object injectedBean, Object bean, BeanFactory beanFactory);
private Object instantiateClass(Class<?> clazz) {
Object bean = beanFactory.getBean(clazz);
if(bean != null) {
return bean;
}
Constructor<?> injectedConstructor = BeanFactoryUtils.getInjectedConstructor(clazz);
if(injectedConstructor == null) {
bean = BeanUtils.instantiate(clazz);
beanFactory.registerBean(clazz, bean);
return bean;
}
log.debug("Constructor : {}", injectedConstructor);
bean = instantiateConstructor(injectedConstructor);
beanFactory.registerBean(clazz, bean);
return bean;
}
private Object instantiateConstructor(Constructor<?> constructor) {
Class<?>[] parameterTypes = constructor.getParameterTypes();
List<Object> args = Lists.newArrayList();
Set<Class<?>> preInstantiatedBeans = beanFactory.getPreInstantiatedBeans();
for(Class<?> clazz : parameterTypes) {
Class<?> concertedClass = BeanFactoryUtils.findConcreteClass(clazz, preInstantiatedBeans);
if(!preInstantiatedBeans.contains(concertedClass)) {
throw new IllegalStateException(clazz + "는 Bean 아닙니다");
}
Object bean = beanFactory.getBean(concertedClass);
if(bean == null) {
bean = instantiateClass(concertedClass);
}
args.add(bean);
}
return BeanUtils.instantiateClass(constructor, args.toArray());
}
}
먼저 생성자 주입 방식은 inject(Class<?> clazz) 에서 instantiateClass() 메소드만 호출해도 처리된다
그래서 추상 메소드를 구현은 아래와 같이 한다
ConstructorInjector 클래스
public class ConstructorInjector extends AbstractInjector {
private static final Logger log = LoggerFactory.getLogger(ConstructorInjector.class);
public ConstructorInjector(BeanFactory beanFactory) {
super(beanFactory);
}
@Override
Set<?> getInjectedBeans(Class<?> clazz) { return Sets.newHashSet(); }
@Override
Class<?> getBeanClass(Object injectedBean) { return null; }
@Override
void inject(Object injectedBean, Object bean, BeanFactory beanFactory) {}
}
*참고. 11장 포스팅 (생성자 DI)
https://dev-ljw1126.tistory.com/409
*참고. 리플렉션 API 테스트 학습
https://dev-ljw1126.tistory.com/407
SetterInjector 클래스
- getInjectedBeans : @Inject 붙어 있는 메소드 정보 반환
- getBeanClass : setter의 경우 method 파라미터 하나에 대한 빈을 찾아야 한다
- inject : injectedBean == method, bean == 실제 주입할 빈, beanFactory 는 리플렉션 API 실행 위해 해당 method를 포함하는 클래스(빈 인스턴스) 찾는 용도
public class SetterInjector extends AbstractInjector {
private static final Logger log = LoggerFactory.getLogger(SetterInjector.class);
public SetterInjector(BeanFactory beanFactory) {
super(beanFactory);
}
@Override
Set<?> getInjectedBeans(Class<?> clazz) {
return BeanFactoryUtils.getInjectedMethods(clazz);
}
@Override
Class<?> getBeanClass(Object injectedBean) {
Class<?>[] parameterTypes = ((Method) injectedBean).getParameterTypes();
if(parameterTypes.length != 1) {
throw new IllegalStateException("DI할 메소드 인자는 하나여야 합니다");
}
return parameterTypes[0];
}
@Override
void inject(Object injectedBean, Object bean, BeanFactory beanFactory) {
Method method = (Method) injectedBean;
try {
method.invoke(beanFactory.getBean(method.getDeclaringClass()), bean);
} catch (IllegalAccessException | InvocationTargetException e) {
log.error(e.getMessage());
}
}
}
FieldInjector 클래스
- getInjectedBeans : @Inject 붙어 있는 필드 정보 반환
- getBeanClass : field 주입방식의 경우 해당 field의 타입을 리턴
- inject : injectedBean == field, bean == 실제 주입할 빈, beanFactory 는 리플렉션 API 실행 위해 해당 field를 포함하는 클래스(빈 인스턴스) 찾는 용도
public class FieldInjector extends AbstractInjector {
private static final Logger log = LoggerFactory.getLogger(FieldInjector.class);
public FieldInjector(BeanFactory beanFactory) {
super(beanFactory);
}
@Override
Set<?> getInjectedBeans(Class<?> clazz) {
return BeanFactoryUtils.getInjectFields(clazz);
}
@Override
Class<?> getBeanClass(Object injectedBean) {
Field field = (Field) injectedBean;
return field.getType();
}
@Override
void inject(Object injectedBean, Object bean, BeanFactory beanFactory) {
Field field = (Field) injectedBean;
try {
field.setAccessible(true);
field.set(beanFactory.getBean(field.getDeclaringClass()), bean);
} catch (IllegalAccessException e) {
log.error(e.getMessage());
}
}
}
BeanFactory 클래스
- AnnotationHandlerMapping 클래스에서 BeanFactory 인스턴스 생성과 초기화를 담당
- List<Injector> injector에 직접 생성한 구현체를 담아 사용 (Composition == 합성)
public class BeanFactory {
[..]
private final Set<Class<?>> preInstantiatedBeans;
private Map<Class<?>, Object> beans = Maps.newHashMap();
private List<Injector> injectors;
public BeanFactory(Set<Class<?>> preInstantiatedBeans) {
this.preInstantiatedBeans = preInstantiatedBeans;
this.injectors = Arrays.asList(
new ConstructorInjector(this),
new FieldInjector(this),
new SetterInjector(this)
);
}
public void initialize() {
for(Class<?> clazz : preInstantiatedBeans) {
if(beans.get(clazz) == null) {
inject(clazz);
}
}
}
private void inject(Class<?> clazz) {
for(Injector injector : injectors) {
injector.inject(clazz);
}
}
[..]
}
12.3 @Inject 개선
문제점
p420
① AbstractInjector 에서 탬플릿 메소드 패턴을 사용하여 중복을 제거 했지만, 소스 코드를 이해하기 쉽지 않다
② BeanFactory 에서 빈에 대한 저장과 조회만 담당하고, 생성과 의존성 주입은 Injector 구현 클래스가 담당하고 있다보니 BeanFactory에서 일을 시키는 것이 아니라 빈 정보를 조회하는 상황이 계속 발생한다
빈 인스턴스 생성과 주입 작업은 BeanFactory가 담당하고 현재 빈 클래스의 상태 정보를 별도의 클래스로 추상화해 관리하는 것이 좀 더 객체 지향적인 개발이 가능하겠다
리팩토링 순서
p421
① BeanScanner 클래스의 이름을 ClasspathBeanDefinitionScanner로 Rename 리팩토링한다. (생략)
② ClasspathBeanDefinitionScanner 에서는 애노테이션이 설정된 클래스를 조회후 BeanDefinition 생성하여 BeanFactory에 전달한다. 이때 ClasspathBeanDefinitionScanner가 BeanFactory와 강한 의존 관계를 가지지 않도록 설계한다.
③ BeanFactory는 BeanDefintion을 저장하고, 이를 활용해 빈 인스턴스 생성, 의존관계 주입을 담당하도록 변경한다.
BeanDefinitionRegistry 인터페이스 생성
public interface BeanDefinitionRegistry {
void registerBeanDefinition(Class<?> clazz, BeanDefinition beanDefinition);
}
인터페이스 활용하여 ClasspathBeanDefinitionScanner 와 BeanFactory가 느슨한 관계를 가질 수 있도록 한다
ClasspathBeanDefinitionScanner 클래스
public class ClasspathBeanDefinitionScanner {
private static final Logger log = LoggerFactory.getLogger(BeanFactory.class);
private final BeanDefinitionRegistry beanDefinitionRegistry;
public ClasspathBeanDefinitionScanner(BeanDefinitionRegistry beanDefinitionRegistry) {
this.beanDefinitionRegistry = beanDefinitionRegistry;
}
public void scan(Object... basePackage) {
Reflections reflections = new Reflections(basePackage);
Class<? extends Annotation>[] annotations = new Class[]{Controller.class, Service.class, Repository.class};
Set<Class<?>> beanClasses = getTypesAnnotationWith(reflections, annotations);
for(Class<?> beanClazz : beanClasses) {
//*여기
beanDefinitionRegistry.registerBeanDefinition(beanClazz, new BeanDefinition(beanClazz));
}
}
[..]
}
p422
즉, ClasspathBeanDefinitionScanner는 classpath에서 빈을 조회하는 역할을 담당하고, 조회한 정보로 BeanDefinition 생성해 BeanDefinitionRegistry에 전달하면 BeanDefinitionRegistry 구현체가 저장소 역할을 담당하게 된다
→ 이때 BeanFactory가 BeanDefinitionRegistry 구현체이다
BeanFactory 클래스
public class BeanFactory implements BeanDefinitionRegistry {
private final Map<Class<?>, BeanDefinition> beanDefinitionMap = new HashMap<>();
[..]
@Override
public void registerBeanDefinition(Class<?> clazz, BeanDefinition beanDefinition) {
log.debug("registered bean : {}", clazz);
beanDefinitionMap.put(clazz, beanDefinition);
}
}
p424
이처럼 객체간의 의존관계를 인터페이스를 통해 분리한 후 DI를 통해 연결하면 유연한 구조로 개발하는 것이 가능하다. 단, 이 경우의 단점은 객체간의 DI를 담당하는 코드가 필요하다는 것이다. ClasspathBeanDefinitionScanner 와 BeanFactory의 관계 설정을 담당하기 위해 ApplictionContext 클래스 생성하도록 한다.
ApplicationContext 클래스
public class ApplicationContext {
private BeanFactory beanFactory;
public ApplicationContext(Object... basePackages) {
this.beanFactory = new BeanFactory();
ClasspathBeanDefinitionScanner scanner = new ClasspathBeanDefinitionScanner(this.beanFactory);
scanner.scan(basePackages);
beanFactory.initialize();
}
public <T> T getBean(Class<T> clazz) {
return beanFactory.getBean(clazz);
}
public Set<Class<?>> getBeanClasses() {
return beanFactory.getBeanClasses(); // beanDefinitionMap의 keySet() 해당
}
}
AnnotationHandlerMapping 클래스 리팩토링 수행한다
public class AnnotationHandlerMapping implements HandlerMapping {
[..]
public AnnotationHandlerMapping(Object... basePackage) {
this.basePackage = basePackage;
}
public void initialize() {
ApplicationContext ac = new ApplicationContext(basePackage);
Map<Class<?>, Object> controllerMap = getControllers(ac);
[..]
}
private Map<Class<?>, Object> getControllers(ApplicationContext ac) {
Map<Class<?>, Object> controllers = new HashMap<>();
for(Class<?> clazz : ac.getBeanClasses()) {
if(clazz.isAnnotationPresent(Controller.class)) {
controllers.put(clazz, ac.getBean(clazz));
}
}
return controllers;
}
[..]
}
p426
지금까지 객체의 책임과 역할을 분리하는 리팩토링을 진행했다. 마지막으로 빈 클래스 정보를 담고 있는 BeanDefinition과 이 정보를 활용해 빈 인스턴스를 생성하고 의존과계 주입을 담당하는 BeanFactory를 구현해보도록 한다.
BeanDefinition과 BeanFactory 구현
BeanDefinition 클래스
빈 인스턴스 후보 클래스 타입의 상태값을 가짐
public class BeanDefinition {
private Class<?> beanClazz;
private Constructor<?> injectConstructor;
private Set<Field> injectFields;
public BeanDefinition(Class<?> clazz) {
this.beanClazz = clazz;
this.injectConstructor = getInjectConstructor(clazz);
this.injectFields = getInjectFields(clazz, injectConstructor);
}
private static Constructor<?> getInjectConstructor(Class<?> clazz) {
return BeanFactoryUtils.getInjectedConstructor(clazz);
}
private Set<Field> getInjectFields(Class<?> clazz, Constructor<?> constructor) {
if(constructor != null) {
return Sets.newHashSet();
}
Set<Field> injectFields = Sets.newHashSet();
Set<Class<?>> injectProperties = getInjectPropertiesType(clazz);
Field[] fields = clazz.getDeclaredFields();
for(Field field : fields) {
if(injectProperties.contains(field.getType())) {
injectFields.add(field);
}
}
return injectFields;
}
private static Set<Class<?>> getInjectPropertiesType(Class<?> clazz) {
Set<Class<?>> injectProperties = Sets.newHashSet();
// setter injection
Set<Method> injectMethod = BeanFactoryUtils.getInjectedMethods(clazz);
for(Method method : injectMethod) {
Class<?>[] parameterTypes = method.getParameterTypes();
if(parameterTypes.length != 1) {
throw new IllegalStateException("DI할 메소드 인자는 하나여야 합니다");
}
injectProperties.add(parameterTypes[0]);
}
// field injection
Set<Field> fields = BeanFactoryUtils.getInjectFields(clazz);
for(Field field : fields) {
injectProperties.add(field.getType());
}
return injectProperties;
}
public Class<?> getBeanClazz() {
return beanClazz;
}
public Constructor<?> getInjectConstructor() {
return injectConstructor;
}
public Set<Field> getInjectFields() {
return injectFields;
}
public InjectType getResolvedInjectMode() {
if(injectConstructor != null) {
return InjectType.INJECT_CONSTRUCTOR;
}
if(!injectFields.isEmpty()) {
return InjectType.INJECT_FIELD;
}
return InjectType.INJECT_NO;
}
}
BeanFactory 리팩토링
beanDefinitionMap에 저장된 정보를 가지고 빈 인스턴스 생성과 DI를 수행
public class BeanFactory implements BeanDefinitionRegistry {
private static final Logger log = LoggerFactory.getLogger(BeanFactory.class);
private Map<Class<?>, BeanDefinition> beanDefinitionMap = Maps.newHashMap();
private Map<Class<?>, Object> beans = Maps.newHashMap();
public BeanFactory() {
}
public void initialize() {
for(Class<?> clazz : getBeanClasses()) {
getBean(clazz);
}
}
@SuppressWarnings("unchecked")
public <T> T getBean(Class<T> requiredType) { // 재귀 호출
Object bean = beans.get(requiredType);
if(bean != null) {
return (T) bean;
}
Class<?> concreteClass = findConcreteClass(requiredType); // 구현 클래스 타입 가져옴
BeanDefinition beanDefinition = beanDefinitionMap.get(concreteClass);
bean = inject(beanDefinition);
registerBean(concreteClass, bean);
return (T) bean;
}
// Bean 대상 여부
private Class<?> findConcreteClass(Class<?> clazz) {
Set<Class<?>> beanClasses = getBeanClasses();
Class<?> concreteClass = BeanFactoryUtils.findConcreteClass(clazz, beanClasses);
if (!beanClasses.contains(concreteClass)) {
throw new IllegalStateException(clazz + "는 Bean이 아닙니다");
}
return concreteClass;
}
private Object inject(BeanDefinition beanDefinition) {
InjectType resolvedInjectMode = beanDefinition.getResolvedInjectMode();
if(resolvedInjectMode == InjectType.INJECT_CONSTRUCTOR) { // 생성자 DI
return injectConstructor(beanDefinition);
} else if(resolvedInjectMode == InjectType.INJECT_FIELD) { // setter, field DI
return injectFields(beanDefinition);
} else { // 기본 생성자
return BeanUtils.instantiate(beanDefinition.getBeanClazz());
}
}
private Object injectConstructor(BeanDefinition beanDefinition) {
Constructor<?> injectConstructor = beanDefinition.getInjectConstructor(); // 생성자 정보 가져옴
List<Object> args = new ArrayList<>();
for(Class<?> clazz : injectConstructor.getParameterTypes()) {
args.add(getBean(clazz));
}
return BeanUtils.instantiateClass(injectConstructor, args.toArray());
}
private Object injectFields(BeanDefinition beanDefinition) {
Object bean = BeanUtils.instantiate(beanDefinition.getBeanClazz()); // 기본 객체 생성
Set<Field> injectFields = beanDefinition.getInjectFields(); // 주입할 파라미터 정보
for(Field field : injectFields) {
injectField(bean, field);
}
return bean;
}
private void injectField(Object bean, Field field) {
log.debug("Inject Bean : {}, Field : {}", bean, field);
try {
field.setAccessible(true);
field.set(bean, getBean(field.getType())); // bean 객체의 field에 getBean(field.getType())을 주입
} catch (IllegalAccessException e) {
log.error(e.getMessage());
}
}
public void clear() {
beanDefinitionMap.clear();
beans.clear();
}
public void registerBean(Class<?> clazz, Object instance) {
beans.put(clazz, instance);
}
@Override
public void registerBeanDefinition(Class<?> clazz, BeanDefinition beanDefinition) {
log.debug("register bean definition : {}", clazz);
beanDefinitionMap.put(clazz, beanDefinition);
}
public Set<Class<?>> getBeanClasses() {
return beanDefinitionMap.keySet();
}
}
좋은 글
p430
빈 클래스의 의존관계에 대한 관련 정보 처리를 BeanDefintion이 담당하고 있기 때문에 BeanFactory가 담당할 책임이 줄어 들었다. 객체지향 설계의 핵심은 객체의 역할과 책임에 대해 계속해서 고민하면서 한 가지 역할과 책임을 가지도록 하는 것이다. 각 객체의 역할과 책임을 명확히 한 후 객체 간의 협업을 통해 동작하는 애플리케이션을 완성해 가는 것이다
하지만 애플리케이션을 개발하는 시작 단계에서 객체의 역할과 책임을 명확히 설계하기 힘들다. 물론 초반 설계와 구현 단계에서
구체화할 수 있는 부분까지 최대한 명확하게 설계해야겠지만 인간이 신이 아닌 이상 불가능하다 (..중략)
따라서 초반 설계를 철저히 하는 것보 중요하지만 그보다 애플리케이션은 언제든지 변경될 수 있다는 가정하에 변경이 발생했을 때
빠르게 대처할 수 있는 리팩토링 역량을 쌓는 것이 더 중요하다
참고. 프로젝트 전체 코드
12.4 설정 추가를 통한 유연성 확보 의 경우 따로 포스팅 하지 않음 → 아래 깃 저장소 소스 코드 참고
https://github.com/ljw1126/my-jwp-basic/tree/chapter11_di_5_complete
12.8 웹 서버 도입을 통한 서비스 운영
아래 포스팅 참고
https://dev-ljw1126.tistory.com/399
'독서 > 📚' 카테고리의 다른 글
[개발도서]프로그래머 열정을 말하다 (채드 파울러) (2) | 2024.05.02 |
---|---|
만화로 배우는 리눅스 시스템 관리1 (3) | 2024.04.30 |
[Next Step] 11장 의존관계 주입(DI)을 통합 테스트 하기 쉬운 코드 만들기 (0) | 2023.11.21 |
[Next Step] 10장 새로운 MVC 프레임워크 구현을 통한 점진적 개선 (2) | 2023.11.20 |
[Next Step] 9장 두 번째 양파 껍질을 벗기기 위한 중간 점검 (0) | 2023.11.18 |
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!