티스토리 뷰
목차
1. 컴포넌트 스캔과 의존관계 주입
2. 컴포넌트 스캔
3. 의존관계 자동 주입
1. 컴포넌트 스캔과 의존관계 주입
[Spring] 1. 스프링 컨테이너 예제에서는 AppConfig와 같이 설정 정보 클래스에서 빈과 의존관계를 설정해 주었다.
하지만 프로젝트 규모가 커지고 등록해야 할 스프링 빈이 많아지면서 수동으로 관리하기가 점점 복잡해진다.
그래서 스프링은 별도의 설정 정보 없이 자동으로 스프링 빈을 등록하는 컴포넌트 스캔이라는 기능을 제공한다.
@Configuration
@ComponentScan
public class AutoAppConfig {
}
@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;
}
}
// 로그
ClassPathBeanDefinitionScanner - Identified candidate component class:
.. RateDiscountPolicy.class
.. MemberServiceImpl.class
.. MemoryMemberRepository.class
.. OrderServiceImpl.class
@Bean으로 등록할 필요 없이 설정 정보 클래스에 @ComponentScan을 붙여주면 @Component가 붙은 클래스의 객체를 스프링 빈으로 등록한다.
@Bean으로 설정 정보를 직접 만들어 사용할 때와 달리 의존관계 주입을 할 수 없기 때문에
@Autowired를 붙여서 의존관계를 자동으로 주입하도록 설정한다.
스프링 컨테이너에서 타입이 맞는 빈을 찾아 주입한다.
동작 과정
@ComponentScan은 @Component가 붙은 모든 클래스를 스프링 빈으로 등록한다.
이때 스프링 빈의 기본 이름은 클래스명을 사용하되 맨 앞글자만 소문자를 사용한다.
- 이름 기본 전략: MemberServiceImpl --> memberServiceImpl
- 빈 이름 직접 지정: @Component("memberService2")
생성자에 @Autowired를 지정하면 스프링 컨테이너가 자동으로 해당 스프링 빈을 찾아서 주입한다.
이때 기본 조회 전략은 타입이 같은 빈을 찾아서 주입한다.
2. 컴포넌트 스캔
1) 탐색 위치
모든 자바 클래스를 다 컴포넌트 스캔하면 시간이 오래 걸리기 때문에
꼭 필요한 위치부터 탐색을 시작하도록 설정할 수 있다.
@ComponentScan(
basePackages = "hello.core",
}
- 기본 설정은 @ComponentScan이 붙은 설정 정보 클래스의 패키지가 시작 위치
- basePackage: 탐색할 패키지의 시작 위치를 지정
- basePackageClasses: 지정한 클래스의 패키지를 탐색 시작 위치로 지정
되도록 설정 정보 클래스의 위치를 최상단에 두고 별도의 패키지 위치를 지정하지 않도록 권장.
@SpringBootApplication
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
}
Spring Boot에서는 @SpringBootApplication에 @ComponentScan이 들어있다.
따라서 @SpringBootApplication이 붙은 클래스의 패키지가 컴포넌트 스캔의 시작 위치가 된다.
2) 컴포넌트 스캔 기본 대상
컴포넌트 스캔은 @Component 뿐만 아니라 다음과 내용도 추가로 대상에 포함한다.
- @Component : 컴포넌트 스캔에서 사용
- @Controlller : 스프링 MVC 컨트롤러에서 사용
- @Service : 스프링 비즈니스 로직에서 사용
- @Repository : 스프링 데이터 접근 계층에서 사용
- @Configuration : 스프링 설정 정보에서 사용
@Component
public @interface Controller {
}
@Controller, @Configuration과 같은 어노테이션은 @Component를 포함하고 있기 때문에 컴포넌트 스캔의 대상이 되는데 어노테이션이 특정 어노테이션을 들고 있는 것을 인식할 수 있는 것은
자바 언어가 아닌 스프링이 지원하는 기술이다.
부가 기능
컴포넌트 스캔의 용도 뿐만 아니라 다음 애노테이션이 있으면 스프링은 부가 기능을 수행한다.
- @Controller : 스프링 MVC 컨트롤러로 인식
- @Repository : 스프링 데이터 접근 계층으로 인식하고, 데이터 계층의 예외를 스프링 예외로 변환해준다.
- @Configuration : 앞서 보았듯이 스프링 설정 정보로 인식하고, 스프링 빈이 싱글톤을 유지하도록 추가
처리를 한다. - @Service : 사실 @Service 는 특별한 처리를 하지 않는다. 대신 개발자들이 핵심 비즈니스 로직이 여기에
있겠구나 라고 비즈니스 계층을 인식하는데 도움이 된다.
프로젝트의 DB를 바꾸면 예외의 내용이 바뀌는데 @Repository가 Spring 예외로 변환해 추상화 해주기 때문에 별도의 문제가 발생하지 않는다.
3) 필터
컴포넌트 스캔 대상을 추가하거나 제외하도록 설정할 수 있다.
@Configuration
@ComponentScan(
includeFilters = @Filter(type = FilterType.ANNOTATION, classes =
MyIncludeComponent.class),
excludeFilters = @Filter(type = FilterType.ANNOTATION, classes =
MyExcludeComponent.class)
)
- includeFilters : 컴포넌트 스캔 대상을 추가로 지정한다.
- excludeFilters : 컴포넌트 스캔에서 제외할 대상을 지정한다.
FilterType 옵션
- 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
스프링 부트는 컴포넌트 스캔을 기본으로 제공하는데,
옵션을 변경하면서 사용하기 보다는 스프링의 기본 설정에 최대한 맞추어 사용하는 것을 권장한다.
3) 중복 등록과 충돌
@Component을 이용한 자동 빈 등록과 @Bean을 이용한 수동 빈 등록 과정에서
같은 빈 이름을 등록하도록 요청하면 컴포넌트 스캔에서 다음과 같은 처리가 이루어 진다.
자동 vs 자동
ConflictingBeanDefinitionException 예외 발생
수동 vs 자동
@Component
public class MemoryMemberRepository implements MemberRepository {}
@Configuration
@ComponentScan(
excludeFilters = @Filter(type = FilterType.ANNOTATION, classes =
Configuration.class)
)
public class AutoAppConfig {
@Bean(name = "memoryMemberRepository")
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
}
// 로그
Overriding bean definition for bean 'memoryMemberRepository' with a different
definition: replacing
이 경우 수동 빈 등록이 우선권을 가진다.
(수동 빈이 자동 빈을 오버라이딩 해버린다.)
동일한 이름의 빈을 오버라이딩하는 경우는 애매한 버그를 만들어 파악하기 어려운 상황을 만들 수 있다.
최근 스프링 부트에서는 수동 빈 등록과 자동 빈 등록이 충돌하면 오류가 발생하도록 기본 값을 바꾸었다.
스프링 부트인 CoreApplication을 실행하면 다음과 같은 에러가 발생한다.
Consider renaming one of the beans or enabling overriding by setting
spring.main.allow-bean-definition-overriding=true
3. 의존관계 자동 주입
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를 생략할 수 있다.
생성자 주입을 선택하자.
대부분의 의존관계는 애플리케이션 종료 전까지 변하면 안되지만(불변)
생성자 주입 이외의 경우에는 외부에서 의존관계를 변경할 수 있다.
또한 의존관계 주입이 누락된 경우 생성자 주입 방식에서는 런타임 시점에서 NPE가 발생하지만
생성자 주입 방식에서는 컴파일 단계에서 확인할 수 있다.
final 키워드를 함께 사용하면 더 안전하게 의존관계 주입을 설정할 수 있다.
private final MemberRepository memberRepository;
항상 생성자 주입을 선택하고 일부 필요한 경우 수정자 주입을 사용하자.
수정자 주입
@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;
}
}
필드의 값을 변경하는 setter 메서드를 통해서 의존관계를 주입하는 방법이다.
선택, 변경 가능성이 있는 의존관계에 사용한다.
필드 주입
@Component
public class OrderServiceImpl implements OrderService {
@Autowired
private MemberRepository memberRepository;
@Autowired
private DiscountPolicy discountPolicy;
}
DI 프레임워크가 없으면 외부에서 협력 객체 변경이 불가능해서 테스트하기 힘들다.
@SpringBootTest 등으로 스프링 빈을 사용해서 테스트 할 수 있지만
상황에 따라 필요한 객체 혹은 가짜 객체(Mock, Stub, Fake 등)를 만들어서 주입하는 방식으로 테스트 코드를 만들지 못한다.
애플리케이션의 실제 코드와 관계없는 테스트 코드, 스프링 설정을 목적으로 하는 @Configuration 같은 곳에서만 특별한 용도로 사용한다.
일반 메서드 주입
@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) 옵션 처리
주입할 스프링 빈이 없어도 동작해야 할 때가 있는데 이런 경우 자동 주입 대상을 옵션으로 처리할 수 있다.
//호출 안됨
@Autowired(required = false)
public void setNoBean1(Member member) {
System.out.println("setNoBean1 = " + member);
}
//null 호출
@Autowired
public void setNoBean2(@Nullable Member member) {
System.out.println("setNoBean2 = " + member);
}
//Optional.empty 호출
@Autowired(required = false)
public void setNoBean3(Optional<Member> member) {
System.out.println("setNoBean3 = " + member);
}
- @Autowired(required=false) : 자동 주입할 대상이 없으면 수정자 메서드 자체가 호출 안됨
- org.springframework.lang.@Nullable : 자동 주입할 대상이 없으면 null이 입력된다.
- Optional<> : 자동 주입할 대상이 없으면 Optional.empty 가 입력된다.
@Nullable, Optional은 스프링 전반에 걸쳐서 지원된다.
생성자 주입, 수정자 주입에서 특정 필드에만 적용해도 된다.
3) 롬복
롬복 라이브러리 설치, IntelliJ Lombok 플러그인 설치, Enable annotation processing 설정.
생성자 주입 방식에서 코드를 간결하게 수정할 수 있다.
@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
}
@RequiredArgsConstructor 는 final 필드에 대해 생성자를 만들어 준다.
4) 조회 대상 빈이 2개인 경우
조회 대상 빈이 2개인 경우 NoUniqueBeanDefinitionException이 발생한다.
@Component
public class FixDiscountPolicy implements DiscountPolicy {}
@Component
public class RateDiscountPolicy implements DiscountPolicy {}
@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;
}
}
NoUniqueBeanDefinitionException: No qualifying bean of type
'hello.core.discount.DiscountPolicy' available: expected single matching bean
but found 2: fixDiscountPolicy,rateDiscountPolicy
@Autowired 필드 명
스프링은 타입 매칭을 먼저 시도하고 여러개의 빈이 조회된 경우 필드명, 파라미터 명으로 빈이름과 매칭한다.
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy rateDiscountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = rateDiscountPolicy;
}
- 타입 매칭
- 타입 매칭의 결과가 2개 이상일 때 필드 명, 파라미터 명으로 빈 이름 매칭
@Qualifier
빈 등록과 주입시에 @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끼리 매칭
- 빈 이름 매칭 (mainDiscountPolicy)
- NoSuchBeanDefinitionException 예외 발생
@Qualifier("mainDiscountPolicy") 에서 mainDiscountPolicy는 문자이기 때문에 컴파일 과정에서
의존관계 주입이 정상적으로 동작할 지 확인할 수 있도록 어노테이션을 별도로 만들어서 사용할 수 있다.
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER,
ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Qualifier("mainDiscountPolicy")
public @interface MainDiscountPolicy {
}
@Autowired
public OrderServiceImpl(MemberRepository memberRepository,
@MainDiscountPolicy DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
@Primary
@Autowired 시에 여러 빈이 매칭되면 @Primary 가 우선권을 가진다.
@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy {}
@Component
public class FixDiscountPolicy implements DiscountPolicy {}
@Primary, @Qualifier 활용
우선 순위 : @Primary < @Qualifier
코드에서 자주 사용하는 메인 데이터베이스의 커넥션을 획득하는 스프링 빈이 있고,
코드에서 특별한 기능으로 가끔 사용하는 서브 데이터베이스의 커넥션을 획득하는 스프링 빈이 있다고 생각해보자.
메인 데이터베이스의 커넥션을 획득하는 스프링 빈은 @Primary 를 적용해서 조회하는 곳에서 @Qualifier
지정 없이 편리하게 조회하고, 서브 데이터베이스 커넥션 빈을 획득할 때는 @Qualifier 를 지정해서
명시적으로 획득 하는 방식으로 사용하면 코드를 깔끔하게 유지할 수 있다.
물론 이때 메인 데이터베이스의 스프링 빈을 등록할 때 @Qualifier 를 지정해주는 것은 상관없다.
5) 조회한 빈이 모두 필요할 때
의도적으로 해당 타입의 스프링 빈이 모두 필요한 경우,
Map과 List로 주입받아 간단하게 전략 패턴을 구현할 수 있다.
class DiscountService {
private final Map<String, DiscountPolicy> policyMap;
private final List<DiscountPolicy> policies;
public DiscountService(Map<String, DiscountPolicy> policyMap,
List<DiscountPolicy> policies) {
this.policyMap = policyMap;
this.policies = policies;
}
public int discount(Member member, int price, String discountCode) {
DiscountPolicy discountPolicy = policyMap.get(discountCode);
return discountPolicy.discount(member, price);
}
}
- Map<String, DiscountPolicy> : map의 키에 스프링 빈의 이름을 넣어주고, 그 값으로
DiscountPolicy 타입으로 조회한 모든 스프링 빈을 담아준다. - List<DiscountPolicy> : DiscountPolicy 타입으로 조회한 모든 스프링 빈을 담아준다.
만약 해당하는 타입의 스프링 빈이 없으면, 빈 컬렉션이나 Map을 주입한다
int discountPrice = discountService.discount(member, 10000, "fixDiscountPolicy");
클라이언트가 할인 정책을 선택해서 할인 받도록 하는 경우
discountCode 파라미터로 빈 이름을 넘겨주면서 사용될 스프링 빈을 결정할 수 있다.
6) 자동, 수동 등록 기준
편리한 자동 기능을 기본으로 사용하자
스프링은@Component 뿐만 아니라 @Controller , @Service , @Repository 처럼 계층에 맞추어 일반적인
애플리케이션 로직을 자동으로 스캔할 수 있도록 지원한다. 거기에 더해서 최근 스프링 부트는 컴포넌트
스캔을 기본으로 사용하고, 스프링 부트의 다양한 스프링 빈들도 조건이 맞으면 자동으로 등록하도록
설계했다.
직접 등록하는 기술 지원 객체는 수동 등록
- 업무 로직 빈: 웹을 지원하는 컨트롤러, 핵심 비즈니스 로직이 있는 서비스, 데이터 계층의 로직을 처리하는
리포지토리등이 모두 업무 로직이다. 보통 비즈니스 요구사항을 개발할 때 추가되거나 변경된다. - 기술 지원 빈: 기술적인 문제나 공통 관심사(AOP)를 처리할 때 주로 사용된다. 데이터베이스 연결이나,
공통 로그 처리 처럼 업무 로직을 지원하기 위한 하부 기술이나 공통 기술들이다.
기술 지원 로직은 업무 로직과 비교해서 그 수가 매우 적고, 보통 애플리케이션 전반에 걸쳐서 광범위하게 영향을 미친다. 그리고 업무 로직은 문제가 발생했을 때 어디가 문제인지 명확하게 잘 들어나지만, 기술 지원 로직은 적용이 잘 되고 있는지 아닌지 조차 파악하기 어려운 경우가 많다. 그래서 이런 기술 지원 로직들은 가급적 수동 빈 등록을 사용해서 명확하게 들어내는 것이 좋다.
스프링과 스프링 부트가 자동으로 등록하는 수 많은 빈들은 예외다.
이런 부분들은 스프링 자체를 잘 이해하고 스프링의 의도대로 잘 사용하는게 중요하다. 스프링 부트의 경우 DataSource 같은 데이터베이스 연결에 사용하는 기술 지원 로직까지 내부에서 자동으로 등록하는데, 이런 부분은 메뉴얼을 잘 참고해서 스프링 부트가 의도한 대로 편리하게 사용하면 된다.
다형성을 적극 활용하는 비즈니스 로직은 수동 등록을 고민해보자
private final Map<String, DiscountPolicy> policyMap;
DiscountPolicy 로 어떤 빈들이 들어오게 될지 한눈에 확인이 되지 않는다.
이런 경우 수동 빈으로 등록하거나 또는 자동으로하면 특정 패키지에 같이 묶어두는게 좋다.
@Configuration
public class DiscountPolicyConfig {
@Bean
public DiscountPolicy rateDiscountPolicy() {
return new RateDiscountPolicy();
}
@Bean
public DiscountPolicy fixDiscountPolicy() {
return new FixDiscountPolicy();
}
}
출처
스프링 핵심 원리 - 기본편(김영한)
'Spring' 카테고리의 다른 글
[Spring] 예외 처리 (0) | 2022.10.21 |
---|---|
[Spring] 트랜잭션 (0) | 2022.10.19 |
[Spring] 3. 스프링 빈 (0) | 2022.04.13 |
[Spring] 1. 스프링 컨테이너 (0) | 2022.03.04 |
- Total
- Today
- Yesterday
- Spring Data JPA
- ATDD
- 육각형 아키텍처
- 트랜잭셔널 아웃박스 패턴
- 스프링 예외 추상화
- 계층형 아키텍처
- Ubiquitous Language
- 스프링 카프카 컨슈머
- JPA
- HTTP 헤더
- MySQL
- http
- Spring
- 도메인 모델링
- kafka
- TDD
- 마이크로서비스 패턴
- clean code
- Git
- java8
- 클린코드
- 이벤트 스토밍
- spring rest docs
- Spring Boot
- H2
- named query
- 학습 테스트
- mockito
- 폴링 발행기 패턴
- Stream
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |