독서/📚

[Next Step] 12장 확장성 있는 DI 프레임워크로 개선

leejinwoo1126 2023. 11. 23. 13:50
반응형

 


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

 

[Next Step] 11장 의존관계 주입(DI)을 통합 테스트 하기 쉬운 코드 만들기

목차 11.1 왜 DI가 필요한가? 의존관계란 객체 혼자 모든 일을 처리하기 힘들기 때문에 내가 해야 할 작업을 다른 객체에게 위임하면서 발생한다. 즉, 내가 가지고 있는 책임과 역할을 다른 객체에

dev-ljw1126.tistory.com

 

*참고. 리플렉션 API 테스트 학습 

https://dev-ljw1126.tistory.com/407

 

[Java] Reflection API 테스트 학습(with Baeldung)

목차 *테스트 코드 작성시 JUnit5, Assertj 사용 Baeldung 테스트 학습 class name 활용하여 인스턴스 생성하기 Job 인터페이스 public interface Job { String getJobType(); } MaintenanceJob 클래스 public class MaintenanceJob imp

dev-ljw1126.tistory.com

 

 

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와 강한 의존 관계를 가지지 않도록 설계한다.

BeanFactoryBeanDefintion을 저장하고, 이를 활용해 빈 인스턴스 생성, 의존관계 주입을 담당하도록 변경한다.

 

 

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

즉, ClasspathBeanDefinitionScannerclasspath에서 빈을 조회하는 역할을 담당하고, 조회한 정보로 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

 

GitHub - ljw1126/my-jwp-basic: Next Step 자바 웹 어플리케이션 개인 학습

Next Step 자바 웹 어플리케이션 개인 학습. Contribute to ljw1126/my-jwp-basic development by creating an account on GitHub.

github.com

 


12.8 웹 서버 도입을 통한 서비스 운영 

아래 포스팅 참고 

https://dev-ljw1126.tistory.com/399

 

[Next Step] 12.8 웹서버 도입을 통한 서비스 운영(p458) 정리

목차 4-1. nginx 설치 및 설정 디렉토리 $ apt-get install -y nginx $ sudo service nginx start $ netstat -tnlp 서버 아이피 접속시(80포트) nginx default 페이지 확인 가능하다 80/tcp 접근 되지 않을 경우 ufw 방화벽 설정

dev-ljw1126.tistory.com

 

반응형