Lined Notebook

[Spring MVC - 핵심 원리 기본] 05. 컴포넌트 스캔과 의존관계 자동 주입

by ymkim

01. 컴포넌트 스캔과 의존관계 자동 주입 시작하기

  • 지금까지 스프링 빈을 등록할 때는 자바 코드의 @Bean이나 XML<bean> 등을 통해 스프링 빈을 등록
  • 만약 이렇게 등록해야 할 빈이 수백개인 경우 누락이 되는 문제가 발생함
  • 스프링은 설정 정보가 없어도 자동으로 스프링 빈을 등록하는 컴포넌트 스캔이라는 기능 제공
  • 또한 의존 관계도 자동으로 주입하는 @Autowired 기능도 제공

01-1. AutoAppConfig 생성

@ComponentScan(
        excludeFilters = @ComponentScan.Filter(
                type = FilterType.ANNOTATION,
                classes = Configuration.class
        )
)
@Configuration
public class AutoAppConfig {
	...
}
  • @ComponentScan
    • @Component가 어노테이션이 붙은 클래스를 자동으로 빈으로 등록
  • excludeFilters
    • component scan에서 제외할 클래스 지정
    • AppConfig 안에 @Component가 있기에 excludeFilters에 지정
  • 기존 AppConfig와 다르게 @Bean을 사용하지 않음
  • 실무에서는 전부 다 Component Scan을 하지만 현재는 일단 안함

@ComponentScan은 @Component가 붙은 클래스를 자동으로 빈 등록

01-2. RateDiscountPolicy, MemberServiceImpl, MemoryMemberRepository, OrderServiceImpl @Component 어노테이션 사용

@Component
public class RateDiscountPolicy implements DiscountPolicy {
	...
}
@Component
public class MemberServiceImpl implements MemberService {
    private final MemberRepository memberRepository;

    @Autowired
    public MemberServiceImpl(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
}
@Component
public class MemoryMemberRepository implements MemberRepository {
	...
}
@Component
public class OrderServiceImpl implements OrderService {
    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

    @Autowired
    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }
}

01-3. 컴포넌트 스캔 테스트 코드 작성

package hello.core.scan;

import hello.core.AutoAppConfig;
import hello.core.member.MemberService;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class AutoAppConfigTest {

    @Test
    void test() throws Exception {
        //given
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class);
        MemberService memberService = ac.getBean(MemberService.class);

        //when
        //then
        Assertions.assertThat(memberService).isInstanceOf(MemberService.class);
    }
}

  • AnnotationConfigApplicationContext 사용은 이전과 동일함
  • ac.getBean을 통해 bean을 가져와도 정상 동작

01-4. @ComponentScan

  • @ComponentScan은 @Component가 붙은 모든 클래스를 스프링 빈(싱글톤)으로 등록 한다
  • 이때 스프링 빈의 기본 이름은 클래스명을 사용하되 맨 앞글자만 소문자로 사용 한다
    • 빈 이름 기본 전략 : MemberSerivceImpl 클래스 → memberServiceImp
    • 빈 이름 직접 지정 : 만약 스프링 빈의 이름을 직접 지정하고 싶으면 @Component(”memberServiceImpl”) 이름을 부여하면 된다

01-5. @Autowired 의존관계 자동 주입

  • 생성자에 @Autowired를 지정하면, 스프링 컨테이너가 자동으로 해당 스프링 빈을 찾아서 주입 한다
  • 이때 기본 조회 전략은 타입이 같은 빈을 찾아서 주입한다
    • getBean(MemberRepository.class) 와 동일

01-6. 의존관계 자동 주입

  • OrderServiceImpl의 경우 파라미터가 많은데, 이렇게 파라미터가 많은 생성자여도 스프링 컨테이너가 모두 빈을 주입 해준다

01-7. 정리

  • 이전에는 자바 소스의 @Bean, XML의 <bean>을 통해 스프링 빈을 등록 하였다
  • 하지만 @ComponentScan 어노테이션을 사용하게 되면 위와 같이 자바 소스에 직접 Bean을 등록할 필요가 없이 스프링이 알아서 @Component가 붙은 클래스를 대상으로 Bean(빈) 등록을 수행 한다
  • @Bean을 사용하지 않았기 때문에 AppConfig 처럼 의존관계 주입 설정이 어려워지는 점이 존재 하였으나, @Autowired를 생성자에 붙여 자동 의존 관계 주입이 되도록 설정 하였다

