티스토리 뷰

Spring

[Spring] Component Scan & Autowired

MAENCO 2021. 8. 13. 17:13
반응형

스프링 컨테이너에 빈을 등록하기 위해서는 @Bean이나 XML의 <bean>을 통해서 직접 등록할 수도 있지만

수많은 스프링 빈을 등록할 때는 매우 비효율적이다

 

Component

@Bean이나 <bean>을 사용하지 않고 빈을 등록할 수 있게 스프링은 Component Scan(컴포넌트 스캔)을 제공하는데

말 그대로인 컴포넌트 '스캔'을 위해서 '컴포넌트'를 먼저 등록해주어야 한다

@Component
public class MemoryMemberRepository implements MemberRepository {}

이렇게 등록된 컴포넌트들은 컴포넌트 스캔이 이 어노테이션의 여부를 보고 스프링의 빈으로 등록하게 된다

 

Component Scan

컴포넌트 스캔이 어노테이션들을 조회할 기준은 base package를 통해 결정된다

@ComponentScan(
          basePackages = "hello.core",
}

이렇게 패키지를 지정하게 되면 해당 패키지의 모든 하위 패키지를 모두 포함하여 컴포넌트들을 조회한다

(배열 형식으로 여러 패키지를 지정할 수도 있다)

base package를 사용할 수도 있지만 대부분은 패키지를 지정하지 않고

설정 정보 클래스의 위치를 프로젝트 최상단에 두어 모든 하위 패키지를 조회하는 법을 선호한다고 한다

스프링 부트 또한 이 방법을 디폴트로 제공한다

 

정리해보자면 @Configuration이 지정된 설정 파일의 @Bean 객체들을 조회하여 빈을 등록하는데

이를 더욱 간편화 하여 최상의 패키지에 설정정보 파일을 두고 @ComponentScan을 설정하면 해당 클래스를 포함한

모든 하위 클래스에서 @Component라고 지정되어 있는 클래스를 찾아 빈으로 등록해주는 것이다

 

컴포넌트 스캔은 Component 이외에도 여러 어노테이션을 조회대상으로 포함하고 부가기능을 수행할 수도 있다

1. @Controller :

스프링 MVC 컨트롤러에서 사용되며, 이 어노테이션을 지정할 시 클라이언트가 요청한 URI를 활용하여 처리한다

 

2. @Service :

서비스는 특별한 부가기능은 없으나, 개발자들이 핵심 비즈니스 로직에 관한 비즈니스 계층을 인식하는 데 사용한다

 

3. @Repository :

스프링 데이터 접근 계층으로 인식하여 데이터 계층의 예외를 스프링 예외로 변환시켜준다

 

4. @Configuration :

스프링 설정 정보로 인식하고 스프링 빈이 싱글톤을 유지하도록 추가 처리를 한다

 

Component Filter

이렇게 컴포넌트 스캔이 가능한 어노테이션 말고도 개발자가 직접 어노테이션을 만들 수 있는데

이러한 어노테이션들은 필터를 통해 컴포넌트 스캔이 조회대상에 포함할지 말지를 정의할 수도 있다

// 컴포넌트 스캔 포함
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyIncludeComponent {
}

// 컴포넌트 스캔 제외
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyExcludeComponent {
}

예시로 두가지의 어노테이션을 만들어 

@MyIncludeComponent
public class BeanA {
}


@MyExcludeComponent
public class BeanB {
}

클래스에 각각 적용시킨 후

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

컴포넌트 스캔에 위와 같이 includeFilters / excludeFilter로 지정하게 되면

include는 포함되어 조회가 되고 exclude는 해당 클래스를 조회 대상에서 제외시켜버린다

 

필터 타입에는 5가지의 옵션이 있다고 한다

1. ANNOTAION : 기본값이며, 어노테이션을 인식해서 동작한다 

2. ASSIGNABLE_TYPE : 지정한 타입과 자식 타입을 인식해서 동작한다

