일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
31 |
- java
- 일급 객체
- Spring
- spring security
- middleware
- 일급 컬렉션
- Dependency Injection
- synchronized
- Google OAuth
- nestjs
- lombok
- builder
- factory
- OAuth 2.0
- Volatile
- Today
- Total
HJW's IT Blog
컴포넌트 스캔과 의존성 자동 주입 본문
컴포넌트 스캔 / Dependency Injection
컴포넌트 스캔과 의존관계 자동 주입
이전 포스팅 까지는, Spring Bean 등록시, @Bean 을 사용해 설정 정보에 직접 등록 해 주었다. 하지만, 프로젝트 규모가 커질 수록, 일일이 등록하기도 힘들고, 누락하는 문제도 발생한다. 이번 포스팅에선, 자동으로 Spring Bean 을 등록하는 방법에 대해 알아보자
우선, AutoAppConfig.java 파일을 생성.
package hello.core;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import static org.springframework.context.annotation.ComponentScan.*;
@Configuration
@ComponentScan(
excludeFilters = @Filter(type = FilterType.ANNOTATION, classes =
Configuration.class))
public class AutoAppConfig {
}
@ComponentScan 을 사용하게 되면, @Component 어노테이션이 붙은 클래스를 스캔하여 스프링 빈으로 등록하게 된다.
이제, 다음과 같이 @Component 등록을 해주자.
@Component
public class MemoryMemberRepository implements MemberRepository {}
@Component
public class RateDiscountPolicy implements DiscountPolicy {}
@Component
public class MemberServiceImpl implements MemberService {}
근데 잘 생각해보자, MemberServiceImpl 은, AppConfig 를 통해 의존관계 주입을 해주었다.
// 이전 AppConfig
@Bean
public MemberService memberService(){
System.out.println("Calling memberService");
return new MemberServiceImpl(memberRepository());
}
하지만, 방금 새로 만든 AutoAppConfig.java 는 그 어떠한 정보도 명시하고 있지 않다. 오로지 @Component 에 의해 자동으로 Spring Bean 으로 등록이 되는데, 그렇다면 어떻게 의존관계를 주입할 수 있을까?
@Autowired
public MemberServiceImpl(MemberRepository memberRepository){
this.memberRepository = memberRepository;
}
생성자에 위와 같이 @Autowired 어노테이션을 붙여주자. 이렇게 되면 Spring 은 MemberRepository 타입에 맞는 클래스를 찾아와서 의존관계 주입을 자동으로 해준다.
@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;
}
OrderServiceImpl 도 같은 방식으로 component 등록과 의존성 주입을 해준다.
그럼 이제 테스트를 통해 잘 등록되고 동작하는지 알아보자.
package hello.core.scan;
import hello.core.member.MemberService;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class AutoAppConfigTest {
@Test
void basicScan(){
ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class);
MemberService memberService = ac.getBean(MemberService.class);
Assertions.assertThat(memberService).isInstanceOf(MemberService.class);
}
}
원리 알아보기
@ComponentScan 은 @Component 가 붙은 모든 클래스를 Bean 으로 등록한다. 이때, Bean 의 기본 이름은 클래스 명을 사용하되, 맨 앞글자만 소문자를 사용한다. 기존 AppConfig 와 마찬가지로 이름을 부여할 수도 있다.

@Autowired 가 지정되어 있으면, 스프링 컨테이너가 자동으로 해당 스프링 빈을 찾아서 주입한다. 기본 조회 전략은 타입이 같은 bean 을 찾아 주입하는 것이다.

