의존관계 자동 주입

KOO HEESEUNG·2021년 10월 19일
0
post-thumbnail

인프런 김영한 님의 <스프링 핵심 원리 - 기본편> 강의 내용을 정리한 것입니다.

다양한 의존관계 주입 방법

의존관계 주입은 크게 4가지 방법이 있다.

1. 생성자 주입

생성자를 통해 의존관계를 주입 받는 방법.

생성자 호출 시점에 딱 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;
  }
  ...
}

2. 수정자 주입(setter 주입)

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) 로 지정

3. 필드 주입

필드에 바로 주입하는 방법.

사용하지 말자!

코드가 간결하지만, 외부에서 변경이 불가능해서 테스트하기 힘들다.
DI 프레임워크가 없으면 아무것도 할 수 없다.

  • 사용해도 되는 곳 : 테스트 코드나 스프링 설정을 목적으로 하는 @Configuration 같은 곳에서만
@Component
public class OrderServiceImpl implements OrderService {

  @Autowired private MemberRepository memberRepository;
  @Autowired private DiscountPolicy discountPolicy;

  ...
}

4. 일반 메서드 주입

일반 메서드를 통해서 주입 받을 수 있다.

한번에 여러 필드를 주입 받을 수 있다. 일반적으로 잘 사용하진 않는다.

@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);
} 

생성자 주입을 선택해라!

생성자 주입 방식은 프레임워크에 의존하지 않고, 순수한 자바 언어의 특징을 잘 살리는 방법이다.

생성자 주입을 권장하는 이유

불변

  • 대부분의 의존관계 주입은 한번 일어나면 애플리케이션 종료 시점까지 의존관계를 변경할 일이 없고, 오히려 변하면 안된다.
  • 수정자 주입을 사용하면 setter를 public 으로 열어두어야 한다. 변경하면 안되는 메서드를 열어두는 것은 좋은 설계 방법이 아니다.
  • 생성자 주입은 객체를 생성할 때 딱 1번만 호출되므로 이후에 호출되는 일이 없다. 따라서 불변하게 설계할 수 있다.

누락 방지

  • 프레임워크 없이 순수한 자바 코드를 단위테스트 하는 경우에 수정자 주입을 할 경우, 주입 데이터를 누락했을 때 컴파일 오류가 발생하여 어떤 값을 필수로 주입해야 하는지 알 수 있다.

final 키워드

  • 생성자에서 혹시라도 값이 설정되지 않는 오류를 컴파일 시점에 막아준다.
  • 생성자 주입을 제외한 다른 주입 방식은 모두 생성자 이후에 호출되므로, 필드에 final 키워드를 사용할 수 없다.

컴파일 오류가 세상에서 가장 빠르고 좋은 오류다!

롬복과 최신 트렌드

생성자 주입의 단점은 코드가 길어지고, 필드가 추가될 때 생성자도 수정해주어야 한다는 점이다.

롬복(lombok) 이라는 라이브러리를 사용하면, 정해진 어노테이션을 붙이는 것만으로도 getter, setter, 생성자 등을 자동으로 생성해준다.

생성자 주입을 사용하기 위해 @RequiredArgsConstructor 어노테이션을 붙이면 private final 이 붙은 필드를 모아 생성자를 자동으로 만들어준다.

@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {

  private final MemberRepository memberRepository;
  private final DiscountPolicy discountPolicy;
	...
}

조회 빈이 2개 이상 - 문제

@Autowired 는 타입으로 조회하며, ac.getBean(~~~.class) 와 유사하게 동작한다.

그러나 타입으로 조회하면 선택된 빈이 2개 이상일 때 문제가 발생한다.

NoUniqueBeanDefinitionException 이 발생한다.

하위 타입으로 지정할 수 있지만, 이는 DIP를 위반하고 유연성이 떨어진다.(역할에 의존해야지, 구체적인 것에 의존하면 안됨.)

이를 해결하는 방법은 다음과 같다:

@Autowired 필드명 매칭

  1. 타입 매칭
  2. 타입 매칭 결과가 2개 이상일 경우, 필드 이름, 파라미터 이름으로 빈 이름 매칭

@Qualifier 사용

@Qualifier 는 추가 구분자를 붙여주는 방법이다. 주입시 추가적인 방법을 제공하는 것이지, 빈 이름을 변경하지는 않는다.

  1. @Qualifier끼리 매칭
  2. 1에서 해당하는 것이 없으면 빈 이름으로 매칭
  3. 2로도 매칭되지 않으면 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;
}

@Primary 사용

우선순위를 정하는 방법. @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을 주입한다.

DiscountServiceMap<String, DiscountPolicy> 으로 모든 DiscountPolicy 를 주입받는다.

discount() 메서드는 discountCodefixDiscountPolicy 가 넘어오면 Map에서 해당 키를 가진 스프링 빈을 찾아서 실행한다.

자동, 수동의 올바른 실무 운영 기준

  1. 편리한 자동 기능을 기본으로 사용

  2. 수동 빈 등록을 사용하면 좋은 경우

    (1) 직접 등록하는 기술 지원 객체
    기술 지원 로직에 사용하여 명확하게 드러내는 것이 유지보수에 좋다. 이는 애플리케이션에 광범위하게 영향을 미치며, 적용이 잘 되고 있는지 아닌지조차 파악하기 어려운 경우가 많기 때문.

    애플리케이션은 크게 업무 로직과 기술 지원 로직으로 니뉜다.

    • 업무 로직 빈 : 컨트롤러, 서비스, 리포지토리 등.
    • 기술 지원 빈 : 기술적인 문제나 공통 관심사를 처리할 때 주로 사용. DB 연결과 같이 업무 로직을 지원하기 위한 하부 기술이나 공통 기술.

    (2) 다형성을 적극 활용하는 비즈니스 로직

    의존관계 자동 주입 - 조회한 빈이 모두 필요할 때.

    협업할 경우, 어떤 빈이 주입될지 여러 코드를 열어봐야 하는 등 어려움이 있기 때문에 수동 빈으로 등록하거나, 자동으로 등록하면 특정 패키지에 같이 묶어두는 것이 좋다.

0개의 댓글