3. ASPECTJ : AspectJ 패턴을 사용한다

4. REGEX : 정규 표현식

5. CUSTOM : TypeFilter 인터페이스를 구현해서 처리

 

하지만 Component만으로 개발을 하기에 충분하기 때문에 filter를 사용하는 경우는 매우 드물다고 한다

 

Bean Duplication

컴포넌트 스캔이 자동을 빈을 등록해주는데, 만약 개발자가 설정 정보에서 수동으로 빈을 등록하게 되면 어떻게 될까

어찌 보면 당연하게도 수동으로 등록한 빈이 우선권을 가지며 자동 등록되어야 하는 빈을 오버 라이딩시켜버린다

(개발자가 의도한 등록이니까)

 

하지만 빈을 중복으로 등록되는 상황이 매우 위험한 문제를 야기할 수 있고

스프링 부트 또한 중복으로 등록된 빈을 오류 처리하여 중복등록 자체를 지양하는 것이 좋다고 한다

 

Autowired (의존관계 자동 주입)

컴포넌트 스캔이 자동으로 빈을 등록하는 것을 살펴봤다

하지만 설정 파일 없이 등록된 빈들은 의존관계를 주입할 수가 없다

그렇기 때문에 Autowired를 사용하여 의존관계를 자동으로 주입하게 된다

@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;
    }
 }

 

생성자에 @Autowired 지정하면 스프링 컨테이너가 자동으로 해당 빈을 찾아서 주입한다 (생성자가 많다고 해도 알아서 주입해준다)

 

의존관계를 주입하는 방법은 크게 4가지가 있다

 

1. 생성자 주입

생성자를 통해서 의존관계를 주입하는 방법이다, 생성자 호출 시점에 딱 1번 호출되는 것을 보장하며 불변&필수적인 의존관계에 사용한다

@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;
      }
      
}

 

2. 수정자 주입(Setter)

setter 메서드를 통해서 의존관계를 주입하는 방법이다 선택, 변경 가능성이 있는 의존관계에 사용한다

@Component
public class OrderServiceImpl implements OrderService {
     private MemberRepository memberRepository;
     private DiscountPolicy discountPolicy;
        
     @Autowired
     public void setMemberRepository(MemberRepository memberRepository) {
          this.memberRepository = memberRepository;
     }
        
     @Autowired
     public void setDiscountPolicy(DiscountPolicy discountPolicy) {
          this.discountPolicy = discountPolicy;
     }
}

 

3. 필드 주입

말 그대로 필드에 바로 주입하는 방법이다

@Component
public class OrderServiceImpl implements OrderService {
        
    @Autowired
    private MemberRepository memberRepository;
        
    @Autowired
    private DiscountPolicy discountPolicy;
    
}

필드 주입이 코드 작성에는 편하지만 외부에서 변경이 불가능하여 테스트 코드 작성이 힘들고

DI 프레임워크가 없다면 아무것도 할 수 없다는 단점이 있다

 

4. 일반 메서드 주입

일반 메서드를 통해서 의존관계를 주입한다

@Component
public class OrderServiceImpl implements OrderService {
        
    private MemberRepository memberRepository;
    private DiscountPolicy discountPolicy;
        
    @Autowired
    public void init(MemberRepository memberRepository, DiscountPolicy
    discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }
        
 }

일반적으로 잘 사용하지 않는다고 한다

 

이렇게 4가지의 의존관계 주입 방법을 살펴봤는데

결론부터 말하자면 생성자 주입을 사용하는 것이 가장 좋다고 한다

 

대부분의 의존관계 주입은 한번 일어나면 애플리케이션의 종료 시점까지

그 관계를 변경할 일이 없으며 오히려 변하면 안 되는 경우가 훨씬 많다

그렇기 때문에 변경이 가능한 수정자 주입이나, 테스트 작성이 힘든 필드 주입 그리고 잘 사용하지 않는 일반 메서드 주입을 모두 제외하면

