[Java] Spring Framework를 최대한 활용하여 응집도를 높일 수 있는 방안 - Dependency Injection(DI)를 통해 멤버변수인 리스트에 동일한 타입의 Bean(구현체)들을 할당하는 과정을 중심으로

Hyo Kyun Lee·2일 전
0

Java

목록 보기
106/106

1. 개요

Kafka 이벤트를 처리하기 위한 Service를 구성하는 과정 중, 이벤트가 들어왔을때 해당 이벤트를 처리하기 위한 이벤트핸들러를 필터링하는 기능을 개발하고자 하였다.

이 이벤트핸들러의 목록, 즉 이벤트핸들러 구현체들을 이벤트핸들러 리스트에 넣고 Stream API를 활용하여 이벤트를 처리하기 위한 핸들러를 필터링하는 부분이다.

여기서 의문이 생겼다.

기존 구현체들을 할당하는 과정을 아래와 같이 일일이 구현체를 넣어야 하는 것이 맞나?라는 생각이 들었다.

EventHandlerList<EventHandler> eventHandlerList = new EventHandlerList<>
eventHandlerList.add ...
eventHandlerList.add ...
eventHandlerList.add ...

이 로직대로라면 이벤트핸들러리스트 뿐 만 아니라, 리스트에 넣어줄 구현체를 일일이 또 구현해주어야 한다.

유지관리나 응집도 측면에서 말이 되지 않은 로직인데, 이에 대한 방안을 찾아보니까 Spring Framework의 DI를 활용하여 이 모든 과정을 Framework 측에 위임할 수 있는 방법이 있다는 것을 알게 되었다.

EventHandlerList<EventHandler> eventHandlerList

Spring Framework의 DI나 생성자 주입 등을 적절하게 활용한다면, 위와 같은 불필요한 구현 과정이나 할당 과정 필요없이 해당 멤버변수의 구현체 할당을 자동적으로 구현할 수 있게끔 해줄 수 있다.

이게 단순히 보면 Framework에 구현체들을 주입받는 DI인데, 자세히 보면 위에서 의문이 들었던 유지관리 및 응집도를 개선할 수 있으며 그만큼 로직을 간결하고 명확하게 작성할 수 있는 방안이기도 하였다.

흥미로운 내용이기도 하고 중요한 내용이란 생각이 들어 공부한 내용을 기록한다.

다만 시간관계상 너무 깊게는 파고들지는 않고, DI나 Bean 등 핵심 개념을 이해하고 향후에 적절하게 활용할 수 있을 정도로만 일단 공부하였음에 유의하자.

2. Spring Framework 측에서 자동으로 멤버변수 리스트에 구현체들을 할당해준다.

결론부터 살펴보겠다.

멤버변수에 리스트가 있을때, Spring Framework 측에서 해당 리스트에 구현체들을 넣어준다.

아래와 같이 한 클래스에 eventHandlers라는 list형태의 멤버변수가 선언되어 있다고 해보자.

private final List<EventHandler> eventHandlers;

이때 이 멤버변수 리스트의 구현 뿐만 아니라, 이 리스트에 이벤트핸들러 객체를 구현하고 이를 할당해주는 번거로운 작업을 하지 않고도 이 멤버변수를 그대로 사용할 수 있다.

return eventHandlers.stream()
                .filter(eventHandler -> eventHandler.supports(event))
                .findAny()
                /*
                * Wrapper -> Object
                * */
                .orElse(null);

즉 위와 같이 마치 구현체들을 이미 할당한 것처럼 해당 리스트를 그대로 사용할 수 있고, 실제로도 이미 구현체들이 할당된 상태이다. 이에 따라 번거로운 구현 및 할당 과정을 제거하였기 때문에 로직이 매우 간결해졌음을 알 수 있다.

이것이 어떻게 가능할까?

그 이유는 Spring의 의존성 주입(DI, Dependency Injection) 덕분이다.

참고로 저 EventHandler는 인터페이스이다.