ComponentScan 탐색 위치
모든 JAVA 클래스를 컴포넌트 스캔하게 될 경우, 시간이 오래 걸린다. 그래서 꼭 필요한 위치부터 탐색하도록 시작 위치를 지정할 수 있는데, 다음과 같이 선언해주면 된다.
@ComponentScan(basePackages = "hello.core")
- 탐색할 패키지의 시작 위치를 지정하며, 해당 패키지부터 하위 패키지를 모두 탐색하게 된다.
- basePackages = {"hello.core", "hello.service"} 처럼 여러 시작 위치를 지정할 수도 있다.
ComponentScan 은 Component 를 포함하는 모든 어노테이션을 Bean 으로 등록한다
- Controller, Service, Repository, Configuration
필터
includeFilters : 컴포넌트 스켄 대상 추가 지정
excludeFilters : 컴포넌트 스켄 제외 대상 지정
예시를 통해 빠르게 알아보자.
Annotation 클래스로 다음과 같이 두가지를 만들었다.
package hello.core.scan.filter;
import java.lang.annotation.*;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyExcludeComponent {
}
package hello.core.scan.filter;
import java.lang.annotation.*;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyIncludeComponent {
}
위에서 생성한 어노테이션을 통해 bean 클래스도 생성해 주자
package hello.core.scan.filter;
@MyIncludeComponent
public class BeanA {
}
package hello.core.scan.filter;
@MyExcludeComponent
public class BeanB {
}
package hello.core.scan.filter;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
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.junit.jupiter.api.Assertions.*;
public class ComponentFilterAppConfigTest {
@Test
void filterScan(){
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(ComponentFilterAppConfig.class);
BeanA beanA = ac.getBean("beanA", BeanA.class);
Assertions.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{
}
}
위 테스트의 ComponentFilterAppConfig 에 include, exclude 필터를 통해 scan 할 대상을 지정해 주었다. exclude 에 BeanB의 annotation 인 MyExcludeComponent이 붙어있기에, 해당 요소는 ComponentScan 의 대상에서 제외되고, bean 조회시, 정상적으로 에러가 터지는 것을 확인할 수 있다.
FilterType 옵션
- ANNOTATION : annotation 을 인식하여 동작
- ASSIGNABLE_TYPE : 지정한 type 과 자식 type 을 인식하여 동작
- ASPECTJ : AspectJ 패턴 사용
- REGEX : 정규 표현식
- CUSTOM : TypeFilter 이라는 인터페이스를 구현하여 처리
중복 등록 충돌
ComponentScan 에서 같은 Bean 이름이 등록된다면?
- 자동 등록 vs 자동 등록의 경우 → ConflictingBeanDefinitionException 발생
- 수동 등록 vs 자동 등록 → 수동 Bean 이 우선권을 가지게 된다
다양한 의존관계 주입 방법
의존관계 주입 4가지 방법
생성자 주입
이름 그래도 생성자를 통해 의존관계를 주입받는 방법으로, 생성자 호출 시점에 딱 1번 호출되는 것이 보장된다. 불변 / 필수 의존관계에 사용된다.
@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;
}
}

