인프런 김영한 님의 <스프링 핵심 원리 - 기본편> 강의 내용을 정리한 것입니다.
의존관계 주입은 크게 4가지 방법이 있다.
생성자를 통해 의존관계를 주입 받는 방법.
생성자 호출 시점에 딱 1번만 호출되는 것이 보장된다.(불변, 필수 의존관계에 사용)
생성자가 단 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;
}
...
}
setter(수정자) 메서드를 통해서 의존관계를 주입하는 방법.
선택적, 변경 가능성 있는 의존관계에 사용.
자바빈 프로퍼티 규약의 수정자 메서드 방식 사용.
자바빈 프로퍼티 규약
자바에서 필드의 값을 직접 변경하지 않고, getXxx(getter), setXxx(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
의 기본 동작은 주입할 대상이 없으면 오류 발생. 주입 대상 없어도 동작하게 하려면@Autowired(required = false)
로 지정
필드에 바로 주입하는 방법.
사용하지 말자!
코드가 간결하지만, 외부에서 변경이 불가능해서 테스트하기 힘들다.
DI 프레임워크가 없으면 아무것도 할 수 없다.
- 사용해도 되는 곳 : 테스트 코드나 스프링 설정을 목적으로 하는
@Configuration
같은 곳에서만
@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;
}
...
}
주입할 스프링 빈이 없어도 동작해야 할 때가 있다. 그러나 @Autowired
만 사용하면 required
옵션의 기본값이 true
로 되어 있어 자동 주입 대상이 없으면 오류가 발생한다.
자동 주입 대상을 옵션으로 처리하는 방법
@Autowired(required = false)
: 자동 주입 대상이 없으면 수정자 메서드 자체가 호출되지 않음.org.springframework.lang.@Nullable
: 자동 주입 대상이 없으면 null 입력됨.Optional<>
: 자동 주입 대상이 없으면Optional.empty
가 입력됨.
// 자동 주입 대상 없으면
@Autowired(required = false) // (1) 수정자 메서드 자체가 호출되지 않음
public void setNoBean1(Member noBean1) {
System.out.println("setNoBean1 = " + noBean1);
}
@Autowired
public void setNoBean2(@Nullable Member noBean2) { // (2) null 입력됨
System.out.println("setNoBean2 = " + noBean2);
}
@Autowired
public void setNoBean3(Optional<Member> noBean3) { // (3) Optional.empty 입력됨
System.out.println("setNoBean3 = " + noBean3);
}
생성자 주입 방식은 프레임워크에 의존하지 않고, 순수한 자바 언어의 특징을 잘 살리는 방법이다.
컴파일 오류가 세상에서 가장 빠르고 좋은 오류다!
생성자 주입의 단점은 코드가 길어지고, 필드가 추가될 때 생성자도 수정해주어야 한다는 점이다.
롬복(lombok) 이라는 라이브러리를 사용하면, 정해진 어노테이션을 붙이는 것만으로도 getter, setter, 생성자 등을 자동으로 생성해준다.
생성자 주입을 사용하기 위해 @RequiredArgsConstructor
어노테이션을 붙이면 private final
이 붙은 필드를 모아 생성자를 자동으로 만들어준다.
@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
...
}
@Autowired
는 타입으로 조회하며, ac.getBean(~~~.class)
와 유사하게 동작한다.
그러나 타입으로 조회하면 선택된 빈이 2개 이상일 때 문제가 발생한다.
NoUniqueBeanDefinitionException
이 발생한다.
하위 타입으로 지정할 수 있지만, 이는 DIP를 위반하고 유연성이 떨어진다.(역할에 의존해야지, 구체적인 것에 의존하면 안됨.)
이를 해결하는 방법은 다음과 같다:
@Qualifier
는 추가 구분자를 붙여주는 방법이다. 주입시 추가적인 방법을 제공하는 것이지, 빈 이름을 변경하지는 않는다.
NoSuchBeanDefinitionException
발생단점 :
@Qualifier
를 붙여야 한다.@Qualifier("mainDiscountPolicy")
이렇게 문자를 적으면 컴파일시 타입 체크가 되지 않는다.@Component
@Qualifier("mainDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy {
...
}
@Component
@Qualifier("fixDiscountPolicy")
public class FixDiscountPolicy implements DiscountPolicy {
...
}
// 생성자나 수정자에서 아래와 같이 주입할 @Qualifier를 입력
public OrderServiceImpl(MemberRepository memberRepository, @Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
우선순위를 정하는 방법. @Autowired
시에 여러 빈이 매칭되면 @Primary
가 우선권을 갖는다.
@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy {
...
}
@Component
public class FixDiscountPolicy implements DiscountPolicy {
...
}
의도적으로 해당 타입의 스프링 빈이 2개 이상 필요한 경우도 있다. 스프링에서는 소위 말하는 전략패턴을 매우 간단히 구현할 수 있다.
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;
}
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을 주입한다.DiscountService
는 Map<String, DiscountPolicy>
으로 모든 DiscountPolicy
를 주입받는다.
discount()
메서드는 discountCode
로 fixDiscountPolicy
가 넘어오면 Map에서 해당 키를 가진 스프링 빈을 찾아서 실행한다.
편리한 자동 기능을 기본으로 사용
수동 빈 등록을 사용하면 좋은 경우
(1) 직접 등록하는 기술 지원 객체
기술 지원 로직에 사용하여 명확하게 드러내는 것이 유지보수에 좋다. 이는 애플리케이션에 광범위하게 영향을 미치며, 적용이 잘 되고 있는지 아닌지조차 파악하기 어려운 경우가 많기 때문.
애플리케이션은 크게 업무 로직과 기술 지원 로직으로 니뉜다.
- 업무 로직 빈 : 컨트롤러, 서비스, 리포지토리 등.
- 기술 지원 빈 : 기술적인 문제나 공통 관심사를 처리할 때 주로 사용. DB 연결과 같이 업무 로직을 지원하기 위한 하부 기술이나 공통 기술.
(2) 다형성을 적극 활용하는 비즈니스 로직
협업할 경우, 어떤 빈이 주입될지 여러 코드를 열어봐야 하는 등 어려움이 있기 때문에 수동 빈으로 등록하거나, 자동으로 등록하면 특정 패키지에 같이 묶어두는 것이 좋다.