ArticleCreatedEventHandler, ArticleDeletedEventHandler 같은 여러 구현체들이 @Component(혹은 @Service, @Bean)로 스프링 빈에 등록되어 있는데, 이 리스트 멤버변수에는 이러한 구현체들이 Spring Framework의 DI를 통해 이미 할당이 되어있는 상태라는 것이다.

이처럼, Spring은 동일 타입의(여기서는 주입대상의 타입으로, 해당 구현체들의 부모 타입인 인터페이스가 되겠다) 빈이 여러 개 있을 경우, List<EventHandler> 타입으로 선언된 곳에 해당 빈들을 모두 주입해준다.

@Component
public class ArticleCreatedEventHandler implements EventHandler<ArticleCreatedPayload> { ... }

@Component
public class ArticleDeletedEventHandler implements EventHandler<ArticleDeletedPayload> { ... }

이런 식으로 등록해놓으면, Spring Framework가 자동으로 List<EventHandler>에 모든 구현체를 모아서 넣어준다.

참고로, 리스트와 같은 컬렉션 타입은 이와 같은 빈주입 및 할당이 일어나고 단일 객체인 경우 해당 객체만 주입하는 동작이 발생한다.

3. Spring Framework가 DI를 통해 구현체를 주입하는 과정

위의 멤버변수 리스트에 RequiredArgsConstructor를 통해 생성자 주입이 일어나는 시점에 Spring Framework가 구현체들을 할당해준다.

이 과정을 세부적으로 살펴보자.

3-1. 전제상황

Spring Framework가 Bean을 자동으로 주입해주는 과정은 몇가지 전제상황을 요한다.

private final List<EventHandler> eventHandlers;

위와 같이 리스트 멤버변수가 있을때,

  • HotArticleService는 @Service로 등록된 Bean이다(즉 해당 멤버변수를 가지는 클래스도 빈으로 등록이 되어있어야 한다).
  • EventHandler는 인터페이스, 다양한 구현체들을 지닐 수 있다.
  • 여러 개의 구현체 (ArticleCreatedEventHandler,ArticleDeletedEventHandler, …)가 각각 @Component 로 등록되어 있다.

3-2. Spring의 빈 주입 기본 흐름

Spring이 Bean을 주입할 때는 크게 3단계가 있다(너무 깊게 살펴보지는 않겠다).

  • 컴포넌트 스캔 (Component Scan)

Spring Application 실행과정에서 빈을 관리하는 Application Context가 @Component, @Service, @Repository 등이 붙은 클래스를 찾아 Bean으로 등록한다.

등록된 Bean들은 ApplicationContext 안에서 타입(Class) + 이름(Bean Name) 조합으로 관리한다.

  • 의존성 주입 (Dependency Injection)

@Autowired 또는 생성자 주입(@RequiredArgsConstructor 같은 Lombok 사용 시 자동)으로 Bean을 주입한다.

주입할 때, Spring은 "이 필드/파라미터 타입에 맞는 Bean이 무엇이 있는가?"를 검색하여 주입한다.

이 과정까지가 흔히 알고있는 구현체 주입 과정으로, 단일 객체를 주입하기 위해 해당 Bean을 찾아 주입하는 과정이다.

  • 컬렉션 타입 주입 (List, Set, Map 지원)

컬렉션 타입 역시 동일하게 DI를 주입하되, 다만 단일 객체를 주입하는 과정과는 조금 다르다.

만약 List<EventHandler> 같은 컬렉션 타입이 있으면,
Spring은 해당 타입을 구현한 모든 Bean을 찾아서 리스트에 담아 주입한다.

이때 위에서 말한 전제와 같이, EventHandler는 인터페이스여야 한다.

3-3. 컬렉션 주입 동작 방식

Spring Framework 내부적으로는 DefaultListableBeanFactory가 의존성 주입을 담당한다.

