2025. 4. 14. 12:35ㆍSpring Framework/스프링 핵심 원리
컴포넌트 스캔과 의존관계 자동 주입
1. 지금까지의 스프링 빈 등록 및 객체 생성, 의존관계 주입의 과정
//스프링 컨테이너 생성, 매개변수에 설정정보 파일 전달
ApplicationContext ac =
new AnnotationConfigApplicationContext(AppConfig.class);
@Configuration //설정정보 클래스임을 알리는 애노테이션, 싱글톤 보장
public class AppConfig {
@Bean //자바코드 메서드와 @Bean 애노테이션을 통해 스프링 컨테이너에 빈 수동 등록
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
@Bean
public OrderService orderService() {
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
@Bean
public DiscountPolicy discountPolicy() {
return new FixDiscountPolicy();
}
}
- 스프링 컨테이너를 생성할 때는 언제나 설정 정보 파일을 매개변수로 전달해야한다.
이때 자바 설정 정보 파일에는 등록할 Bean 객체를 생성하고 반환하는 메서드들을 작성한다. - 스프링이 자바 코드로 설정할 수 있는 방법을 제공하기 위해 @Configuration 클래스 안에서 @Bean 메서드들을 통해 객체를 생성하고 조립하게 해놓은 것이다. 이러한 자바 메서드 방식은 스프링이 xml파일이 아니라 우리에게 익숙한 자바 코드로 편리하게 설정 파일을 작성할 수 있도록 제공하는 것이다.
- 애노테이션들의 뜻
- @Configuration: 해당 클래스가 설정 정보라는 걸 알려주는 애노테이션, 싱글톤을 보장한다.
- @Bean: 해당 메서드의 반환값을 스프링 컨테이너가 빈으로 등록하라는 뜻.
@Bean 메서드의 이름이 해당 메서드가 반환하는 객체의 빈 이름이 된다.
- @Bean 메서드에 파라미터가 있는 경우 스프링이 자동으로 파라미터에 들어오는 객체를 생성하고 해당 타입의 빈을 찾아서 의존관계를 주입해준다.
- 흐름 정리
스프링 컨테이너 생성
-> 설정 정보 파일에서 @Bean 메서드 확인, 이 메서드들을 기반으로 BeanDefinition 생성, 내부적으로 설계도 등록됨
-> BeanDefinition 기반으로 @Bean 메서드를 실행해서 객체 생성 및 의존관계 주입
2. 컴포넌트 스캔과 의존관계 자동 주입의 필요성
- 지금까지 스프링 빈을 등록할 때는 AppConfig 라는 설정 정보 파일에
자바 코드의 @Bean이나 XML의 <bean> 등을 통해서 직접 등록할 스프링 빈을 나열하고 의존관계를 설정함 - 예제에서는 몇 개가 안되었지만, 이렇게 등록해야 할 스프링 빈이 수십, 수백개가 되면 일일이 등록하기도 귀찮고, 설정 정보도 커지고, 누락하는 문제도 발생할 수 있음
- 그래서 스프링에서는 빈 등록, 객체 생성 및 의존관계 주입을 직접 코드를 작성하지 않아도 자동으로 할 수 있도록 지원해준다.
3. 컴포넌트 스캔과 의존관계 자동 주입
1) 컴포넌트 스캔: @ComponentScan, @Component
- 스프링 컨테이너가 생성될 때 전달받은 설정 정보 class에 @ComponentScan이 붙어있는 경우 컴포넌트 스캔을 진행한다.
- 컴포넌트 스캔이란 스프링이 클래스패스를 뒤져서 @Component, @Service, @Repository, @Controller가 붙어 있는 클래스들을 찾아내고 이 클래스들을 BeanDefinition이라는 이름표(설계도) 형태로 스프링 컨테이너의 빈 저장소에 저장하는 것이다.
- 따라서 설정정보 파일에 개발자가 직접 메서드를 작성하지 않아도 빈 등록이 가능해진다.
2) 의존관계 자동 주입: @Autowired
- 스프링이 정의된 빈을 실제 객체로 만들 때, 생성자, setter, field 등에 붙어있는 @Autowired를 보고 의존성을 자동으로 주입함.
- 이때, 어떤 방식으로 의존성을 주입하는지에 따라 라이프 사이클이 달라지는데 이는 추후 자세히 설명
3) 스프링에서 컴포넌트 스캔, 자동 의존관계 주입이 진행 되는 과정 정리
스프링 컨테이너가 생성될 때 @ComponentScan을 실행함
→ 스캔 범위 내의 클래스 중에 @Component 라는 애노테이션이 있는 클래스들을 모두 스프링 빈 저장소에 스프링 빈으로 등록함
→ 이후 정의된 빈을 실제 객체로 만드는데 이때 생성자, setter, field 등에 붙어있는 @Autowired를 보고 자동으로 의존성을 주입함.
@Autowired 애노테이션이 붙어있으면 스프링 컨테이너가 등록되어 있는 스프링 빈 중 타입이 같은 빈을 찾아서 자동으로 의존 관계를 주입하면서 객체를 생성한다.
3. 스프링 코드로 확인
@Configuration
@ComponentScan
public class AutoAppConfig {
//원래 AppConfig는 이곳에 개발자가 직접 등록할 스프링 빈을 나열하고 의존관계를 주입했지만
//스프링 컨테이너가 지원하는 컴포넌트 스캔과 자동 의존관계 주입을 사용하면 아무 코드도 쓰지 않아도 된다.
}
- 컴포넌트 스캔을 사용하려면 설정 정보 클래스 위에 @ComponentScan 애노테이션을 붙여주면 된다.
-
기존의 AppConfig와는 다르게 @Bean으로 등록한 클래스가 하나도 없어도 된다!
-
참고로 @Configuration 소스코드를 열어보면 @Component 애노테이션이 붙어있기 때문에
@Configuration도 컴포넌트 스캔의 대상이 된다.
@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
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;
}
}
- 이전에 AppConfig에서는 @Bean으로 직접 설정 정보를 작성했고, 의존관계도 직접 명시했다.
- 이제는 이런 설정 정보를 AppConfig에 명시적으로 작성하는 것이 아니라, 애노테이션을 통해 클래스 내에서 설정한다.
- 스프링 빈으로 등록하고 싶은 클래스 위에 @Component 애노테이션을 달아 컴포넌트 스캔이 일어날 수 있도록 하고
- 의존관계를 주입하고 싶은 경우 생성자 위에 @Autowired를 달아 스프링이 의존관계를 자동으로 주입하도록 한다.
public class AutoAppConfigTest {
@Test
void basicScan() {
ApplicationContext ac =
new AnnotationConfigApplicationContext(AutoAppConfig.class);
MemberService memberService = ac.getBean(MemberService.class);
assertThat(memberService).isInstanceOf(MemberService.class);
}
}
- 스프링 컨테이너를 생성할 때 새로 만든 AutoAppConfig.class를 설정 정보로 넘기고 실제로 컴포넌트 스캔이 잘 일어났는지 확인해본다.
컴포넌트 스캔 - @ComponentScan
1. 탐색 위치와 기본 스캔 대상
1) 탐색할 패키지의 시작 위치 지정하기
@Configuration
@ComponentScan(basePackages = "hello.core")
public class AutoAppConfig { }
- 모든 자바 클래스를 다 컴포넌트 스캔하면 시간이 오래 걸린다. 그래서 꼭 필요한 위치부터 탐색하도록 시작 위치를 지정할 수 있다.
- basePackages : 탐색할 패키지의 시작 위치를 지정한다. 이 패키지를 포함해서 하위 패키지를 모두 탐색한다.
- basePackages = "hello.core"
- basePackages = {"hello.core", "hello.service"} 처럼 여러 시작위치 지정도 가능하다.
- basePackageClasses: 클래스의 패키지를 탐색 시작 위치로 지정할 수도 있다.
- 디폴트 위치: 만약 지정하지 않으면 @ComponentScan이 붙은 설정 정보 클래스(AppConfig)의 패키지가 시작 위치가 된다.
- 권장 방법: ComponentScan에 패키지 위치를 지정하지 않고, 설정 정보 클래스의 위치를 프로젝트 최상단에 두는 것
최근 스프링 부트도 이 방법을 기본으로 제공한다.
2) 컴포넌트 스캔 기본 대상
@Component : 컴포넌트 스캔에서 사용하는 기본 애노테이션.
@Configuration : 스프링 설정 정보로 인식하고, 스프링 빈이 싱글톤을 유지하도록 추가 처리
@Controller : 스프링 MVC 컨트롤러로 인식한다.
@Repository : 스프링 데이터 접근 계층으로 인식하고, 데이터 계층의 예외를 스프링 예외로 변환해준다.
@Service : 스프링 비즈니스 로직에서 사용.
특별한 처리를 하지 않지만 개발자들이 핵심 비즈니스 로직이 여기에 있겠구나라고 비즈니스 계층을 인식하는데 도움이 된다.
- 컴포넌트 스캔은 @Component 뿐만 아니라 위와 같은 애노테이션이 붙은 클래스들도 추가로 대상에 포함한다.
해당 애노테이션 클래스의 소스 코드를 보면 @Component를 내부적으로 포함하고 있다. - 컴포넌트 스캔의 용도 뿐만 아니라 다음 애노테이션이 있으면 스프링은 부가 기능을 수행한다.
2. 필터
@Configuration
@ComponentScan(
includeFilters = @Filter(type = FilterType.ANNOTATION,
classes = MyIncludeComponent.class),
excludeFilters = @Filter(type = FilterType.ANNOTATION,
classes = MyExcludeComponent.class))
public class ComponentFilterAppConfig { }
-
includeFilters : 컴포넌트 스캔 대상을 추가로 지정한다.
@MyIncludeComponent 애노테이션이 붙은 클래스는 컴포넌트 스캔 대상이 된다.
-
excludeFilters : 컴포넌트 스캔에서 제외할 대상을 지정한다.
@MyExcludeComponent 애노테이션이 붙은 클래스는 컴포넌트 스캔 대상에서 제외된다. -
FilterType 옵션 5가지
-
ANNOTATION: 기본값, 애노테이션을 인식해서 동작한다. ex) org.example.SomeAnnotation
-
ASSIGNABLE_TYPE: 지정한 타입과 자식 타입을 인식해서 동작한다. ex) org.example.SomeClass
-
ASPECTJ: AspectJ 패턴 사용 ex) org.example..*Service+
-
REGEX: 정규 표현식 ex) org\.example\.Default.*
-
CUSTOM: TypeFilter이라는 인터페이스를 구현해서 처리 ex) org.example.MyTypeFilter
-
-
@Component면 충분하기 때문에, includeFilters를 사용할 일은 거의 없고 excludeFilters는 여러가지 이유로 간혹 사용할 때가 있지만 많지는 않다. 특히 최근 스프링 부트는 컴포넌트 스캔을 기본으로 제공하는데, 옵션을 변경하면서 사용하기 보다는 스프링의 기본 설정에 최대한 맞추어 사용하는 것을 권장한다.
3. 중복 등록과 충돌
- 스프링 컨테이너가 컨포넌트 스캔을 하는데 이름이 같은 빈이 중복해서 존재하면 어떻게 될까?
- 자동빈등록vs자동빈등록: ConflictingBeanDefinitionException 예외 발생
- 수동빈등록vs자동빈등록: 수동 빈이 자동 빈을 오버라이딩 해버림 (수동빈이 우선)
단, 이런 경우 잡기 어려운 버그가 생기는 경우가 많기 때문에 스프링 부트부터는 오류가 나도록 변경됐다.
의존관계 자동 주입 - @Autowired
1. 다양한 의존관계 주입 방법
1) 생성자 주입
2) 수정자 주입 (setter 주입)
3) 필드 주입
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번만 호출되는 것이 보장된다.
- 불변, 필수 의존관계에 사용
-
중요! 생성자가 딱 1개만 있으면 @Autowired를 생략해도 자동 주입 된다. (물론 스프링 빈인 경우에만 해당)
2) 수정자 주입 (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;
}
}
- 선택, 변경 가능성이 있는 의존관계에 사용
- 자바빈 프로퍼티 규약의 수정자 메서드 방식을 사용하는 방법이다.
자바빈 프로퍼티 규약: 필드의 값을 직접 변경하지 않고, setXxx, getXxx 라는 메서드를 통해서 값을 읽거나 수정하는 규칙
3) 필드 주입
@Component
public class OrderServiceImpl implements OrderService {
@Autowired
private MemberRepository memberRepository;
@Autowired
private DiscountPolicy discountPolicy;
}
- 코드가 간단해서 좋아보이지만 DI 프레임워크가 있는 곳에서만 동작하는 단점
- 순수 자바로는 테스트조차 할 수 없기 때문에 사용하지 않는 것을 권장
- 스프링 컨테이너를 사용하는 곳에서만 제한적으로 사용
예: @SpringBootTest, @Configuration 같은 곳
4) 일반 메서드 주입
@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;
}
}
- 일반적으로 잘 사용하지 않는다.
2. 생성자 주입과 롬복
1) 생성자 주입을 사용하라!
과거에는 수정자 주입과 필드 주입을 많이 사용했지만, 최근에는 스프링을 포함한 DI 프레임워크 대부분이 생성자 주입을 권장한다.
- 이유 1: 불변
- 대부분의 의존관계 주입은 한번 일어나면 애플리케이션 종료시점까지 의존관계를 변경할 일이 없다. 오히려 대부분의 의존관계는 애플리케이션 종료 전까지 변하면 안된다.(불변해야 한다.)
- 수정자 주입을 사용하면, setXxx 메서드를 public으로 열어두어야 한다. 누군가 실수로 변경할 수 도 있고, 변경하면 안되는 메서드를 열어두는 것은 좋은 설계 방법이 아니다.
- 생성자 주입은 객체를 생성할 때 딱 1번만 호출되므로 이후에 호출되는 일이 없다. 따라서 불변하게 설계할 수 있다.
- 이유 2: 누락 방지 (컴파일 에러 활용)
- 단위 테스트를 할 때는 스프링 프레임워크나 데이터베이스 연동 없이 순수한 자바 코드로 해야하는 경우가 많다.
- 이런 경우 생성자 주입을 사용하면 객체를 생성할 때 주입 데이터를 누락 했을 때 컴파일 오류가 발생하고
IDE에서 바로 어떤 값을 필수로 주입해야 하는지 알 수 있다. - 수정자 주입은 의존관계 주입 데이터 누락시 널포인트 예외가 뜨는데 이것보다 컴파일 오류가 훨씬 편리함
- 컴파일 오류는 세상에서 가장 빠르고, 좋은 오류다!
- 이유 3: 필드에 final 키워드 사용 가능 (컴파일 에러 활용)
-
생성자 주입을 사용하면 필드에 final 키워드를 사용할 수 있다.
final 키워드를 사용하는 필드는 선언과 동시에 초기화되지 않은 경우 반드시 초기화 블럭 또는 생성자를 통해 초기화되어야 한다. 그래서 생성자에서 혹시라도 값이 설정되지 않는 오류를 컴파일 시점에 막아준다.
-
수정자 주입을 포함한 모든 방식은 모두 생성자 이후에 호출 되므로 필드에 final을 사용할 수 없다.
-
2) 롬복 Lombok
- 생성자 주입을 사용하면 좋지만, 클래스에 생성자 코드를 하나하나 쓰는 것이 귀찮다.
→ 롬복은 개발자가 클래스 내에 생성자 코드를 직접 쓸 필요 없도록 어노테이션을 통해 생성자 코드를 만들어주는 라이브러리이다! - 롬복에서 제공하는 어노테이션
- @Getter, @Setter : Getter, Setter 메서드 자동 생성
- @ToString: ToString 메서드 자동 생성
- @NoArgsConstructor: 기본 생성자 자동 생성
- @AllArgsConstructor: 모든 필드를 포함하는 생성자 자동 생성
- @RequiredArgsConstructor: final이 붙은 필드를 모아서 생성자를 자동으로 만들어준다.
- 롬복 사용 예시
//롬복 사용 전
//직접 생성자 코드 작성, 생성자 1개니까 @Autowired 생략
@Component
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
public OrderServiceImpl(MemberRepository memberRepository,
DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
}
//롬복 사용 후
@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
}
3. 옵션 처리
- 주입할 스프링 빈이 없어도 동작해야 할 때가 있다.
그런데 @Autowired 만 사용하면 required 옵션 기본값이 true로 되어 있어서 자동 주입 대상(빈)이 없으면 오류가 발생한다. - 자동 주입 대상을 옵션으로 처리하는 방법 3가지
// Member는 스프링 빈이 아니기 때문에 오류 발생
//1. required = false: 아예 메서드가 호출 안됨
@Autowired(required = false)
public void setNoBean1(Member member) {
System.out.println("setNoBean1 = " + member);
}
//2. @Nullable: null 호출
@Autowired
public void setNoBean2(@Nullable Member member) {
System.out.println("setNoBean2 = " + member);
}
//3. Optional<Member>: Optional.empty 호출
@Autowired(required = false)
public void setNoBean3(Optional<Member> member) {
System.out.println("setNoBean3 = " + member);
}
4. 의존관계 자동 주입 시 발생할 수 있는 문제 상황과 해결 방법
1) 문제: 조회할 빈이 2개 이상인 경우
@Component
public class FixDiscountPolicy implements DiscountPolicy {}
@Component
public class RateDiscountPolicy implements DiscountPolicy {}
@Autowired
private DiscountPolicy discountPolicy
- @Autowired는 타입으로 빈을 조회한다. 비슷한 예시: ac.getBean(DiscountPolicy.class)
- 이때, DiscountPolicy 타입의 클래스가 2개 이상 스프링 빈으로 등록되어 있으면 NoUniqueBeanDefinitionException이 발생한다.
2) 해결방법 3가지: @Autowired 필드 명, @Qualifier, @Primary
//해결1: 필드 명을 빈 이름으로 변경
@Autowired
private DiscountPolicy rateDiscountPolicy
//해결2: @Qualifier 사용
@Component
@Qualifier("mainDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy {}
@Autowired
public OrderServiceImpl(MemberRepository memberRepository,
@Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
//해결3: @Primary
@Component
@Primary //이게 우선!
public class RateDiscountPolicy implements DiscountPolicy {}
@Component
public class FixDiscountPolicy implements DiscountPolicy {}
- 해결1: @Autowired 필드 명 매칭
@Autowired 매칭 순서: 타입매칭 -> 타입매칭의 결과가 2개 이상일 때 필드명, 파라미터명으로 빈 이름 매칭
- 해결2: @Qualifier
@Qualifier로 추가 구분자를 붙여준다. 단, 빈 이름을 변경하는 것은 아니다.
이때 애노테이션을 직접 만들어서 컴파일시 타입 체크가 되도록 할 수도 있다. - 해결3: @Primary 사용
@Primary는 우선순위를 정하는 방법이다. @Autowired 시에 여러 빈이 매칭되면 @Primary가 우선권을 가진다. - OCP 원칙 위반?
Q: discountPolicy에 두 개의 빈이 찾아져버리므로,
특정 빈을 찾을 수 있도록 인자의 파라미터 이름을 수정해야했습니다. (@Autowired 필드명 방식)
또한 @Quilifier 혹은 @Primary 어노테이션을 붙이기 위해 구현체의 클래스를 찾아가서 수정해줘야 하는 것 같습니다.
이것이 개방-폐쇄 원칙을 못지킨 것이 아닌가 하는 의문이 들었습니다.
-> A: 네 맞습니다. 클라이언트 코드를 고쳐야 하기 때문에 OCP를 지키기 못했습니다.
기존 구현 클래스의 애노테이션도 변경하지 않으면 더 좋겠지만, 이 부분까지는 컴포넌트 스캔의 한계입니다.
@Bean을 사용하면 확실하게 되지만 약간은 불편하지요. 따라서 둘의 트레이드 오프로 이해하시면 됩니다.
3) 조회한 빈이 실제로 모두 필요한 경우: List, Map으로 가져오기
@Component
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;
}
}
- DiscountService는 Map과 List로 모든 DiscountPolicy타입의 클래스들을 주입받는다. 이때 fixDiscountPolicy, rateDiscountPolicy가 주입된다.
- Map<String, DiscountPolicy> : map의 키에 스프링 빈의 이름을 넣어주고, 그 값으로 DiscountPolicy 타입으로 조회한 모든 스프링 빈을 담아준다.
- List<DiscountPolicy> : DiscountPolicy 타입으로 조회한 모든 스프링 빈을 담아준다.
- 만약 해당하는 타입의 스프링 빈이 없으면, 빈 컬렉션이나 Map을 주입한다.
5. 의존관계 주입시 자동, 수동 결정 기준
- 편리한 자동 기능을 기본으로 사용하자
- 직접 등록하는 기술 지원 객체는 수동 등록
- 다형성을 적극 활용하는 비즈니스 로직은 수동 등록을 고민해보자