결국 생성자 주입을 사용하는 것이 가장 효율적일 것이다 또한 스프링을 포함한 대부분의 DI프레임워크가 생성자 주입을 권장한다

 

생성자 주입의 장점 중 하나가 final 키워드 또한 사용할 수 있는데

final 키워드는 값이 꼭 입력되어야 하기 때문에 컴파일에 오류를 잡아줄 확률이 높다

하지만 예외가 존재하지 않는 것은 불가능하니 생성자 주입을 베이스로 하되 꼭 필요한 부분에서만 수정자 주입을 섞어 사용하면 되겠다

 

중복 조회

Autowired는 타입으로 조회하여 개발자 대신 의존성을 주입한다

하지만 타입으로 조회할 때 아래와 같이 선택된 빈이 2개 이상이면 문제가 발생한다

@Component
public class FixDiscountPolicy implements DiscountPolicy {
}

@Component
pulib class RateDiscountPolicy implements DiscountPolicy {
}

/**/

@Autowired
private DiscountPolicy discountPolicy

바로 어떤 빈을 주입할지 모르기 때문인데 해결책으로는 보통 3가지 방법을 쓴다고 한다

 

1.Autowired

Autowired 같은 경우 필드명 혹은 파라미터명으로 빈을 찾아 매칭 하는데

 @Autowired
 public void init(MemberRepository memberRepository, DiscountPolicy
 rateDiscountPolicy) {
   this.memberRepository = memberRepository;
   this.rateDiscountPolicy = rateDiscountPolicy;
 }
 
 @Autowired
 private DiscountPolicy rateDiscountPolicy

파라미터의 이름이나 필드의 이름으로 매칭 되는 빈을 매칭 시켜주는 것이다

 

2.Qualifier

빈을 등록할 때 추가 구분자를 붙여주는 방법이다 이때 주의해야 할 점은 구분을 하기 위한 이름이지

빈 이름 자체를 변경하는 것은 아니다

(Qualifier를 사용하기 위해서는 꼭 값을 같이 넣어주어야 한다)

@Component
@Qulifier("fixDiscountPolicy")
public class FixDiscountPolicy implements DiscountPolicy {
}

꼭 값이 있어야 하기 때문에 아래와 같이 생성자나 setter의 경우에도 값을 입력해야 한다

//생성자의 경우
@Autowired
public OrderServiceImpl(MemberRepository memberRepository,
@Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy) {
   this.memberRepository = memberReposityro;
   this.discountPolicy = discountPolicy;
}

//Setter의 경우
@Autowired
public DiscountPolicy setDiscountPolicy(@Qualifier("mainDiscountPolicy")
DiscountPolicy discountPolicy) {
    return discountPolicy;
}

이렇게 등록된 Qualifier들은 서로 매칭 되는 이름을 찾아 자동 주입을 하게 된다

 

3.Primary

@Primary란 어노테이션만 붙여주면 우선순위를 가지기 때문에 사용하기가 간편하다

@Component
@Primary
public class FixDiscountPolicy implements DiscountPolicy {
}

@Component
pulib class RateDiscountPolicy implements DiscountPolicy {
}

/**/

@Autowired
private DiscountPolicy discountPolicy

이렇게 간단하게 @Primary만 선언해준다면 같은 타입 안에서 우선수위를 가지게 되는 것이다

 

Primary의 간편한 사용법도 한계점이 존재한다 이 때문에 Qualifier를 같이 혼용해서 사용한다

예를 들어 메인로직 / 보조로직 있다고 했을 때

메인로직에 priamry 값을

(아주 드물게 사용하는) 보조 로직에 사용할 때만 Qualifier 값을 주어 사용하는 것이다

이는 primary보다 qualifier의 조회범위가 좁기 때문에 우선권을 비교했을 때 qualifier가 우선권이 높다

 

List 와 Map으로 조회하기

만약 모든 빈을 조회하거나 동적으로 조회해야 한다면 위의 방법만으로는 한계가 있다

