7장 의존관계 자동주입

인프런 김영한님의 스프링 핵심 원리 - 기본편 7장 의존관계 자동주입 정리했으며 본인이 핵심이라고 생각하지 않은 부분에 대해서는 과감하게 생략하였습니다.


다양한 의종관계 주입 방법

  • 생성자 주입
  • 수정자 주입(Setter)
  • 필드 주입
  • 일반 메서드 주입

생성자 주입

이름 그대로 생성자를 통한 의존 관계 주입

특징

  • 생성자 호출시점에 딱 1번만 호출되는 것이 보장
  • 불변, 필수 의존관계에 사용
  • 생성자가 실행중에는 아직 클래스의 멤버 변수들이 초기화 되지 않은 시점이기 때문에 final 키워드가 붙은 멤버 변수에 대하여 의존성 주입이 가능

사용 예시

  1. 일반적인 사용
    @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;
     }
    }
    

Tip : 클래스 생성자가 하나뿐이라면 @Autowired 는 생략 가능합니다.

  1. 롬북(lombok)을 활용한 사용 (추천)
    @Component
    @RequiredArgsConstructor
    public class OrderServiceImpl implements OrderService {
     private final MemberRepository memberRepository;
     private final DiscountPolicy discountPolicy;
    }
    

룸북을 이용하면 final 키워드가 붙은 필수 초기화 멤버 변수들에 대하여 의존성 주입이 가능한 생성자를 자동으로 만들어줍니다. 코드가 깔끔하기 때문에 이 방식을 추천합니다.

옵션(주입의 필수 여부) 처리

말 그대로 주입의 필수 여부 선택이 가능합니다. 만약 원하는 이 존재하지않아 주입이 불가능한 상황이라면 이를 그냥 진행할지, 또는 진행하면 안될지를 정할 수 있습니다.

@Autowired 만 사용하면 required 옵션의 기본값이 true 로 되어 있어서 자동 주입 대상이 없으면 오류가 발생합니다.

  1. @Autowired(required=false) 자동 주입할 대상이 없으면 메서드 자체가 호출되지 않습니다.
//호출 안됨@Autowired(required = false)
public void setNoBean1(Member member) {
    System.out.println("setNoBean1 = " + member);
}
  1. org.springframework.lang.@Nullable 자동 주입할 대상이 없으면 null이 입력됩니다.
//null 호출
@Autowired
public void setNoBean2(@Nullable Member member) {
    System.out.println("setNoBean2 = " + member);
}
  1. Optional<> 자동 주입할 대상이 없으면 Optional.empty 가 입력됩니다.
//Optional.empty 호출
@Autowired(required = false)
public void setNoBean3(Optional<Member> member) {
    System.out.println("setNoBean3 = " + member);
}

### Optional<>

여기서 Optional<> 생소하실수 있어서 잠깐 설명을 드리자면 자바8 부터 사용할 수 있는 클래스이며, 런타임중에 의도치않게 일어나는 NullPointerException 을 최대한 줄이기 위해서 개발되었습니다.

우리가 개발을 하다보면 NullPointerException 꼭 한번은 존재하는데, 이 객체가 언제 Null이 된 것인지 확인을 하고자 디버깅하는 시간이 생각보다 많이 소요됩니다.

왜냐면 객체를 Set 하고나서 바로 사용하지않고 한참 뒤에 사용하면 콜스택을 추적하면서 이게 언제 Null로 변했을까~~ 하고 스크롤을 올려보면서 찾거나 열심히 Ctrl + F를 하여 Null이 되어버린 객체 = 를 검색해야하죠,

이를 근본적으로 해결하기 위한 방법은 간단합니다. 객체의 값이 바뀌는 시점에 객체가 Null이면 바로 예외를 발생시켜버리는 것 입니다.

  1. Optional 사용 전 ```java

User myUser = userService.findUser(“wusub”); // findUser() 의 반환값이 null 일 때

// 기타 부가적인 로직.. // 기타 부가적인 로직..

User tempUser = myUser;

// 기타 부가적인 로직.. // 기타 부가적인 로직..

tempUser.getName(); // NullPointerException 발생!

 
 `myUser`객체가 실제로 `null` 이 된 시점은 `myUser.getName();` 가 호출되기 한참 전인 `userService.findUser("wusub");` 호출 시점이며 이미 `myUser`객체가 null이 된 시점에서 해당 로직은 실패가 예정되어있는데 기타 부가적인 로직을 진행하여 불필요한 리소스가 소모된다는 점, 그리고 로직이 섞여있어 `myUser` 가 null이된 실제 시점을 찾기에 불편하다는 점이 존재합니다.
 
 2. **Optional 사용 후**
 ```java
 Optional<User> myUser = Optional.of(userService.findUser("wusub")); // findUser() 의 반환값이 null 일 때 바로 NullPointerException 발생!

