컴포넌트 스캔을 이용해 스프링 의존 관계 자동 주입하기

심현민·2024년 9월 13일
2

Spring

목록 보기
9/18

컴포넌트 스캔과 의존관계 자동 주입

  • 지금까지 스프링 빈을 등록할 때는 자바 코드의 @Bean이나 XML의 <bean> 등을 통해서 설정 정보에 직접 등록할 스프링 빈을 나열했다.
  • 스프링 빈을 사실 일일이 등록하기는 귀찮을 수 있고, 누락하는 문제도 발생할 수 있다.
  • 그래서 스프링은 설정 정보 없이 자동으로 스프링 빈을 등록하는 컴포넌트 스캔이라는 기능을 제공한다.
  • 의존관계도 자동으로 주입하는 @Autowired 기능도 있다.
  • @ComponentScan@Component 애노테이션이 붙은 클래스를 찾아서 자동으로 스프링 빈에 등록한다.
package com.example.demo;

import com.example.demo.member.MemberRepository;
import com.example.demo.member.MemoryMemberRepository;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;

@Configuration
@ComponentScan(
//        basePackages = "com.example.demo.member",
        //어디서부터 찾는지 시작 위치를 설정해줄 수 있다.
        //지정을 하면
        //이 패키지를 포함한 하위 패키지를 모두 탐색한다.
        //만약 지정하지 않으면 `@ComponentScan`이 붙은 설정 정보 클래스의 패키지가 시작 위치가 된다.
        //권장하는 방법은 패키지 위치를 지정하지 않고, 설정 정보 클래스의 위치를 프로젝트 최상단에 두는 것
        excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Configuration.class)
        //스프링 빈 등록하는 것 중에 뺄 것이 있으면 빼주는 부분
        //AppConfig에서 @Configuration을 썼기 때문에 제외해주는 것이다. 중복 등록이 될 수 있기 때문에
)
public class AutoAppConfig {

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

*이제 스프링 빈을 등록한 자바 파일로 가서 @Component 애노테이션만 붙혀주면 된다.

MemberServiceImpl

package com.example.demo.member;


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class MemberServiceImpl implements MemberService {

    private final MemberRepository memberRepository;

    @Autowired  //자동 의존관계 주입 //ac.getBean(MemberRepository.class)과 비슷하게 동작
    public MemberServiceImpl(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
    //생성자 주입 , AppConfig가 대신 해준다.

    @Override
    public void join(Member member) {
        memberRepository.save(member);
    }

    @Override
    public Member findMember(Long memberId) {
        return memberRepository.findById(memberId);
    }

    //테스트 용도
    public MemberRepository getMemberRepositoy() {
        return memberRepository;
    }
}

OrderServiceImpl

package com.example.demo.order;

import com.example.demo.discount.DiscountPolicy;
import com.example.demo.member.Member;
import com.example.demo.member.MemberRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class OrderServiceImpl implements OrderService{

    private final MemberRepository memberRepository;
    private DiscountPolicy discountPolicy;
    @Autowired
    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
        this.discountPolicy = discountPolicy;
        this.memberRepository = memberRepository;
    }
    //생성자 주입 , AppConfig가 맡아서 한다.

    @Override
    public Order createOrder(Long memberId, String itemName, int itemPrice) {
        Member member = memberRepository.findById(memberId);
        int discountPrice = discountPolicy.discount(member, itemPrice);

        return new Order(memberId, itemName, itemPrice, discountPrice);
    }

    public MemberRepository getMemberRepository(){
        return memberRepository;
    }

}

그렇다면, 이렇게 애노테이션을 통해 빈을 등록하는 것까지는 좋지만 가장 중요한 의존 관계 주입은 어떻게 하는걸까?


이때 쓰이는 것이 바로 @Autowired이다.

  • @Autowired를 사용하면 생성자에서 여러 의존관계도 한번에 주입받을 수 있다.

  • AnnotationConfigApplicationContext를 사용하는 것은 기존과 동일

  • 다만, 설정 정보로 AutoAppConfig클래스를 넘겨준다.

  • @Component이 붙은 모든 클래스를 찾아서 스프링 컨테이너에 싱글톤으로 자동 등록

    • 우리가 일반적으로 아는 스프링 컨테이너 형태로 저장한다.
  • 스프링 빈의 이름은 애노테이션이 붙은 클래스명을 사용하되 맨 앞글자만
    소문자를 사용

    • MemberServiceImpl -> memberServiceImp
  • @Autowired 애노테이션은 생성자에 파라미터가 많아도 타입으로 다 찾아서 자동으로 주입l

    • 중복 타입이 있을 때 충돌이 일어나는 경우 해결책은 아래에 소개하겠다.


탐색 위치와 기본 스캔 대상

