컴포넌트 스캔을 이용한 의존관계 주입
@Bean 등을 이용해 설정정보에서 스프링 빈을 등록하는 방법도 있지만,
실제 개발에서 사용하는 빈이 한두개도 아니고, @Bean으로 일일이 등록해주는 방법은 번거롭다.
그래서 보통 스프링 빈을 자동으로 등록해주는 방법을 이용하는데, 가장 자주 쓰이는 컴포넌트 스캔 방식에 대해 다룬다.
1. 컴포넌트 스캔 적용하기
방법은 간단하다.
1. 설정 정보에 @ComponentScan 어노테이션을 붙인다.
2. 빈으로 등록할 클래스들에 @Component 어노테이션을 붙인다.
3. @Autowired로 의존성을 주입한다.
1-1. 설정 정보에 @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") 방식으로 빈 이름을 임의로 지정할 수도 있다.
권장되는 방법은 아닌데, 바꾸면 헷갈리는 경우가 더 많기 때문이다.
+)
@ComponentScan을 붙일 때, 스캔에서 제외할 클래스로 AppConfig를 지정했었다.
근데 AppConfig에는 @Configuration만 붙어있다.
@Component가 붙어있지 않음에도 AppConfig를 제외 대상으로 지정한 이유는,
@Configuration에 @Component가 포함되어 있기 때문이다.
이런 어노테이션들이 몇가지 있다. 이후에 설명한다.
1-3. @Autowired로 의존성 주입하기
@Component를 이용해 빈을 등록하면, 클래스 간 의존성을 따로 명시해줄 방법이 없다.
그래서 보통 사용하는 방법은 @Autowired를 이용한 의존관계 자동 주입이다.
@Autowired를 붙이면 스프링이 의존성을 파악해 자동으로 빈을 주입해준다.
이 때 주입할 빈을 찾는 기준은 참조변수의 타입이다. (기본 설정)
예컨데 아래 MemberServiceImpl의 생성자에서 MemberRepository 타입의 의존성을 가지고 있으므로,
스프링 컨테이너는 등록된 빈 중 MemberRepository 타입의 객체를 찾아서 주입해주는 방식이다.
클래스 내 의존성을 주입받는 부분에 @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'
다음 글에서 탐색 위치를 지정하는 기준과 빈 이름이 충돌할 경우 등에 대해 다룬다.