package sangyunpark.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 {
}
참고 : 컴포넌트 스캔을 사용하려면 @Configuration이 붙은 설정 정보도 자동으로 등록되기 때문에, AppConfig, TestConfig 등 앞서 만들어두었던 설정 정보도 함께 등록되고, 실행되어 버린다. 그래서 excludeFilters를 이용해서 설정정보는 컴포넌트 스캔 대상에서 제외했다. 스캔 대상에서 제외하지는 않지만, 기존 예제 코드를 최대한 남기고 유지하기 위해서 이 방법을 선택
컴포넌트 스캔은 이름 그대로 @Component
애노테이션이 붙은 클래스를 스캔해서 스프링 빈으로 등록한다.
@Component
를 붙여주자
참고 : @Configuration이 컴포넌트 스캔의 대상이 된 이유도 @Configuration 소스코드를 열어보면 @Component 애노테이션이 붙어있기 때문이다.
각 클래스가 컴포넌트 스캔의 대상이 되도록 @Component 애노테이션을 붙여주자
package sangyunpark.core.member;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
@Component
public class MemoryMemberRepository implements MemberRepository{
public 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);
}
}
package sangyunpark.core.discount;
import org.springframework.stereotype.Component;
import sangyunpark.core.member.Grade;
import sangyunpark.core.member.Member;
@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;
}
}
}
package sangyunpark.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 // ac.getBean(MemberRepository.class)
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);
}
// test 용도
public MemberRepository getMemberRepository() {
return memberRepository;
}
}
@Autowired
는 의존관계를 자동으로 주입해준다.package sangyunpark.core.order;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import sangyunpark.core.discount.DiscountPolicy;
import sangyunpark.core.member.Member;
import sangyunpark.core.member.MemberRepository;
@Component
public class OrderServiceImpl implements OrderService {
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);
int discountPrice = discountPolicy.discount(member, itemPrice); // 할인 금액 첨부
return new Order(memberId, itemName, itemPrice, discountPrice);
}
public MemberRepository getMemberRepository() {
return memberRepository;
}
}
package sangyunpark.core.scan;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import sangyunpark.core.AutoAppConfig;
import sangyunpark.core.member.MemberService;
import static org.assertj.core.api.Assertions.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
은 @Component
가 붙은 모든 클래스를 스프링 빈으로 등록한다.@Component("memberService")
이름을 부여하면 된다.getBean(MemberRepository.class)
와 동일하다고 이해하면 된다.컴포넌트 스캔에서 같은 빈 이름을 등록하면 어떻게 될까?
다음 두가지 상황이 있다.
ComflictingBeanDefinitionException
예외 발생package sangyunpark.core;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import sangyunpark.core.member.MemoryMemberRepository;
@Configuration
@ComponentScan( // 제외할 부분을 지정
excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Configuration.class)
)
public class AutoAppConfig {
@Bean(name="memoryMemberRepository")
MemoryMemberRepository memberRepository(){
return new MemoryMemberRepository();
}
}
이 경우 수동 빈 등록이 우선권을 가진다.
(수동 빈이 자동 빈을 오버라이딩 해버린다.)
Overriding bean definition for bean 'memoryMemberRepository' with a different definition: replacing
물론 개발자가 의도적으로 이런 결과를 기대했다면, 자동 보다는 수동이 우선권을 가지는 것이 좋다. 하지만 현실은 개발자가 의도적으로 설정해서 이런 결과가 만들어지기 보다는 여러 설정들이 꼬여서 이전 결과가 만들어지는 경우가 대부분이다!
그래서 최근 스프링 부트에서는 수동 빈 등로고가 자동 빈 등록이 충돌나면 오류가 발생하도록 기본 값을 바꾸었다.
The bean 'memoryMemberRepository', defined in class path resource [sangyunpark/core/AutoAppConfig.class], could not be registered. A bean with that name has already been defined in file [/Users/developer/Downloads/core/out/production/classes/sangyunpark/core/member/MemoryMemberRepository.class] and overriding is disabled.
스프링 부트인 CoreApplication을 실행해보면 오류를 볼 수 있다.
spring.main.allow-bean-definition-overriding=true
위와같이 설정해주면 overriding이 가능해진다.