본문 바로가기

Backend/Spring

컴포넌트 스캔

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

스프링은 설정 정보가 없어도 자동으로 스프링 빈을 등록하는 컴포넌트 스캔이라는 기능을 제공한다.

또한, 의존 관계도 자동으로 주입하는 @Autowired라는 기능도 제공한다.

 

코드로 한 번 알아보자.

// AutoAppConfig.java
package ghdtjgus76.core;

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

@Configuration
@ComponentScan(excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Configuration.class)
public class AutoAppConfig {

}

컴포넌트 스캔을 사용하려면 @ComponentScan을 설정 정보에 붙여주면 된다.

기존 AppConfig와는 다르게 @Bean으로 등록한 클래스가 하나도 없다.

 

컴포넌트 스캔 사용 시 @Configuration이 붙은 설정 정보도 자동으로 등록되기 때문에 AppConfig, TestConfig 등 이전에 만들어두었던 설정 정보도 함께 등록되고 실행된다.

그래서 excludeFilters를 이용해서 설정 정보는 컴포넌트 스캔 대상에서 제외했다.

 

컴포넌트 스캔은 @Component 애노테이션이 붙은 클래스를 스캔해서 스프링 빈으로 등록한다.

@Configuration이 컴포넌트 스캔의 대상이 된 이유도 @Configuration 소스 코드에서도 @Component 애노테이션이 붙어있기 때문이다.

 

각 클래스가 컴포넌트 스캔의 대상이 되도록 @Component 애노테이션을 붙여주자.

// MemoryMemberRepository
package ghdtjgus76.core.member;

import org.springframework.stereotype.Component;

import java.util.HashMap;
import java.util.Map;

@Component
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
package ghdtjgus76.core.discount;

import ghdtjgus76.core.member.Grade;
import ghdtjgus76.core.member.Member;
import org.springframework.stereotype.Component;

@Component
public class RateDiscountPolicy implements DiscountPolicy {
    private int discountPercent = 10;

    @Override
    public int discount(Member member, int price) {
        if (member.getGrade() == Grade.VIP) {
            return price * discountPercent / 100;
        } else {
            return 0;
        }
    }
}
// MemberServiceImpl.java
package ghdtjgus76.core.member;

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

@Component
public class MemberServiceImpl implements MemberService {
    private final 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 memberRepository;
    }
}
// OrderServiceImpl.java
package ghdtjgus76.core.order;

import ghdtjgus76.core.discount.DiscountPolicy;
import ghdtjgus76.core.discount.FixDiscountPolicy;
import ghdtjgus76.core.discount.RateDiscountPolicy;
import ghdtjgus76.core.member.Member;
import ghdtjgus76.core.member.MemberRepository;
import ghdtjgus76.core.member.MemoryMemberRepository;
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.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }

    @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의존 관계를 자동으로 주입해준다.

또한 생성자에서 여러 의존 관계도 한 번에 주입받을 수 있다.

 

// AutoAppConfigTest.java
package ghdtjgus76.core.scan;

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

import static org.assertj.core.api.AssertionsForClassTypes.assertThat;

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

 

컴포넌트 스캔과 자동 의존 관계 주입이 어떻게 동작하는지 그림으로 알아보자.

 

@ComponentScan

@ComponentScan은 @Component가 붙은 모든 클래스를 스프링 빈으로 등록한다.

이때 스프링 빈의 기본 이름은 클래스명을 사용하되 맨 앞 글자만 소문자를 사용한다.

 

@Autowired 의존 관계 자동 주입

생성자에 @Autowired를 지정하면 스프링 컨테이너가 자동으로 해당 스프링 빈을 찾아서 주입한다.

기본 조회 전략은 타입이 같은 빈을 찾아서 주입한다.

 

 

탐색 위치와 기본 스캔 대상

필요한 위치부터 탐색하도록 시작 위치를 지정할 수 있다.

// AutoAppConfig.java
package ghdtjgus76.core;

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

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

}

이는 basePackages를 설정하면 되는데, 이 패키지를 포함해서 하위 패키지를 모두 탐색하도록 한다.

