[inflearn] 스프링 핵심 원리 - 기본편/섹션 6 - 컴포넌트 스캔

컴포넌트 스캔을 이용한 의존관계 주입

슬픈 야옹이 2024. 5. 11. 15:46

 

@Bean 등을 이용해 설정정보에서 스프링 빈을 등록하는 방법도 있지만,

 

실제 개발에서 사용하는 빈이 한두개도 아니고, @Bean으로 일일이 등록해주는 방법은 번거롭다.

 

그래서 보통 스프링 빈을 자동으로 등록해주는 방법을 이용하는데, 가장 자주 쓰이는 컴포넌트 스캔 방식에 대해 다룬다.

 

 

 

1. 컴포넌트 스캔 적용하기

방법은 간단하다.

 

1. 설정 정보에 @ComponentScan 어노테이션을 붙인다.

2. 빈으로 등록할 클래스들에 @Component 어노테이션을 붙인다.

3. @Autowired로 의존성을 주입한다.

 

 

 

1-1. 설정 정보에 @ComponentScan 어노테이션 붙이기

@ComponentScan 어노테이션을 붙이면, 스프링 빈을 컴포넌트 스캔 방식으로 등록하겠다는 의미다.

설정 클래스에 해당 어노테이션을 붙여준다.

 

@ComponentScan

 

// 컴포넌트 스캔을 이용해 자동으로 스프링 빈을 등록하도록 설정
@Configuration
@ComponentScan(
        // 컴포넌트 스캔 대상에서 @Configuration이 붙은 클래스를 제외한다.
        excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Configuration.class)
//        useDefaultFilters = true 기본 설정이 되어있음. false로 지정하면 기본 스캔 대상들이 제외됨.
)
public class AutoAppConfig {

    @Bean(name = "memoryMemberRepository") // 빈 이름이 충돌할 경우 수동으로 등록한 빈으로 덮어씌워진다.
    MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }

}

 

@ComponentScan 내부에 excludeFilters를 적용하면 스캔에서 제외할 대상들을 지정할 수 있다. (그다지 자주 사용하진 않는다)

 

여기선 이전 실습에 사용한 설정 클래스를 제외하기 위해 사용했다.

 

 

 

1-2. 빈으로 등록할 클래스에 @Component 어노테이션 붙이기

빈으로 등록할 클래스들에 @Component 어노테이션을 붙인다.

 

빈 이름은 첫 글자가 소문자로 바뀐 클래스 이름으로 등록된다. (기본 설정)

 

예) RateDiscountPolicy 클래스 -> rateDiscountPolicy 이름으로 등록됨

 

@Component("beanname") 방식으로 빈 이름을 임의로 지정할 수도  있다.

권장되는 방법은 아닌데, 바꾸면 헷갈리는 경우가 더 많기 때문이다.

 

@Component 붙이기

 

@Component 붙이기

 

@Component 붙이기

 

@Component 붙이기

 

 

+)

@ComponentScan을 붙일 때, 스캔에서 제외할 클래스로 AppConfig를 지정했었다.

 

근데 AppConfig에는 @Configuration만 붙어있다.

 

@Component가 붙어있지 않음에도 AppConfig를 제외 대상으로 지정한 이유는,

@Configuration에 @Component가 포함되어 있기 때문이다.

 

이런 어노테이션들이 몇가지 있다. 이후에 설명한다.

 

 

 

 

1-3. @Autowired로 의존성 주입하기

@Component를 이용해 빈을 등록하면, 클래스 간 의존성을 따로 명시해줄 방법이 없다.

 

그래서 보통 사용하는 방법은 @Autowired를 이용한 의존관계 자동 주입이다.

 

@Autowired를 붙이면 스프링이 의존성을 파악해 자동으로 빈을 주입해준다.

 

이 때 주입할 빈을 찾는 기준은 참조변수의 타입이다. (기본 설정)

 

예컨데 아래 MemberServiceImpl의 생성자에서 MemberRepository 타입의 의존성을 가지고 있으므로,

스프링 컨테이너는 등록된 빈 중 MemberRepository 타입의 객체를 찾아서 주입해주는 방식이다.

 

