의존관계 자동 주입
- 생성자에 @Autowired 를 지정하면, 스프링 컨테이너가 자동으로 스프링 빈을 찾아서 주입한다.
- 생성자에 파라미터가 많아도 다 찾아서 자동으로 주입한다.
- 이때 기본 조회 전략은 타입이 같은 빈을 찾아서 주입한다.
- getBean(MemberRepository.class) 와 동일하다고 이해하면 된다.
중복 등록과 충돌
Case1 - 자동 빈 등록 vs 자동 빈 등록 → 오류 ( ConflictingBeanDefinitionException ) 예외 발생
Case2 - 수동 빈 등록 vs 자동 빈 등록 → 수동 빈 등록이 자동 빈 등록 오버라이딩 해버린다.
다양한 의존관계 주입 방법
4가지 방법이 있다.
- 생성자 주입
- 수정자 주입(setter 주입)
- 필드 주입
- 일반 메서드 주입
생성자 주입
- 특징
- 생성자 호출시점에 딱 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;
}
}
생성자가 하나일경우 @Autowired 생략해도 자동 주입 된다.
수정자 주입(setter 주입)
- setter라 불리는 필드의 값을 변경하는 수정자 메서드를 통해 주입하는 방법
- 특징
- 선택, 변경 가능성이 있는 의존관계에 사용
- 자바빈 프로퍼티 규약의 수정자 메서드 방식을 사용
@Component
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
@Autowired
public void setMemberRepository(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
}
@Autowired 의 기본 동작은 주입할 대상이 없으면 오류가 발생하지만 대상 없이도 동작하게 하려면 @Autowired(required = false) 로 지정하면 동작한다.
필드 주입
- 필드에 바로 주입하는 방법
- 특징
- 코드가 간결하지만 외부에서 변경이 불가능해서 테스트하기 어려운 단점이 있다.
- DI 프레임워크가 없으면 아무것도 할 수 없다.
- 사용 하지말자!
일반 메서드 주입
- 일반 메서드를 통해서 주입 받을 수 있다.
- 특징
- 한번에 여러필드를 주입 받을 수 있다.
- 일반적으로 잘 사용하지는 않는다.
참고: 당연한 이야기지만 의존관계 자동 주입은 스프링 컨테이너가 관리하는 스프링 빈이어야 동작한다. 스프링 빈이 아닌 일반 자바(Member 같은) 클래스에서 @Autowired 를 적용해도 동작하지 않는다.
옵션 처리
주입할 스프링 빈이 없어도 동작해야 할 때가 있다.
@Autowired 만 사용하면 required 옵션의 기본값이 true 로 되어 있어서 오류가 발생한다.
자동 주입 대상을 옵션으로 처리하는 방법은 다음과 같다.
- @Autowired(required = false) : 자동 주입할 대상이 없으면 수정자 메서드 자체가 호출x
- org.springframework.lang.@Nullable : 자동 주입할 대상이 없으면 null이 입력된다.
- Optional<> : 자동 주입할 대상이 없으면 Optional.empty 가 입력된다.
//호출 안됨
@Autowired(required = false)
public void setNoBean1(Member noBean1) {
System.out.println("setNoBean1 = " + noBean1);
}
//null 호출
@Autowired
public void setNoBean2(@Nullable Member noBean2) {
System.out.println("setNoBean2 = " + noBean2);
}
//Optional.empty 호출
@Autowired(required = false)
public void setNoBean3(Optional<Member> noBean3) {
System.out.println("setNoBean3 = " + noBean3);
}
/*
출력결과
setNoBean2 = null
setNoBean3 = Optional.empty
*/
- Member 는 스프링 빈이 아니다.
- setNoBean1() 은 @Autowired(required = false) 이므로 호출 자체가 안된다.
final 키워드
생성자 주입을 사용하면 필드에 final 키워드를 사용할 수 있다. 그래서 생성자에서 혹시라도 값이 설정되지 않는 오류를 컴파일 시점에서 막아준다.
@Component
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
}
//...
}
- 필수 필드인 discountPolicy 에 값이 설정이 안되어있고 누락되었다. 자바는 컴파일 시점에 다음 오류를 발생시킨다.
- java: variable discountPolicy might not have been initialized
생성자 주입을 제외한 나머지 주입 방식은 모두 생성자 이후에 호출되므로, 필드에 final 키워드를 사용할 수 없다. 오직 생성자 주입 방식만 사용가능
정리
- 기본으로 생성자 주입을 사용하고, 필수 값이 아닌 경우에는 수정자 주입 방식을 옵션으로 부여하면 된다. 생성자 주입과 수정자 주입을 동시에 사용 불가하다.
- 항상 생성자 주입을 선택하고 가끔 옵션이 필요하면 수정자 주입을 선택하자 필드 주입 사용x
조회한 빈이 모두 필요할 때, List, Map
의도적으로 해당 타입의 스프링 빈이 다 필요한 경우도 있다.
예를 들어 할인 서비스를 제공하는데, 클라이언트가 할인의 종류(rate, fix)를 선택할 수 있다고 가정해보자.
public class AllbeanTest {
@Test
void findAllBean() {
ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class, DiscountService.class);
DiscountService discountService = ac.getBean(DiscountService.class);
Member member = new Member(1L, "userA", Grade.VIP);
int discountPrice = discountService.discount(member, 10000, "fixDiscountPolicy");
assertThat(discountService).isInstanceOf(DiscountService.class);
assertThat(discountPrice).isEqualTo(1000);
}
static 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;
System.out.println("policyMap = " + policyMap);
System.out.println("policies = " + policies);
}
public int discount(Member member, int price, String discountCode) {
DiscountPolicy discountPolicy = policyMap.get(discountCode);
System.out.println("discountCode = " + discountCode);
System.out.println("discountPolicy = " + discountPolicy);
return discountPolicy.discount(member, price);
}
}
}
- 로직 분석
- DiscountService 는 Map으로 모든 DiscountPolicy 를 주입받는다. 이때 fixDiscountPolicy, rateDiscountPolicy가 주입된다.
- discount() 메서드는 discountCode로 “fixDiscountPolicy” 가 넘어오면 map에서 fixDiscountPolicy 스프링 빈을 찾아서 실행한다. 물론 rateDiscountPolicy → rateDiscountPolicy 동일하다.
- 주입 분석
- Map <String, DiscountPolicy> : map의 키에 스프링 빈의 이름을 넣어주고, 그 값으로 DiscountPolicy 타입으로 조회한 모든 스프링 빈을 담아준다.
- List <DiscountPolicy> : DiscountPolicy 타입으로 조회한 모든 스프링 빈을 담아준다.
- 만약 해당하는 타입의 스프링 빈이없으면, 빈 컬렉션이나 Map을 주입한다.
의존관계 자동, 수동 올바른 실무 운영 기준
결론부터 이야기하면, 스프링이 나오고 점점 자동을 선호하는 추세
스프링은 @Component 뿐만 아니라 @Controller , @Service , @Repository 처럼 계층에 맞추어 일반적인 애플리케이션 로직을 자동으로 스캔할 수 있도록 지원한다. 또한 최근 스프링 부트는 컴포넌트 스캔을 기본으로 사용하며 스프링 부트의 다양한 스프링 빈들도 조건이 맞으면 자동으로 등록하도록 설계했다.
개발자 입장에서 스프링 빈을 하나 등록할 때 @Component 만 넣어주면 끝나는 일을 @Configuration 설정 정보에 가서 @Bean 을 적고, 객체를 생성하고, 주입할 대상을 일일이 적어주는 과정은 너무 번거롭다. 그리고 관리할 빈이 많아서 설정 정보가 커지면 설정 정보를 관리하는 것 자체가 부담이 된다. 결정적으로 자동 빈 등록을 사용해도 OCP, DIP를 지킬 수 있다. → 사기
하지만 수동 빈 등록을 사용할 상황도 충분히 있음~
특히 스프링 부트가 아니라 내가 직접 기술 지원 객체를 스프링 빈으로 등록한다면 수동으로 등록해서 아래처럼 명확하게 드러내는 것이 좋다.
@Configuration
public class DiscountPolicyConfig {
@Bean
public DiscountPolicy rateDiscountPolicy() {
return new RateDiscountPolicy();
}
@Bean
public DiscountPolicy fixDiscountPolicy() {
return new FixDiscountPolicy();
}
}
'Develop > Spring' 카테고리의 다른 글
[Spring/기본편] 빈 생명주기 콜백 (0) | 2024.03.19 |
---|---|
[Spring/기본편] 롬복 (lombok) (0) | 2024.03.19 |
[Spring/기본편] 컴포넌트 스캔 (0) | 2024.03.19 |
[Spring/기본편] 싱글톤 컨테이너 (0) | 2024.03.19 |
[Spring/기본편] 스프링 빈 조회 (0) | 2024.03.19 |
댓글