해당 게시글은 김영한 강사님의 스프링 핵심 원리 강의를 바탕으로 작성하였습니다.
https://www.inflearn.com/courses/lecture?courseId=325969&tab=curriculum&type=LECTURE&unitId=55396&subtitleLanguage=ko&audioLanguage=ko
이전 포스팅까지 학습한 내용으로 본다면, 지금까지 우리는 스프링 빈을 등록할 때는 자바 코드의 @Bean이나 XML의 <bean>을 통해서 설정 정보에 직접 등록할 스프링 빈을 나열하였다.
근데 프로젝트 규모가 조금만 커져도 스프링 빈을 일일이 등록하고 있기에는 너무 힘들고 누락할 위험도 존재한다.
따라서 스프링은 설정 정보가 없어도 자동으로 스프리 빈을 등록하는 Component Scan 기능을 제공한다.
아래는 새로운 설정정보 클래스인 AutoAppConfig.java 이다.
@Configuration
@ComponentScan(
excludeFilters = @Filter(type = FilterType.ANNOTATION,
classes = Configuration.class))
public class AutoAppConfig {
}
컴포넌트 스캔을 사용하기 위해서는 일단 @ComponentScan을 설정 정보에 붙여주면 된다. 기존의 AppConfig 와는 다르게 @Bean으로 등록한 클래스가 하나도 없다는 점에 주목하자.
참고로 excludeFilters는 @Configuration 역시 스캔 대상이 되어 기존에 우리가 만든 설정 정보인 AppConfig, TestConfig도 함께 등록되는 것을 방지하기 위해 추가해 주었다.
일반적으로는 제외하지 않고 모두 스캔하니 해당 예제에서만 적용했다는 점 참고하길 바란다.
이제 스캔할 대상을 찾을 수 있도록 스프링 빈으로 등록할 부분에 @Component 어노테이션을 붙여주자.
@Component
public class MemoryMemberRepository implements MemberRepository { ... }
@Component
public class RateDiscountPolicy implements DiscountPolicy { ... }
@Component
public class MemberServiceImpl implements MemberService {
private final MemberRepository memberRepository;
@Autowired
public MemberServiceImpl(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
}
Component Scan 동작에 대해 스프링 빈 저장소와 함께 도식화해보면 아래와 같다.

@ComponentScan은 @Component가 붙은 클래스를 스프링 빈으로 등록한다.@Component("memberService2)와 같이 사용하면 된다.근데 여기서 한가지 의문점이 생긴다. 기존의 @Bean 등록 방식은 설정정보인 AppConfig.class 에 각 빈이 가지는 의존관계를 등록해 주었지만, 지금과 같이 Component Scan을 쓰면 어느 위치에서 의존관계를 설정해줄 것인가?
정답은 위 코드에서 마지막 부분인 MemberServiceImpl에서 사용하는 @Autowired에 있다.
의존관계를 자동으로 주입해주는 어노테이션이며, 보통 @Component와 함께 자주 사용한다.
@Component
public class MemberServiceImpl implements MemberService {
private final MemberRepository memberRepository;
@Autowired
public MemberServiceImpl(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
}
이를 스프링 빈 저장소와 함께 도식화해보면 다음과 같다.

생성자에 @Autowired를 지정하면 스프링 컨테이너가 자동으로 해당 스프링 빈을 찾아서 주입해 주는데, 이때 기본 조회 전략은 타입이 같은 빈을 찾아서 주입한다.
getBean(MemberRepository.class)와 동일하다고 이해하면 된다.
근데 MemberRepository는 구현체가 아닌 인터페이스이다! 따라서 이를 구현한 클래스인 MemoryMemberRepository를 찾아서 주입된다고 이해하면 된다.
또한 다음과 같이 2개 이상의 의존 관계도 자동으로 찾아서 주입한다.
@Component
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
// 생성자 주입 코드
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, @MainDiscountPolicy DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
}

지금까지는 생성자 방식을 통한 의존관계 주입을 사용하였다. 생성자 방식을 가장 많이 사용하고 권장하는 방법이지만, 가끔씩 수정자 주입(setter 주입), 필드 주입 방식도 사용할 일이 있으니 추가로 설명하도록 하겠다.
지금까지 배운 방법이니 어떻게 사용하는 지는 위의 내용을 참고하면 되고, 한 가지 팁은 생성자가 딱 1개만 있으면 @Autowired를 생략해도 자동 주입 기능이 동작한다.
@Component
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
// 생성자가 딱 1개인 경우 @Autowired 생략해도 동일한 기능 보장
public OrderServiceImpl(MemberRepository memberRepository, @MainDiscountPolicy DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
}
주로 필수, 불변인 의존관계에서 사용한다.
setter라 불리는 필드의 값을 변경하는 수정자 메서드를 통해서 의존관계를 주입하는 방법이다.
@Component
public class OrderServiceImpl implements OrderService {
private MemberRepository memberRepository;
private DiscountPolicy discountPolicy;
@Autowired
public void setMemberRepository(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Autowired
public void setDiscountPolicy(DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
}
}
주로 선택, 변경 가능성이 있는 의존관계에서 사용한다.
이름 그대로 필드에 바로 @Autowired를 써서 (냅다 꽂아버리는) 사용하는 방식
@Component
public class OrderServiceImpl implements OrderService {
@Autowired
private MemberRepository memberRepository;
@Autowired
private DiscountPolicy discountPolicy;
}
코드가 가장 간결해서 좋아보이지만, 외부에서 변경이 불가능하여 테스트 하기 힘들다는 치명적인 단점이 존재한다.
DI 프레임워크가 없으면 아무것도 할 수 없다.
실제 코드와 관계 없는 테스트 코드나 특별한 용도가 아닌 이상 사용하지 말자.
막상 개발 해보면 대부분이 다 불변이고 필수적으로 의존관계를 가지는 경우가 대다수라 필드에 final 키워드와 함께 생성자 주입 방식을 사용했다.
@Component
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
// 생성자가 딱 1개인 경우 @Autowired 생략해도 동일한 기능 보장
public OrderServiceImpl(MemberRepository memberRepository, @MainDiscountPolicy DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
}
근데 롬복의 @RequiredArgsConstructor를 사용하면 아래와 같이 동일한 기능이지만 코드를 더욱 간편하게 만들 수 있다.
@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
}
@RequiredArgsContructor는 이름 그대로 final 키워드가 붙은 필드에 대해 자동으로 생성자를 만들어주는 어노테이션이다.
따라서 요즘 트렌드는 생성자를 딱 1개 두고 @Autowired를 생략한 다음, @RequiredArgsConstuctor를 사용하면 모든 기능을 제공하면서 코드는 간결하게 사용할 수 있다.
모든 자바 클래스를 Component Scan 하려면 시간이 너무 오래 걸리는 문제가 있다. 따라서 필요한 위치부터 탐색하도록 시작 위치를 지정할 수 있다.
@ComponentScan(
basePackages = "hello.core",
)
이렇게 작성하면 "hello.core" 패키지를 기준으로 탐색을 진행하게 된다.
만약 따로 지정하지 않으면 @ComponentScan이 붙은 설정 정보 클래스의 패키지가 시작 위치가 된다.
따라서 보통 설정 정보 클래스의 위치를 프로젝트 최상단에 두는 것을 권장하는 편이다.
근데 이 역시도 스프링부트를 사용하면 생략할 수 있다. 왜냐하면 스프링부트 시작 지점인 @SpringBootApplication에 @ComponentScan이 이미 들어가 있기 때문이다.
@SpringBootApplication
public class PureToSpringApplication {
public static void main(String[] args) {
SpringApplication.run(PureToSpringApplication.class, args);
}
}
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
excludeFilters = {@Filter(
type = FilterType.CUSTOM,
classes = {TypeExcludeFilter.class}
), @Filter(
type = FilterType.CUSTOM,
classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication {
그래서 이론적으로는 @ComponentScan을 배우긴 했지만, 사실 바로 @Component만 붙이고 돌려도 빈 저장소에 등록할 수 있는 것이다.
추가로 @ComponentScan은 @Component 외에도 다음과 같은 어노테이션도 빈 등록 대상에 포함시킨다.
@Controller: 스프링 MVC 컨트롤러로 인식@Service: 특별한 처리를 하진 않지만, 개발자들이 핵심 비지니스 로직이 있겠구나 하고 식별하는데 도움을 줌@Repository: 스프링 데이터 접근 계층으로 인식하고 데이터 계층의 예외를 스프링 예외로 변환해줌@Configuration: 스프링 설정 정보로 인식하여 스프링 빈이 싱글톤을 유지하도록 추가 처리