02. 탐색 위치와 기본 스캔 대상

02-1. 탐색할 패키지의 시작 위치를 지정

package hello.core;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;

@ComponentScan(
        basePackages = "hello.core.member",
        excludeFilters = @ComponentScan.Filter(
                type = FilterType.ANNOTATION,
                classes = Configuration.class
        )
)
@Configuration
public class AutoAppConfig {

}
  • basePackages 키워드를 사용하여 ComponentScan의 대상 패키지 지정 가능
  • 해당 시작 위치를 기준으로 하위 패키지를 전부 스캔(Scan)
    • basePackages는 여러개 지정이 가능
  • 또한 basepackgesClass를 통해 클래스를 기준으로 스캔 가능
  • 권장 방법
    • 설정 정보 클래스의 위치를 프로젝트 최상단에 위치 한다
  • Spring Boot는 @SpringBootApplication 안에 @ComponentScan 어노테이션이 존재한다
package org.springframework.boot.autoconfigure;

/**
 * Indicates a {@link Configuration configuration} class that declares one or more
 * {@link Bean @Bean} methods and also triggers {@link EnableAutoConfiguration
 * auto-configuration} and {@link ComponentScan component scanning}. This is a convenience
 * annotation that is equivalent to declaring {@code @Configuration},
 * {@code @EnableAutoConfiguration} and {@code @ComponentScan}.
 *
 * @author Phillip Webb
 * @author Stephane Nicoll
 * @author Andy Wilkinson
 * @since 1.2.0
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
		@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
	...
}

02-2. 컴포넌트 스캔의 기본 대상

⚠️ 핵심 포인트임, 자동 컴포넌트 스캔을 지원함
  • 컴포넌트 스캔 @Component 뿐만 아니라 다음 내용도 추가로 대상에 포함
    • @Component : 컴포넌트 스캔에서 사용
    • @Controller : 스프링 MVC 컨트롤러에서 사용
    • @Service : 스프링 비즈니스 로직에서 사용
    • @Repository : 스프링 데이터 접근 계층에서 사용
    • @Configuration : 스프링 설정 정보에서 사용

02-3. 컴포넌트 스캔 관련 부연 설명

  • 어노테이션의 경우 상속 관계가 없다
  • 즉, 어노테이션이 특정 어노테이션을 들고 있는 것을 인식할 수 있는 것은 자바 언어가 지원하는 기능이 아니고, 스프링이 지원하는 기능이다
  • 컴포넌트 스캔의 용도 뿐만이 아니라 어노테이션을 통해 부가 기능 수행
    • 마커 인터페이스
      • @Controller : 스프링 MVC 컨트롤러로 인식
      • @Repository : 스프링 데이터 계층으로 인식, 데이터 계층의 예외를 스프링 예외로 변환
      • @Configuration : 스프링 설정 정보 인식, 스프링 빈이 싱글톤을 유지 하도록 추가 처리
      • @Service : 특별한 처리 안함, 개발자가 비즈니스 계층을 인식하는데 사용

03. 필터

  • includeFilters : 컴포넌트 스캔 대상을 추가로 지정
  • excludeFilters : 컴포넌트 스캔에서 제외할 대상 지정

03-1. 컴포넌트 스캔 대상에 추가할 어노테이션

package hello.core.scan.filter;

import java.lang.annotation.*;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyIncludeComponent {
}
  • MyIncludeComponent 라는 어노테이션 생성
  • ComponentScan 대상이 되는 클래스 지정 시 사용

03-2. 컴포넌트 스캔 대상에서 제외할 어노테이션

package hello.core.scan.filter;

import java.lang.annotation.*;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyExcludeComponent {
}
  • MyExcludeComponent 라는 어노테이션 생성
  • ComponentScan 대상에서 제외 될 클래스 지정 시 사용

03-3. BeanA 생성

package hello.core.scan.filter;

@MyIncludeComponent
public class BeanA {
}

03-4. BeanB 생성

package hello.core.scan.filter;

@MyExcludeComponent
public class BeanB {
}

03-5. 컴포넌트 스캔 관련 테스트 코드 작성

package hello.core.scan.filter;

...

public class ComponentFilterAppConfigTest {

    @Test
    void test() throws Exception {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(ComponentFilterAppConfig.class);
        BeanA beanA = ac.getBean("beanA", BeanA.class);

        assertThat(beanA).isNotNull();

        Assertions.assertThrows(
                NoSuchBeanDefinitionException.class,
                () -> ac.getBean("beanB", BeanB.class)
        );
    }

    @Configuration
    @ComponentScan(
            includeFilters = @ComponentScan.Filter(
                    type = FilterType.ANNOTATION, classes = MyIncludeComponent.class
            ),
            excludeFilters = @ComponentScan.Filter(
                    type = FilterType.ANNOTATION, classes = MyExcludeComponent.class
            )
    )
    static class ComponentFilterAppConfig {

    }
}
  • AnnotationConfigApplicationContext를 통해 Bean을 받아 오는데 includeFilters, excludeFilters 옵션에 따라서 Bean 스캔 대상 테스트를 진행 하였다

03-6. FilterType 옵션

FilterType은 5가지 옵션 존재

  • ANNOTATION: 기본값, 어노테이션을 인식해 동작
    • ex) org.example.SomeAnnotation
  • ASSIGNABLE_TYPE: 지정한 타입과 자식 타입을 인식해 동작
    • ex) org.example.SomeClass
  • ASPECTJ: AspectJ 패턴 사용
    • ex) org.example..Service
  • REGEX: 정규식
    • ex) org\.example\.Default.*
  • CUSTOM: TypeFilter라는 인터페이스를 구현해 처리
    • ex) org.example.MyTypeFilter

참고 : 최근에는 @Component면 충분하기에 includ, exclude는 자주 사용하지 않는다. 즉, 스프링 기본 설정에 맞춰 개발을 하는 것을 권장함

04. 중복 등록과 충돌

컴포넌트 스캔에서 같은 빈 이름을 등록하면 어떻게 될까? 다음 두 가지 상황이 존재한다.

  1. 자동 빈 등록 vs 자동 빈 등록
  2. 수동 빈 등록 vs 자동 빈 등록

04-1. 자동 빈 등록 vs 자동 빈 등록

  • 컴포넌트 스캔에 의해 자동으로 스프링 빈이 등록됨
  • 이 때 이름이 같은 경우 스프링은 오류를 발생 시킴
    • ConflictingBeanDefinitionException @ 예외 발생
     

04-2. OrderServiceImpl, MemberServiceImpl

@Component("service")
public class OrderServiceImpl implements OrderService {
	...
}
@Component("service")
public class MemberServiceImpl implements MemberService {
	...
}
  • @Component 안에 Bean 이름을 service로 동일하게 지정
  • ConflictingBeanDefinitionException 예외가 발생 한다

04-3. 수동 빈 등록 vs 자동 빈 등록

만약 수동 빈 등록과 자동 빈등록에서 빈 이름이 충돌되면 어떻게 될까?

@Component
public class MemoryMemberRepository implements MemberRepository {}
@ComponentScan(
//        basePackages = "hello.core.member",
//        basePackageClasses = AutoAppConfig.class,
        excludeFilters = @ComponentScan.Filter(
                type = FilterType.ANNOTATION,
                classes = Configuration.class
        )
)
@Configuration
public class AutoAppConfig {

    @Bean(name = "memoryMemberRepository")
    MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }

}
  • 이 경우 수동 빈 등록이 우선권을 가짐
  • 수동 빈이 자동 빈을 오버라이딩
  • 최근 스프링 부트에서는 수동 빈과 자동 빈의 이름이 충돌나면 자동으로 오류가 발생되도록 수정이 되었다

애매한 경우 적용을 하지 말자, 또한 어설픈 추상화 어설픈 빈 주입 등등… 애매하면 그냥 하지말고 정확하게 이해하고 적용하는게 좋음

블로그의 정보

기록하고, 복기하고

ymkim

활동하기