Optional을 사용하면 userService.findUser("wusub") 의 반환이 null이 시점에서 바로 NullPointerException 이 발생합니다. 불필요한 기타 부가적인 로직은 진행되지도 않고 콜스택도 Optional.of(userService.findUser("wusub")); 에서 멈추게 되기에 리소스 낭비가 없고 디버깅하기 훨씬 수월합니다.

그렇다고 Optional을 남발하면 만사 오케이냐? 그건 또 아닙니다. Optional 객체는 내부적으로 null 검증이라는 작업이 진행되는데 모든 변수에대하여 null 검증 로직은 생각보다 리소스가 많이 소모됩니다.

해당 포스팅은 Optional 에 대한 내용이 아니기에 여기까지만 설명하며, 추후에 Optional 에 대해서는 별도의 포스팅을 작성해보겠습니다.

다시 본론으로 돌아가

//Optional.empty 호출
@Autowired(required = false)
public void setNoBean3(Optional<Member> member) {
    System.out.println("setNoBean3 = " + member);
}

위 코드에서 Optional.empty 라는 말은 null은 아니고 비어있는 객체, 사용자가 null이 아닌 다른 방법으로 객체가 비어있는것을 체크할 수 있도록 하기 위함입니다.


조회 빈이 2개 이상

Spring이 자동으로 의존성 주입을 시도하려하는데, 가져와야할 Bean이 2개일 경우 어떻게될까요 실제로 Bean 이름으로 조회시는 위 현상이 발생할 경우가 드물지만 타입으로 빈을 조회하게될 경우 선택지가 2개 이상일 가능성이 존재합니다.

특히 @Autowired 는 기본이 Type으로 조회이기 때문에 하위 타입을 갖는 빈을 주입시 선택지가 2개 이상이 되어 스프링이 선택을 하지못하고 에러가 발생하는 경우가 있습니다.

예를 들어

@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy {}
@Component
public class FixDiscountPolicy implements DiscountPolicy {}

위와 같이 DiscountPolicy 를 구현하는 rateDiscountPolicy, fixDiscountPolicy 라는 두개의 Bean으 등록되어 있을 때

아래와 같이 상위 타입인 DiscountPolicy@Autowired 를 주입받으려하면 에러가 발생합니다.

@Autowired
private DiscountPolicy discountPolicy

이때 대표적인 해결법 3가지가 존재합니다.

1. Bean 이름과 변수명 매칭

쉽습니다. 내가 주입 받고자 하는 bean이름과 @Autowired 가 선언된 변수 이름을 매칭 시키는 것 입니다.

@Autowired 를 통한 빈 주입 순서는 타입 -> 빈 이름과 동일 여부 or 파라미터 이름 (생성자 또는 셋터 주입일 경우)

@Autowired
private DiscountPolicy rateDiscountPolicy
@Autowired
private DiscountPolicy fixDiscountPolicy

2. @Qualifier (내용 식별 수식자)

빈이 등록되는 객체에 수식어 를 달아서 구분할 수 있게 하는 것 입니다. 이는 빈 이름과는 별도의 메타 정보입니다.

사용법은 빈 등록시의존성 주입시 양쪽 모두에다가 @Qualifier 어노테이션을 사용해야합니다.

빈 등록

@Component
@Qualifier("mainDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy {}

의존성 주입

@Autowired
public OrderServiceImpl(MemberRepository memberRepository, @Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy) {
    this.memberRepository = memberRepository;
    this.discountPolicy = discountPolicy;
}

@Qualifier 주입 순서는 Qualifier 끼리 비교 -> Qualifier() 에 적힌 내용으로 빈 이름 검색 -> 못찾으면 예외발생

3. @Primary (주요한/우선순위)

주요한 이라는 단어 뜻 처럼 중요도를 매깁니다. 2개 의 Bean 검색되었을 시 @Primary 어노테이션을 사용하여 Bean으로 등록된 Bean을 선택하게 됩니다.

@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy {}
@Component
public class FixDiscountPolicy implements DiscountPolicy {}

@Qualifier 와 @Primary 둘 중 누가 더 우선일까?

@Qualifier 가 더 우선, 왜? @Qualifier 는 메타 정보를 토대로 콕! 집어내는 성격이라 더 우선순위가 높음