즉, 해당 BeanFactory가 리플렉션하여 Bean 주입하는과정에 대한 명세가 @Component, @Service 등의 어노테이션에 들어가있는 것이다.

이후 resolveDependency() 라는 메서드에서, 파라미터 타입을 보고 어떤 Bean을 주입할지 결정한다. 즉, Bean을 주입하기 위해선 기본적으로 주입할 빈의 타입이 동일해야 하겠다.

이때 만약 주입받을 타입이 List<T>라면, BeanFactory는 T(generic, 여기서는 메타데이터를 의미) 타입을 구현한 모든 Bean들을 찾아 리스트를 만든다. 단일 객체 주입과는 다르게, 해당 인터페이스 타입을 구현한 모든 구현체를 찾는다.

찾은 Bean들은 기본적으로 ApplicationContext에 등록된 순서(또는 @Order, @Priority 애노테이션이 붙으면 그 순서)대로 리스트에 담는다.

만들어진 리스트를 그대로 생성자/필드에 주입, 생성자 주입을 종료한다.

참고로 이 과정은

private final List<EventHandler> eventHandlers;

이 멤버변수에

eventHandlers = Arrays.asList(
   new ArticleCreatedEventHandler(...),
   new ArticleDeletedEventHandler(...),
   new ArticleLikedEventHandler(...),
   ...
);

하여 구현체들을 주입하여 준다.

3-4. ApplicationContext 수준에서 보았을때

위에서 기술한 세부적인 과정들은 Application Context가 실행시점에서 진행하는데, 이때 모든 EventHandler 구현체들은 ApplicationContext에 BeanDefinition 으로 먼저 등록된다.

실제 객체 인스턴스는 Bean Lifecycle 과정(instantiate → populate → initialize → postProcess)을 거쳐 만들어진다.

HotArticleService가 초기화될 때, 생성자 파라미터(List<EventHandler>)를 보고, 세부적으로 DefaultListableBeanFactory~resolveDependency()의 과정이 일어나는 것이고, 최종적으로 이 주입과정에서 ApplicationContext는 getBeansOfType(EventHandler.class)를 호출한다.

반환된 맵(Map<String, EventHandler>)에서 value 들을 꺼내 List로 변환하여 주입하여 구현체 할당을 완료한다.

3-5. 정리

이 모든 과정은 Spring Framework의 DI로 인해 진행된다.

eventHandlers 리스트는 직접 new 해서 넣는 게 아니라, Spring이 ApplicationContext에 등록된 모든 EventHandler 구현체 Bean들을 수집해서 주입하는 방식으로 채워진다.

주입 순서는 ApplicationContext의 Bean 등록 순서지만, 필요하면 @Order 나 Ordered 인터페이스, 혹은 @Priority로 순서를 강제할 수 있다.

따라서 개발자가 신경 쓸 건 "EventHandler 구현체들을 전부 @Component 로 빈 등록해두기"뿐이고, 리스트 할당은 Spring이 알아서 해준다.

4. Bean에 대하여

추가적으로 Bean에 대한 몇가지 개념에 대해 알아보도록 한다.

Spring이 “동일 타입 빈이 여러 개 있다”라고 말할 때, 타입(Type)은 클래스 타입(Class Type) 또는 인터페이스 타입을 의미한다.

쉽게 말하면 상속이나 구현한게 없다면 Class와 해당 Class의 이름, 있다면 상위 부모 클래스 혹은 인터페이스를 의미한다.

@Component
public class ArticleCreatedEventHandler implements EventHandler<ArticleCreatedPayload> { ... }

@Component
public class ArticleDeletedEventHandler implements EventHandler<ArticleDeletedPayload> { ... }

현재 프로젝트 구조로 보면, 핸들러 인터페이스를 구현한 클래스들은 이름은 다르지만 모두 EventHandler 인터페이스의 구현체이기에 빈을 등록할 Context 입장에서는 타입으로 보았을때는 동일한 Bean들이다(구체적으로 말하면, Spiring Context 입장에서 검색가능하기에 단일/인터페이스 형태의 Bean로 볼 수 있지만 인터페이스 형태에서는 모두 동일한 Bean임).