클래스 내 의존성을 주입받는 부분에 @Autowired 어노테이션을 붙인다.

@Autowired 붙이기

 

아래처럼 다수의 의존성을 가진 경우에도 자동으로 주입해준다.

@Autowired 붙이기

 

 

 

 

 

작성된 코드의 최종 형태는 다음과 같다.

 

AutoAppConfig.java

// 컴포넌트 스캔을 이용해 자동으로 스프링 빈을 등록하도록 설정
@Configuration
@ComponentScan(
        // 컴포넌트 스캔 대상에서 @Configuration이 붙은 클래스를 제외한다.
        excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Configuration.class)
//        useDefaultFilters = true 기본 설정이 되어있음. false로 지정하면 기본 스캔 대상들이 제외됨.
)
public class AutoAppConfig {

}

 

 

MemoryMemberRepository.java

@Component // memoryMemberRepository 이름으로 빈이 등록됨
public class MemoryMemberRepository implements MemberRepository {

    private static Map<Long, Member> store = new HashMap<>();

    @Override
    public void save(Member member) {
        store.put(member.getId(), member);
    }

    @Override
    public Member findById(Long memberId) {
        return store.get(memberId);
    }

}

 

RateDiscountPolicy.java

@Component
public class RateDiscountPolicy implements DiscountPolicy {

    // 할인률은 10퍼센트
    private int discountPercent = 10;

    // 회원 등급이 VIP일 경우 10% 할인
    @Override
    public int discount(Member member, int price) {
        if (member.getGrade() == Grade.VIP) {
            return price * discountPercent / 100;
        }
        else {
            return 0;
        }
    }

}

 

 

MemberServiceImpl.java

@Component
public class MemberServiceImpl implements MemberService {

//    private final MemberRepository memberRepository = new MemoryMemberRepository();

    private final MemberRepository memberRepository;

    // memberRepository에 들어갈 구현체를 생성자를 통해 전달받는다.
    @Autowired
    public MemberServiceImpl(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

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

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

    public MemberRepository getMemberRepository() {
        return this.memberRepository;
    }
}

 

 

OrderServiceImpl.java

@Component
public class OrderServiceImpl implements OrderService {

//    private final MemberRepository memberRepository = new MemoryMemberRepository();
//    private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

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

    @Override
    public Order createOrder(Long memberId, String itemName, int itemPrice) {
        Member member = memberRepository.findById(memberId);
        // OrderService는 할인에 관여하지 않고 discountPolicy에 member정보를 넘기기만 한다.
        // 단일 책임 원칙이 잘 지켜진 사례. 할인 정책이 변경되어도 OrderService는 변화가 없음.
        int discountPrice = discountPolicy.discount(member, itemPrice); // discount()는 할인 액수를 리턴함.

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

    public MemberRepository getMemberRepository() {
        return this.memberRepository;
    }

}

 

 

@Autowired는 생성자에 붙일 수도, 필드에 붙일 수도 있다. 상황에 따라 선택한다.

 

 

테스트 돌려보기

테스트 코드를 통해 빈이 생성되는 모습을 확인해본다.

 

package hdxian.hdxianspringcore.componentscan;

import hdxian.hdxianspringcore.AutoAppConfig;
import hdxian.hdxianspringcore.member.MemberService;
import hdxian.hdxianspringcore.member.MemberServiceImpl;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import static org.assertj.core.api.Assertions.*;

public class AutoAppConfigTest {

    @Test
    void basicScan() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class);
        MemberService memberService = ac.getBean(MemberService.class);
        assertThat(memberService).isInstanceOf(MemberServiceImpl.class);

    }

}

 

 

콘솔 출력 내용을 보면 Autowired를 통해 의존성이 주입되었다는 부분을 확인할 수 있다.

15:42:53.802 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Autowiring by type from bean name 'orderServiceImpl' via constructor to bean named 'memoryMemberRepository'
15:42:53.802 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Autowiring by type from bean name 'orderServiceImpl' via constructor to bean named 'rateDiscountPolicy'

 

 

다음 글에서 탐색 위치를 지정하는 기준과 빈 이름이 충돌할 경우 등에 대해 다룬다.