<Spring> 스프링 핵심원리 이해 7 - 의존관계 자동 주입
다양한 의존관계 주입 방법
의존관계 주입에는 아래와 같이 크게 4가지 방법이 존재한다.아래 네가지 방법이 어떤식으로 의존관계를 주입하는지 알아보자.
- 생성자 주입
- 수정자 주입(setter 주입)
- 필드 주입
- 일반 메서드 주입
생성자 주입
- 이름 그대로 생성자를 통해서 의존관계를 주입받는 방법이다.
- 기존에 우리가 진행했던 DI가 생성자 주입 방식을 이용했다.
- 이는 생성자 호출 시점에 단 한번만 호출이 되기 때문에, 불변하고 필수적인 의존관계에 사용한다.
- 아래 코드처럼 생성자가 한개만 있는 경우는
@Autowired
를 생략해도 자동으로 스프링 빈을 통해 DI를 진행한다.
@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;
}
}
수정자 주입(setter 주입)
- setter라는 불리는 필드의 값을 변경하는 수정자 메서드를 이용하여, 의존관계를 주입하는 방식이다.
- 선택,변경의 가능성이 있는 의존관계에 사용한다.
- 기존의 코드에서 주입할 인스턴스들을 선언하는 부분에서 final 키워드를 없애주고 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;
}
}
생성자와 수정자가 모두 있는 경우의 의존관계 주입 순서는?
- 생성자의 경우 스프링 빈이 생성되는 동시에 의존관계 주입을 진행해 주기 때문에 수정자를 통한 의존관계 주입보다 먼저 해당 동작을 수행한다.
자바빈 프로퍼티 규약이란?
- 자바에서는 과거부터 필드의 값을 직접 변경하지 않고, setXX 또는 getXX라는 메서드를 이용해서 값을 수정하거나 읽는 규칙을 만들었는데, 이가 바로 자바빈 프로퍼티 규약이다.
필드 주입
- 말 그대로 필드에 그대로 주입해버리는 방식이다.
- 주입할 인스턴스 앞에
@Autowired
를 붙이면 코드가 끝나버리기에 굉장히 간결한 장점을 가지고 있다. - 하지만 외부에서 해당 인스턴스에 대한 변경이 불가하다는 큰 단점이 존재한다.
@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 옵션을 처리하는 방식을 알아보자. - 먼저 required 는 디폴트 값이 true로 설정되어 있기 때문에, 자동 주입 대상이 없을 경우 오류를 발생시킨다.
- 테스트에 autowired 패키지를 생성하고 아래에 AutowiredTest를 생성한다
- 이후 내부 클래스로 스프링빈 등록이 되지 않은 Member 객체를 연결하는 코드를 작성해준다.
- setNoBean1()의 required 옵션을 디폴트 값으로 지정하면 아래와 같은 오류가 발생한다.
- setNoBean1()의 옵션을 다시 false로 하고 테스트를 진행해보면 아래와 같은 출력 결과를 얻을 수 있다.
- 주입할 스프링 빈이 없지만 동작시켜야 할때는 아래의 3가지 방식을 이용하면 된다.
public class AutowiredTest {
@Test
void AutowiredOption() {
ApplicationContext ac = new AnnotationConfigApplicationContext(TestBean.class);
}
static class TestBean {
@Autowired(required = false)
public void setNoBean1(Member noBean1) {
System.out.println("noBean1 = " + noBean1);
}
@Autowired
public void setNoBean2(@Nullable Member noBean2) {
System.out.println("noBean2 = " + noBean2);
}
@Autowired
public void setNoBean3(Optional<Member> noBean3) {
System.out.println("noBean3 = " + noBean3);
}
}
}
생성자 주입
최근의 DI 프레임워크들은 생성자를 통한 의존관계 주입을 권장하고 있다.위에서 읽었듯이 필드 주입 또는 메서드 주입은 자명한 단점이 있는데, 수정자 주입은 어떠한 단점이 있어서 권장되지 않는지 그리고 왜 생성자 주입을 사용해야 하는지 그에 대한 이유는 다음과 같다.
불변
- 대부분의 의존관계 주입은 한번 발생하면 애플리케이션 종료시점까지 의존관계가 변경되는 일이 없다. 오히려 이러한 의존관계들은 애플리케이션이 구동하는 동안 동일하게 유지되어야한다
- 만약 수정자 주입을 사용하면 public으로 set객체() 등등의 메서드를 오픈해놓아야한다.
- 불변한 상태를 유지해야하는 인스턴스를 구지 public으로 열어둘 필요도 없고, 누군가가 실수로 변경할 여지도 있기에 해당 방식은 좋은 설계가 아니라 생각된다.
- 생성자 주입의 경우, 스프링 빈이 생성될때 딱 한번만 의존관계를 주입해주고 접근 할 수 있는 문 자체를 닫아버리기에 불변성 유지 측면에서 완벽하다고 볼 수 있다.
누락
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;
}
//...
}
- 위와 같은 수정자 주입으로 의존관계를 설정하는 경우 아래의 테스트 코드를 실행하게 되는 어떤결과가 나오게 될까?
@Test
void createOrder() {
OrderServiceImpl orderService = new OrderServiceImpl();
orderService.createOrder(1L, "itemA", 10000);
}
- 테스트는 이상없이 돌아간다. 하지만 보이는 바와 같이 주입할 인스턴스를 생략(누락)했기 때문에 NPE가 발생한다.
- 하지만 위의 테스트를 생성자 주입을 통해 의존관계를 넣어주면, OrderService 인스턴스를 선언하는 라인에서 바로 빨간줄을 띄우며, 컴파일 에러를 발생시킨다.
- 즉, IDE에서 어떤 값을 필수로 주입해야하는지 알려준다.
final 사용
- 위와 같이 생성자 주입을 사용하는 경우 final은 사용하지 못한다.
- 그러면 당연히 위와 같이 인스턴스 누락을 컴파일 타임에서 잡아주지 못하는 단점이 생긴다.
- 그래서 유일하게 final 키워드를 사용할 수 있는 생성자 주입이 권장된다.
- 생성자 주입을 제외한 나머지 경우는 생성자 이후에 호출되기에 final을 사용할수 없다.
생성자 주입에 롬복 적용하기
롬복 설치하기
- build.gradle에 적용한다.
- Prefrences -> plugin -> lombok 검색 후 설치
- Prefrences -> Annotation Processors 검색 -> Enable annotation processing 활성화
@RequiredArgsConstructor 적용하기
<기존 코드>
@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;
}
}
<적용 코드>
@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
}
- 롬복 라이브러리가 제공하는
@RequiredArgsConstructor
를 기능을 사용하면 final이 붙은 필드들을 모아서 생성자를 자동으로 만들어준다.(적용코드에는 보이지 않지만 실제 호출 가능함)
조회되는 빈이 2개이상인 경우의 해결
- 다음 상황을 OrderServiceImpl로 설명하자면, 주입할 인스턴스인 DiscountPolicy를 찾기위해 컨테이너에서 컴포넌트 스캔을 진행할 텐데, 이 때 DiscountPolicy을 가진 동일한 두개의 스프링 빈(RateDiscountPolicy, FixedDiscountPolicy)이 등록되어있으면 아래와 같이 중복된다는 내용의 오류가 발생한다.
- 해당 문제를 해결할 수 있는 방법은 3가지가 존재한다.
- @Autowired 필드 명 매칭
- 다음과 같이 정의된 인스턴스의 이름을
@Autowired private DiscountPolicy discountPolicy
@Autowired private DiscountPolicy rateDiscountPolicy
로 바꿔준다. - 즉, 중복되는 빈이 존재할 경우 필드 이름(변수명)으로 빈 이름을 추가해준다.
- 그러면 변경된 변수명을 가진 스프링 빈을 의존 관계 주입의 대상으로 선정한다.
- @Qualifier
@Qualifier
는 추가 구분자를 붙여주는 방식이다.- 아래 코드와 같이 빈 등록 시
@Qualifier
를 붙여준다
<RateDiscountPolicy>
@Component
@Qualifier("mainDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy {}
<FixedDiscountPolicy>
@Component
@Qualifier("fixDiscountPolicy")
public class FixDiscountPolicy implements DiscountPolicy {}
- 이후 생성자 의존관계 주입 시
@Qualifier
를 붙여주고 이름을 적어준다
@Autowired
public OrderServiceImpl(MemberRepository memberRepository,
@Qualifier("mainDiscountPolicy") DiscountPolicy
discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
- @Primary
- 어노테이션 이름과 같이 중복된 타입의 빈들 사이에서 우선순위를 정해주는 방식이다.
- 간단하게 사용할 빈의 클래스에
@Primary
만 붙여주면 해당 스프링 빈이 의존관계 주입 시 사용된다.
<rateDiscountPolicy
가 우선순위를 가지게 하는 코드>
@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy {}
@Component
public class FixDiscountPolicy implements DiscountPolicy {}
동일한 타입의 빈을 모두 조회
의도적으로 동일한 타입의 모든 스프링 빈이 필요한 경우도 존재한다. 예를 들어 현재 할인 정책은 ratePolicy와 fixedPolicy로 나뉘는데 클라이언트가 해당 정책을 선택 할 수 있는 상황이면 두개의 정책 모두 필요한 상황이다.아래의 테스트 코드를 통해 동일한 타입을 모두 조회하는 코드를 구현한다.
package hello.core.autowired;
import static org.assertj.core.api.Assertions.*;
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, "brido", 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> policyList;
@Autowired
public DiscountService(Map<String, DiscountPolicy> policyMap, List<DiscountPolicy> policyList) {
this.policyMap = policyMap;
this.policyList = policyList;
System.out.println("policyMap = " + policyMap);
System.out.println("policyList = " + policyList);
}
public int discount(Member member, int price, String discountCode) {
DiscountPolicy discountPolicy = policyMap.get(discountCode);
return discountPolicy.discount(member,price);
}
}
}
- DiscountService는
@Autowired
로 인해 Map으로 모든DiscountPolicy
를 주입받는다.이때fixDiscountPolicy
,rateDiscountPolicy
가 주입된다. - discount()메서드는 discountCode의 인자로 ‘fixDiscountPolicy’가 넘어오면 map에서
fixDiscountPolicy
를 스프링 빈을 찾아서 실행한다. - Map<String, DiscountPolicy> policyMap은 map의 키에 스프링 빈의 이름을 넣어주고, value값으로는
DiscountPolicy
타입으로 조회한 모든 스프링 빈을 담아준다. - 아래와 같이 해당 타입의 모든 스프링 빈이 담긴 것을 알 수 있다.
소스코드 : https://github.com/brido4125/core-spring
해당 글은 김영한님의 <스프링 핵심> 강좌를 기반으로 작성되었습니다.