이때 활용할 수 있는 것이 바로 List와 Map이다

// fixDiscountPolicy & rateDiscountPolicy 총 2개의 빈이 있다

static class DiscountService {
     private final Map<String, DiscountPolicy> policyMap;
     private final List<DiscountPolicy> policies;

     @Autowired
     public DiscountService(Map<String, DiscountPolicy> policyMap, List<DiscountPolicy> policies) {
         this.policyMap = policyMap;
         this.policies = policies;
     }

     public int discount(Member member, int price, String discountCode) {
         DiscountPolicy discountPolicy = policyMap.get(discountCode);
     return discountPolicy.discount(member, price);
     }
}

Map에 key값을 빈의 이름으로 넣어, 파라미터 값으로 조회하게 설계하면

메서드를 통하여 모든 빈들을 이름에 따라 동적으로 조회도 가능하다

void findAllBean() {
     ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class, DiscountService.class);
     DiscountService discountService = ac.getBean(DiscountService.class);
     
     Member member = new Member(1L, "userA", Grade.VIP);
     int discountPrice = discountService.discount(member, 10000, "fixDiscountPolicy");
}

 

자동 빈과 수동 빈

개발자 입장에서 편한 자동 빈만 사용하면 너무 좋겠지만 수동 빈이 필요한 시점도 분명 있다

그래서 기본적으로 자동 빈을 사용하지만 어떤 상황에서 수동 빈을 사용하여야 하는지 알아보자

 

기술 지원 Bean:

기술적인 문제나 AOP를 처리할 때 주로 사용되는 기술 지원 빈은 DB 연결이나 공통 로그 같은 로직을 지원하는 하부 or 공통 기술이다

이런 경우 애플리케이션 '전반'에 걸쳐 영향이 가기 때문에 수동 빈으로 등록하여 설정 정보를 바로 알 수 있게 하는 것이 유지보수에 더 효율적이라고 한다

 

자동빈이든 수동빈이든 굉장히 중요한 요점 중 하나는

한눈에 알 수 있어야 한다는 것이다, 특정 패키지에 같이 묶어 놓는 방법 등을 사용하여

내가 아닌 타 개발자가 보더라도 알아보기 쉽게 만들어야 한다

 

핵심정리

빈을 수동 또는 자동으로 등록하는 방법이 있다

기본적으로는 자동을 사용하되, 애플리케이션 전반에 영향을 끼치는 빈은 수동으로 등록하는 것이 유지보수에 용이하다

 

하지만 자동으로 설정 파일 없이 컴포넌트 스캔을 이용하면 의존 관계를 주입할 수가 없다

이때 @Autowired이 자동으로 의존성을 주입하는 역할을 한다 또한 여러 가지 방법으로 이 의존성을 주입할 수 있는데

대부분 값이 필수적이고 변하지 않는 의존관계는 final로 설정하여 컴파일 쪽에서 에러를 잡아줄 수 있는 생성자 주입을 사용하는 것이 좋다

 

Autowired 같은 경우 타입으로 조회하기 때문에 중복된 빈이 있다면

primary와 qulifier를 통하여 빈의 사용 우선순위를 정의하고, 직접 애노테이션을 만들어 사용할 수도 있다

또한 List와 Map을 활용하면 모든 빈을 동적으로 조회하는 것도 가능하다

 

더보기

개인 학습을 위해 작성되는 글입니다.

제가 잘못 알고 있는 점에 대한 지적 / 더 나은 방향에 대한 댓글을 환영합니다.

 

참조 링크:

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8/dashboard

반응형

'Spring' 카테고리의 다른 글

[Spring] Web Scope(웹 스코프)  (0) 2021.08.17
[Spring] Bean & Bean Scope  (0) 2021.08.16
[Spring] Singleton(싱글톤)  (0) 2021.08.13
[Spring] Spring Container(스프링 컨테이너)  (0) 2021.08.11
[Spring] POJO(Plain Old Java Object)  (0) 2021.08.11
댓글