컴포넌트 스캔과 의존 관계 자동 주입 시작하기
스프링은 설정 정보가 없어도 자동으로 스프링 빈을 등록하는 컴포넌트 스캔이라는 기능을 제공한다.
또한, 의존 관계도 자동으로 주입하는 @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 |