basePackageClasses는 지정한 클래스의 패키지를 탐색 시작 위치로 지정한다.

따로 지정하지 않으면 @ComponentScan이 붙은 설정 정보 클래스의 패키지가 시작 위치가 된다.

위 경우는 ghdtjgus76.core가 될 것이다.

 

권장하는 방법은 따로 패키지 위치를 지정하지 않고 설정 정보 클래스의 위치를 프로젝트 최상단에 두는 것이다.

스프링 부트 사용 시 스프링 부트의 대표 시작 정보인 @SpringBootApplication을 프로젝트 시작 루트 위치에 두는 것이 관례이다.

이 설정 안에 @ComponentScan이 들어 있다.

 

컴포넌트 스캔 기본 대상

컴포넌트 스캔은 @Component뿐만 아니라 다음과 같은 애노테이션도 추가로 대상에 포함한다.

- @Controller: 스프링 MVC 컨트롤러에서 사용

- @Service: 스프링 비즈니스 로직에서 사용

- @Repository: 스프링 데이터 접근 계층에서 사용

- @Configuration: 스프링 설정 정보에서 사용

 

애노테이션에는 따로 상속 관계가 없다.

애노테이션이 특정 애노테이션을 포함하고 있는 것을 인식할 수 있는 것은 자바 언어가 지원하는 기능이 아니라 스프링이 지원하는 기능이다.

 

필터

- includeFilters: 컴포넌트 스캔 대상을 추가로 지정한다.

- excludeFilters: 컴포넌트 스캔에서 제외할 대상을 지정한다.

// MyIncludeComponent.java
package ghdtjgus76.core.scan.filter;

import java.lang.annotation.*;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyIncludeComponent {
}
// MyExcludeComponent.java
package ghdtjgus76.core.scan.filter;

import java.lang.annotation.*;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyExcludeComponent {
}
// BeanA.java
package ghdtjgus76.core.scan.filter;

@MyIncludeComponent
public class BeanA {
}
// BeanB.java
package ghdtjgus76.core.scan.filter;

@MyExcludeComponent
public class BeanB {
}
// ComponentFilterAppConfigTest.java
package ghdtjgus76.core.scan.filter;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.context.ApplicationContext;
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.AssertionsForClassTypes.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;

public class ComponentFilterAppConfigTest {
    @Test
    void filterScan() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(ComponentFilterAppConfig.class);

        BeanA beanA = ac.getBean("beanA", BeanA.class);
        assertThat(beanA).isNotNull();

        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 {
    }
}

 

@Component면 충분해서 includeFilters를 사용할 일은 거의 없지만 excludeFilters는 간혹 사용하는 경우가 있다.

 

중복 등록과 충돌

컴포넌트 스캔에서 같은 빈 이름을 등록한 경우를 두 가지로 나눠서 알아보자.

 

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

컴포넌트 스캔에 의해 자동으로 스프링 빈이 등록되는데, 그 이름이 같은 경우 스프링은 ConflictingBeanDefinitionException 예외를 발생시킨다.

 

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

// MemoryMemberRepository.java
package ghdtjgus76.core.member;

import org.springframework.stereotype.Component;

import java.util.HashMap;
import java.util.Map;

@Component
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);
    }
}
// AutoAppConfig.java
package ghdtjgus76.core;

import ghdtjgus76.core.member.MemberRepository;
import ghdtjgus76.core.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 = "ghdtjgus76.core.member",
        excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes =  Configuration.class)
)
public class AutoAppConfig {
    @Bean(name = "memoryMemberRepository")
    MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }

}

이 경우 수동 빈 등록이 우선권을 가져서 수동 빈이 자동 빈을 오버라이딩해버린다.

 

스프링 부트에서는 이 경우 오류를 발생시키도록 기본값을 변경했다.

'Backend > Spring' 카테고리의 다른 글

빈 생명주기 콜백  (0) 2023.08.03
의존 관계 자동 주입  (0) 2023.08.03
싱글톤 컨테이너  (0) 2023.08.02
스프링 컨테이너와 스프링 빈  (0) 2023.08.02
스프링 예제-2  (0) 2023.08.02