생성자가 딱 1개있는 클래스의 경우, Autowired 를 생략해도 된다.
수정자 (Setter) 주입
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(required = false) 로 지정하자
필드 주입
필드에서 직접 주입하는 방식인데, 외부에서 변경이 불가능하다. 그렇게에 테스트 하기 힘들다는 단점이 있으며, DI 프레임 워크가 필수적이다. 왠만하면 사용하지 말자.
@Component
public class OrderServiceImpl implements OrderService {
@Autowired
private MemberRepository memberRepository;
@Autowired
private DiscountPolicy discountPolicy;
}
일반 메서드 주입
일반 메서드를 통해 주입 받는 방식으로, 여러 필드를 한번에 주입 받을 수 있다. 일반적으로 잘 사용되지 않는 방식이다.
@Component
public class OrderServiceImpl implements OrderService {
private MemberRepository memberRepository;
private DiscountPolicy discountPolicy;
@Autowired
public void init(MemberRepository memberRepository, DiscountPolicy
discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
옵션 처리
주입할 Spring Bean 이 없어도 동작해야 할 때가 있다.
- @Autowired(required=false) : 이 방식은 자동 주입할 대상이 없다면 setter 메서드 자체가 호출이 안된다.
- @Nullable : 자동 주입할 대상이 없다면 null 이 입력된다
- Optional<> : 자동 주입할 대상이 없다면 Optional.empty 가 입력된다.
생성자 주입을 선택하라
생성자 주입을 사용하게 되면 다음과 같은 이점이 있다.
불변성
- 대부분의 의존관계 주입은 한번 일어나면 어플리케이션 종료시점까지 변할 이유가 없다. 오히려 변해서는 안된다
- 수정자 주입을 사용하면 setXxx 메서드를 public 으로 열어두어야 한다 → 좋은 설계가 아니다
- 객체가 생성될 때 딱 1변만 호출되면 된다
누락
- 프레임워크 없이 순수한 JAVA 코드를 단위 테스트 하는 경우, 수정자 의존관계인 경우, 실행은 되나, NPE 가 발생한다. 하지만 생성자 주입을 사용한다면, 컴파일 오류가 발생하기에, 어떤 값을 주입해야 하는지 바로 알 수 있다.
Final 키워드
- 생성자 주입을 사용하면 final 키워드를 사용하여, 혹시라도 값이 설정되지 않는 오류를 막아준다.
- 생성자 주입이 아닌 다른 방법은 모두 생성자 이후 실행되므로 final 키워드 사용이 불가능 하다.
롬복과 최신 트렌드
막상 개발을 해보면 대부분이 다 불변이기에 final 키워트를 사용하게 된다. 하지만 생성자, 주입, 등등 여러 귀찮은 부분이 많은데, 좀 편리하게 사용할 수 있는 방법은 없을까?
Lombok 을 사용하자 → 여러 기능이 있으니 이후 포스팅에서 다루도록 하겠다.
@RequiredArgsConstructor 를 사용하면 final 이 붙은 필드를 모두 생성자를 자동으로 만들어준다.
최근에는 생성자를 딱 1개 두고, Autowired 를 생략하는 방법을 주로 사용한다.
조회 빈이 2개 이상일때
@Autowired 는 기본적으로 type 으로 조회한다. 같은 type 이 두가지가 있다면 어떻게 될까?
@Component
public class FixDiscountPolicy implements DiscountPolicy {}
@Component
public class RateDiscountPolicy implements DiscountPolicy {}
이렇게 두가지의 DiscountPolicy 가 있을때 자동 의존관계 주입을 사용하면, NoUniqueBeanDefinitionException 오류가 발생한다.
이 때, 하위 타입으로 지정할 수도 있지만, 유연성의 문제, DIP 의 위반 등의 문제가 있다. 그렇다면 어떻게 해결할까?
@Qualifier, @Primary
메서드 명 매치
조회 대상 빈이 2개 이상일때, 필드명 매칭시키는 방법이 있다.
- @Autowired 는 우선 type matching 을 시도하는데, 여러 bean 이 존재한다면, 필드 이름, 파라미터 이름으로 추가 매칭한다
public class OrderServiceImpl implements OrderService{
private final MemberRepository memberRepository;
private final DiscountPolicy rateDiscountPolicy;
@Override
public Order createOrder(Long memberId, String itemName, int itemPrice) {
Member member = memberRepository.findById(memberId);
int discountPrice = rateDiscountPolicy.discount(member, itemPrice);
return new Order(memberId, itemName, itemPrice, discountPrice);
}
- 이렇게 변경하고 이전에 실패한 test 를 다시 돌려보면 통화한다.
@Qualifier 사용
@Qualifier 는 추가 구분자를 붙여주는 방법이다. Bean 등록시 @Qualifier 를 붙여주면 되는데, 다음과 같다
@Component
@Qualifier("mainDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy {}
@Component
@Qualifier("fixDiscountPolicy")
public class FixDiscountPolicy implements DiscountPolicy {}
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, @Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
@Qualifier 로 주입할 때, matching 되는 qualifer 이름이 없다면, main 이 붙은 spring bean 을 추가로 찾는다.
@Primary 사용
@Primary 는 우선 순위를 정하는 방법으로, 만약 여러 bean 이 조회될 시, @Primary 가 우선순위를 가지게 된다.
@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy {}
@Component
public class FixDiscountPolicy implements DiscountPolicy {}

@Qualifier 는 @Primary 보다 우선순위가 높다.
어노테이션 직접 만들기
@Qualifier("mainDiscountPolicy) 와 같이 문자를 적을 시, 컴파일타임 체크가 안된다. 이 문제는 직접 어노테이션을 만들어 문제를 해결할 수 있다.
package hello.core.annotataion;
import org.springframework.beans.factory.annotation.Qualifier;
import java.lang.annotation.*;
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER,
ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Qualifier("mainDiscountPolicy")
public @interface MainDiscountPolicy {
}
@Component
@MainDiscountPolicy
public class RateDiscountPolicy implements DiscountPolicy {}
@Autowired
public OrderServiceImpl(MemberRepository memberRepository,
@MainDiscountPolicy DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
조회한 빈이 모두 필요할 때
특정 타입의 스프링 빈이 전부 필요한 경우도 있는데, 예를 들어 할인 서비스를 제공하는데, 클라이언트가 할인의 종류를 선택할 수 있다고 가정해 보자. 그렇다면, rate 와 fix discount policy 모두 필요할 것이다.
static class DiscountService{
private final Map<String, DiscountPolicy> policyMap;
private final List<DiscountPolicy> policies;
@Autowired
public DiscountService(Map<String, DiscountPolicy> policyMap, List<DiscountPolicy> policies) {
this.policyMap = policyMap;
this.policies = policies;
System.out.println("policyMap = " + policyMap);
System.out.println("policies = " + policies);
}
이 때, Spring 은 DiscountPolicy 의 구현체들을 수집하여 policyMap과 policies에 주입하는데, 동작 과정은 다음과 같다.
- Spring 은 DiscountPolicy 타입의 빈을 모두 검색한다
- Map 에 주입시, Bean 의 이름을 사용하여 주입한다 (앞글자 소문자)
- List 주입시 Bean 객체를 사용한다
'SPRING' 카테고리의 다른 글
프론트 컨트롤러와 DispatcherServlet (0) | 2025.01.09 |
---|---|
스프링 빈 초기화·소멸 로직 : 인터페이스, Bean, @PostConstruct (0) | 2024.12.12 |
스프링 싱글톤 컨테이너의 동작 원리 (0) | 2024.12.02 |
스프링 컨테이너와 스프링 빈 (1) | 2024.11.29 |
객체 지향 설계와 스프링 (2) | 2024.11.27 |