즉, Spring 입장에서는 둘 다 EventHandler 타입의 Bean이다.
(*여기서도 구체적인 이해가 필요한데, 일단은 이해를 용이하게 하기위해 이렇게 기술)

이 Bean을 정상적으로 주입받기 위해선 반드시 list 등의 컬렉션 형태 주입이 이루어져야 한다.

  • @Autowired EventHandler handler; 라고 쓰면 모호해진다.

왜냐면 Bean을 검색한 Spring Context 입장에서는 EventHandler 타입의 빈이 2개 이상 있기 때문이다.

  • @Autowired List<EventHandler> handlers; 라고 하면 Spring은 EventHandler 타입 빈들을 모두 모아서 리스트에 넣어줄 수 있다.

컬렉션에 맞춘 빈 주입이 일어나기 때문에 모호한 빈이 없기에 정상적인 주입이 가능하기 때문이다.

다시한번 정리하면, 여기서 “동일 타입”은 주입받고자 하는 타입 기준이 되겠다.

List<EventHandler> → EventHandler 인터페이스를 구현한 모든 Bean

List<ArticleCreatedEventHandler> → ArticleCreatedEventHandler 클래스 Bean만

5. ApplicationContext에서 Bean이 관리되는 방식

매우 중요한 내용이기에, 조금만 더 깊게 살펴보겠다.

Spring의 Bean은 내부적으로, 즉 Spring Context 관리 하에 DefaultListableBeanFactory라는 BeanFactory 구현체에서 관리한다.

여기서 Bean은 이름 + 타입 조합으로 관리한다.

예를 들어,

@Component
public class ArticleCreatedEventHandler implements EventHandler<ArticleCreatedPayload> {}

이 클래스는 다음과 같이 Bean으로 등록된다.

이때 "타입" 관점에서 보았을때 Spring Context는 단일 객체, 인터페이스 객체로 둘 다 파악이 가능하겠다.

  • Bean 이름: 기본적으로 articleCreatedEventHandler (클래스명을 카멜케이스로 바꿈)
  • Bean 타입: ArticleCreatedEventHandler.class
  • 할당 가능한 슈퍼 타입: EventHandler 인터페이스도 포함 (즉, 이 Bean은 EventHandler로도 조회 가능)

따라서 ApplicationContext에선 Bean을 찾기 위해 아래와 같은 과정을 거친다.

  • getBean("articleCreatedEventHandler") → ArticleCreatedEventHandler 인스턴스
  • getBean(EventHandler.class) → EventHandler 타입 Bean이 여러 개라면 예외 발생 (모호성)
  • getBeansOfType(EventHandler.class) → Map<String, EventHandler> 반환 (여러 개 가능)

이때 리스트 멤버변수 입장에서는 getBeansOfType이 가능하겠다.

그리고 Spring이 List<EventHandler>를 주입할 때 하는 일은,

Map<String, EventHandler> beans = beanFactory.getBeansOfType(EventHandler.class);
List<EventHandler> list = new ArrayList<>(beans.values());

이 과정을 통해 List<EventHandler>에는 모든 EventHandler 구현체들이 들어가게 된다.

6. 결론

막혀있던 혈이 뚫린 기분인데, Bean을 등록하고 관리하는 과정을 살펴보니 DI의 의미와 Spring Framework에서 이를 활용하여 유지관리성을 어떻게 높일 수 있을지에 대해 깊게 생각할 수 있었던 계기가 되었다.

DI는 유지관리에 직결하는 강력하고 가장 효율적인 관리 방안 중 하나이다.

생각없이 @Autowired를 사용하지말고, 정확한 이해를 바탕으로 Bean을 사용하여 간결명확한 DI활용, 나아가 Spring Framework 활용이 가능하도록 해보자.

0개의 댓글