컴포넌트 스캔과 의존관계 자동주입
드디어 의존관계 자동주입을 공부했다
처음 입문했을때, 스프링을 들어봤지만 제대로 공부하지 않았을때는 이 개념이 이해되지 않았다
스프링이 편하다고는 하지만, 개념적으로 어려워 그냥 '그렇구나~' 하고 넘어갔었다
그때 제대로 공부하고, 이해했다면...적용하면서 업무를 진행했으면 달라졌을까 싶다
지금이라도 이렇게 공부하여 정리할 수 있어서 다행이다.
이번 파트는 기존 시간에 배웠던 @Configuration, @Bean을 활용한 방법(스프링 컨테이너에 직접 등록)에서
발생하는 문제점(불편한점)을 제시하고, 그에 따른 해결책으로 '컴포넌트 스캔'과 '의존관계 자동주입'을 설명한다.
Java 코드에 @Bean 애노테이션을 활용하여 직접 스프링 빈을 등록 할 수 있다.
그렇지만, 직접 등록할 빈이 많아지면 반복되는 과정에서 누락되는 문제가 발생한다(반복 자체도 지루함)
스프링은 설정정보가 없어도 자동으로 스프링 빈을 등록할 수 있는 기능이 있는데 그것이 바로
컴포넌트 스캔을 활용한 의존관계 자동주입이다.
컴포넌트 스캔
@Component가 붙은 모든 클래스를 스프링 빈으로 등록한다
@ComponentScan이라는 애노테이션을 설정 정보 클래스에 붙여주면 된다.
기존에 AppConfig는 해당 클래스에 @Configuration을 붙이고,
그 안의 메소드마다 @Bean을 붙여 스프링 빈으로 관리되게끔 설정할 수 있었다
하지만, 컴포넌트 스캔은 @Component 애노테이션이 붙은 모든 클래스를 스캔해서 스프링 빈으로 등록한다
각 클래스가 스캔의 대상이 되도록 애노테이션을 붙어주어야 한다.
스프링 빈 직접등록 설정
@Configuration
public class AppConfig {
@Bean
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
@Bean
public OrderService orderService() {
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
@Bean
public static MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
@Bean
public static DiscountPolicy discountPolicy() {
//return new FixDiscountPolicy();
return new RateDiscountPolicy();
}
}
- AppConfig 클래스에 @Configuration 애노테이션을 추가(설정정보로 인식)
- AppConfig 안에 각 서비스를 생성해주는 메소드를 작성하고, @Bean을 붙여서 스프링 컨테이너가 관리하는 객체로 인식
- 비즈니스 로직 작성 시, 스프링 컨테이너에서 사용하고자 하는 서비스를 꺼내서 활용
스프링 빈 자동등록 설정
@Configuration
@ComponentScan
public class AutoAppConfig {
}
@Component
public class MemoryMemberRepository implements MemberRepository {
-- 내용 생략 --
}
@Component
public class MemberServiceImpl implements MemberService {
private final MemberRepository memberRepository;
@Autowired
public MemberServiceImpl(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
-- 내용 생략 --
}
- AutoAppConfig 클래스에 @ComponentScan 애노테이션을 붙여준다(설정정보)
- 각 서비스에 @Component 애노테이션을 붙여준다
- @Bean으로 등록한 메소드가 없기 때문에, 각 서비스안에서 호출이 되도록 @Autowired 애노테이션을 붙여 의존관계 주입
두 단계 모두 목적은 스프링 컨테이너에 스프링 빈을 담아 의존관계를 만들어 주는 것 이다
얼핏보면 큰 차이가 없어보이지만, 첫번째는 스프링 빈에 등록하기 위해 모든 메소드를 직접 작성해야한다
두번째 방법은 새로운 서비스를 만들때, @Component만 붙여주고 의존관계를 설정할 부분에 @Autowired만 넣어주면 된다
따라서 편하게 의존관계를 주입 후 개발할 수 있다
기본 스캔 대상
@Controller, @Service, @Repository 등의 애노테이션은 그 안에 @Component를 포함하고 있다
따라서, MVC 패턴에서는 해당 계층별로 애노테이션을 넣어주면 기능도 명확하게 분리하면서 의존관계 주입이 가능하다
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Service {
@AliasFor(
annotation = Component.class
)
String value() default "";
}
탐색위치 / 필터
@ComponentScan에 basePackages 옵션을 주어서 꼭 필요한 위치부터 탐색할 수 있도록 설정할 수 있다.
이를 설정하지 않으면, 컴포넌트 스캔 애노테이션이 붙어있는 설정 정보 클래스의 패키지가 시작 위치가 된다.
설정 정보 클래스의 위치를 프로젝트 최상단에 두어서 모든 파일이 대상이 되도록 설정하는 것을 권장한다
스프링부트에서는 @SpringBootApplication에 @ComponentScan이 기본적으로 셋팅되어 있다
includeFilters / excludeFilters 옵션으로 컴포넌트 스캔 대상에서 제외할 대상을 지정하거나 스캔 대상을 추가할 수 있다.
하지만 추가는 @Component면 충분하고 제외도 많이 사용되지 않기 때문에 최대한 스프링의 기본 설정에 맞추어 사용하는 것이 권장된다.
의존관계 자동주입
컴포넌트 스캔과 같이 사용하는 의존관계 자동주입은 스프링 빈 등록의 편리함을 완성시켜준다.
의존관계 주입은 4가지 방법이 있다
- 생성자 주입
- 수정자 주입(Setter 주입)
- 필드 주입
- 일반 메서드 주입
생성자주입
생성자를 통해서 의존관계를 주입받는 방법이다.
이 방법은 생성자 호출 시점에 딱 1번만 호출되는 것이 보장된다.
그렇기 때문에 불변, 필수 의존관계에 사용한다.
또한 생성자가 딱 1개만 있으면 @Autowired를 생략해도 자동 주입된다.
수정자주입( Setter 주입 )
setter라 불리는 필드의 값을 변경하는 수정자 메소드를 통해서 의존관계를 주입받는 방법이다.
선택, 변경의 가능성이 있는 의존관계에 사용한다
필드주입
필드에 바로 주입하여 의존관계를 주입받는 방법이다.
코드가 간결하지만, 외부에서 변경이 불가능하여 테스트 하기 힘들다는 치명적인 단점이 있다.
DI 프레임워크가 없으면 쓸 수 없어서 되도록 사용하지 않는것이 좋다.
일반 메서드주입
일반적인 메소드(init과 같은 이름으로)를 통해서 의존관계를 주입받는 방법이다.
한번에 여러 필드를 주입 받을 수 있지만, 일반적으로 메소드를 통한 주입 방법을 잘 사용하지 않는다.
생성자주입을 사용하자!
사실, 이 부분은 항상 이해가 되지 않았던 부분이다.
전 회사에서 필드주입을 왕창 넣어놨는데, 강의를 들으면서 보니 필드주입은 사용하지 말라고 한 것 이다.
심지어, 인텔리제이에서는 필드주입을 추천하지 않는다는 경고 표시까지 되어있다.
( 전 회사에서는 이클립스를 사용하였기 때문에 전혀 알지 못했다. )
생성자 주입을 사용해야 하는 이유중 하나는 불변이다.
대부분의 의존관계 주입은 한번 일어나면, 애플리케이션 종료시점까지 변경할 일이 없다.
변경하면 안되는 메소드를 열어두는 것은 좋은 설계 방법이 아니기 때문에,
객체 생성시 딱 1번 호출되는 생성자 주입을 통한 의존관계 주입이 추천된다.
다른 이유는 누락이다.
프레임워크 없이 순수한 자바 코드를 통한 테스트 진행 시 다른 주입방법을 선택하면
예를들어 OrderServiceImpl에 대한 테스트를 진행한다고 가정하면, NPE이 발생한다.
( 해당 서비스는 MemberRepository, discountPolicy를 필요로 함 )
이는 해당 서비스가 필요로하는 Repository들이 생성되어 있지 않기 때문인데,
테스트 코드를 작성할때, 그것을 바로 파악하기 어렵다.
오류가 발생하면, 그 후에 내용을 파악해야하고, 그 다음 해당 Repository들에 대한 생성을 따로 해주어야한다.
생성자 주입을 선택한다면, 의존관계 주입이 아니더라도 생성자에서 해당 서비스에 필요로 하는 Repository들을 요구하기 때문에, 순수 자바 코드를 통한 테스트에서도 바로 해당 Repository들을 생성해주면 되기 때문에 문제가 발생하지 않는다.
그리고 혹 입력하지 않고 실행한다면 컴파일 오류가 발생한다.
( IDE에서 어떤 값을 넣어야하는지 바로 알 수 있고, 컴파일 시점에 오류이기 때문에 바로 잡을 수 있다. )
생성자 주입 방식은 프레임워크에 의존하지 않고, 순수한 자바의 특징을 잘 살리는 방법이라 이 방법을 추천한다.
롬복과 최신트렌드
대부분 서비스에 final을 붙여 불변으로 만들어주고, 생성자 주입을 통해 의존관계를 설정한다.
매번 이렇게 설정하기 번거로운 면이 있어서, 롬복에서는 @RequiredArgsConstructor 애노테이션을 제공한다.
이 애노테이션이 붙은 서비스는 final이 붙어있는 필드를 모아서 생성자를 자동으로 만들어 준다.
그리고, 딱 1개의 생성자만 있을때는 @Autowired를 생략할 수 있어서 간편한 사용이 가능하다.
Service에 @Component, @RequiredArgsConstructor 만 입력해 주면 의존관계 주입 끝!
조회 대상 빈이 2개 이상일때
- @Autowired 필드 명 매칭 : 필드 명을 빈 이름으로 변경한다
- @Qualifier : 추가 구분자 설정( @Qualifier("mainDiscountPolicy") )
- @Primary : 해당 애노테이션이 붙으면 우선순위를 가진다
@Qualifier와 @Primary 가 동시에 사용되면 @Qualifier 가 우선순위를 가진다
( 스프링은 자세한걸 우선순위로 )
조회한 빈이 모두 필요할 때 - List, Map
의도적으로 모든 타입이 다 필요할때 - 고객이 할인의 종류를 선택하다면? ( rate, fix )
Map<String, DiscountPolicy>: map의 키에 스프링 빈의 이름을, 값에는 DiscountPolicy 타입으로 조회한 모든 스프링빈을 담아준다
List<DiscountPolicy>: DiscounyPolicy 타입으로 조회한 모든 스프링 빈을 담아준다
'Spring > 핵심원리' 카테고리의 다른 글
[스프링 핵심원리 - 기본편] 내용 정리7 - 빈 스코프 (0) | 2023.09.04 |
---|---|
[스프링 핵심원리 - 기본편] 내용 정리6 - 빈 생명주기 (0) | 2023.08.30 |
[스프링 핵심원리 - 기본편] 내용 정리4 - 싱글톤 (0) | 2023.08.27 |
[스프링 핵심원리 - 기본편] 내용 정리3 - 스프링 컨테이너 (0) | 2023.08.22 |
[스프링 핵심원리 - 기본편] 내용 정리2 - AppConfig(관심사의 분리) (0) | 2022.02.21 |