  • 모든 자바 클래스를 다 컴포넌트 스캔하면 시간이 오래 걸린다. 그래서 꼭 필요한 위치부터 탐색하도록
    시작 위치를 지정할 수 있다.
    AutoAppConfig 코드
    basePackages = "com.example.demo.member”,
    • 어디서부터 찾는지 시작 위치를 설정해줄 수 있다.
    • 지정을 하면 이 패키지를 포함한 하위 패키지를 모두 탐색한다.
    • 만약 지정하지 않으면 @ComponentScan이 붙은 설정 정보 클래스의 패키지가 시작 위치가 된다.
    • 권장하는 방법은 패키지 위치를 지정하지 않고, 설정 정보 클래스의 위치를 프로젝트 최상단에 두는 것
      • ex)
        com.hello
        com.hello.service
        com.hello.repository
        가 있다면, 설정 정보 클래스의 위치를 com.hello로 하여 최상단에 두는 것이다.


컴포넌트 스캔 기본 대상

  • 컴포넌트 스캔은 @Componenet 뿐만 아니라 아래 내용도 추가 대상에 포함한다.
  • @Component : 컴포넌트 스캔에서 사용
  • @Controller : 스프링 MVC 컨트롤러에서 사용
  • @Service: 스프링 비즈니스 로직에서 사용
  • @Repository : 스프링 데이터 접근 계층에서 사용
  • @Configuration : 스프링 설정 정보에서 사용
  • 애노테이션에는 상속관계라는 것이 없다. 그래서 이렇게 애노테이션이 특정 애노테이션을 들고 있는 것일 인식할 수 있는 것은 스프링이 이를 지원하기 때문이다.
@Component
public @interface Controleer{
}

@Component
public @interface Service{
}

@Component
public @interface Configuration{
} 
  • 컴포넌트 스캔의 용도 뿐만 아니라 다음 애노테이션이 있으면 스프링은 부가 기능을 수행한다.
    • @Controller : 스프링 MVC 컨트롤러로 인식
    • @Repository : 스프링 데이터 접근 계층으로 인식하고, 데이터 계층의 예외를 스프링 예외로 변환해준다.
    • @Configuration : 앞서 보았듯이 스프링 설정 정보로 인식하고, 스프링 빈이 싱글톤을 유지하도록 추가 처
      리를 한다.
  • @Service : 개발자들이 핵심 비즈니스 로직이 여기에 있겠구나라고 비즈니스 계층을 인식하는데 도움이 된다.


필터

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

MyExcludeComponent(직접 만드는 애노테이션)

package com.example.demo.scan.filter;

import java.lang.annotation.*;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented

public @interface MyExcludeComponent {

}

MyIncludeComponent(직접 만드는 애노테이션)

package com.example.demo.scan.filter;

import java.lang.annotation.*;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented

public @interface MyIncludeComponent {

}

BeanA

package com.example.demo.scan.filter;

@MyIncludeComponent
public class BeanA {
}

BeanB

package com.example.demo.scan.filter;

@MyExcludeComponent
public class BeanB {
}

ComponentFilterAppConfigTest

package com.example.demo.scan.filter;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;

import static org.assertj.core.api.Assertions.*;
import static org.springframework.context.annotation.ComponentScan.*;

public class ComponentFilterAppConfigTest {

    @Test
    void filterScan(){
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(ComponentFilterAppConfig.class);
        BeanA beanA = ac.getBean("beanA", BeanA.class);
        assertThat(beanA).isNotNull();
        System.out.println("beanA = " + beanA);

//        ac.getBean("beanB", BeanB.class);
//        org.junit.jupiter.api.Assertions.assertThrows(
//                NoSuchBeanDefinitionException.class,
//                () -> ac.getBean("beanB", BeanB.class)
//        );
        //bean은 exclude를 했기 때문에
    }
    @Configuration
    @ComponentScan(
            includeFilters = @Filter(type = FilterType.ANNOTATION, classes = MyIncludeComponent.class),
            excludeFilters = @Filter(type = FilterType.ANNOTATION, classes = MyExcludeComponent.class)
    )
    static class ComponentFilterAppConfig{}

}
  • @MyIncludeComponent애노테이션이 붙은 beanA는 추가됐고 @MyExcludeComponent애노테이션이 붙은 beanB는 추가되지 않았다.


중복 등록과 충돌

컴포넌트 스캔에서 같은 빈 이름을 등록하면 어떻게 될까?

두 가지 경우가 있다.

  • 자동 빈 등록 vs 자동 빈 등록
  • 수동 빈 등록 vs. 자동 빈 등록
    -> ConfilictingBeanDefinitionException 예외 발생

AutoAppConfig 부분에 memoryMemberRepository라는 이름을 가진 빈을 생성하고
테스트를 돌려보자


AutoAppConfig에 추가한 모습

public class AutoAppConfig {

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

이 경우 수동 등록이 우선순위를 가진다.

다만, 나중에 방대한 양의 코드를 다룰 때 이런 문제가 발생하면 굉장히 복합적인 오류가 발생할 수 있어서
스프링이 자체적으로 오류를 발생시켜서 막는다.

profile
혼자 성장하는 것보다 함께 성장하는 것을 선호합니다.

0개의 댓글