자바생
article thumbnail
728x90

다양한 의존관계 주입 방법

의존관계 주입은 생성자 주입, setter 주입, 필드 주입, 일반 메서드 주입이 있다.


생성자 주입

  • 생성자를 통해 의존 관계를 주입 받는 방법
  • 생성자가 딱 1개만 있으면 @Autowired를 붙이지 않아도 자동 주입 된다.
    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

    @Autowired
    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy rateDiscountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = rateDiscountPolicy;
    }
  • 필드에 "final 키워드를 작성할까?"
  • final 변수는 재할당 불가, 생성자나 초기화 block을 통해 "무조건" 값을 가져야만 함
    • 불변, 필수 의존관계에 사용

setter 주입

  • 생성자 주입
    • 불변/필수
  • setter
    • 변경/선택
    • setXXX를 사용하면 메서드를 통해 변경할 수 있음
    • setXXX를 "선택적"으로 호출하여 주입하여 "선택"할 수 있음
Q.
setter주입은 선택, 변경
생성자 주입은 불변, 필수 라고 말씀하셨습니다.
setter주입은 setXXX 메서드를 통하여 변경할 수 있고, 파라미터마다 setXXX를 만들면 선택적으로 주입할 수 있다.
생성자 주입은 생성자가 1번 호출 되는 것이 보장되기 때문에 불변이라 할 수 있다.
그러나 필수 이 부분은 어느 부분때문에 필수라고 말할 수 있을까?
A.
생성자로 설정한 객체는 별도의 수정자가 없으면 변경이 불가하므로 불변이다.
필수는 생성자의 특징을 말하는 것이 아니라 파라미터가 필수적으로 들어와야한다.
왜? 
setter주입은 해당 set 메서드를 사용하지 않으면 상관이 없지만, 생성자는 클래스를 생성할 때 당연하게 실행이 되니까 파라미터가 빈으로 존재하지 않으면, 클래스의 객체를 생성하지 못한다.

필드 주입

@Autowired private MemberRepository memberRepository;
@Autowired private DiscountPolicy discountPolicy;
  • 장점
    • 매우 편함!
  • 단점
    • 테스트를 돌릴 때에 순수 자바 코드로 작성하기 어렵다.
    • 외부에서 변경이 어려움
      • memberRepository 나 discountPolicy 에 접근할 수 있는 메서드가 없다면 해당 객체에 접근할 수 없다!!
  • 테스트
    • 순수 자바 코드로 작성한 테스트
      • 일반적으로 new 로 객체를 생성
      • 당연히 @Autowired 작동하지 않음
      • 당연히 의존관계 주입 X, 해당 필드 NPE 발생!
    • 스프링 컨테이너를 띄워서 실행하는 테스트 (feat. @SpringBootTest)
      • Bean을 사용하여 의존관계 주입
      • @Autowired 작동하기 위해서는 스프링 컨테이너가 "관리"해야함
      • 즉, @SpringBootTest가 없다면 @Autowired 사용할 수 없다!!
  • 그래서 필드 주입은 "테스트 코드"를 작성할 때 많이 사용함!
    • ? 애플리케이션의 실제 코드와 관계가 없기 때문~
Q.
setter주입은 단계가 나눠져있고, 생성자 의존관계 주입은 빈을 등록하면서 의존관계 주입이 동시에 일어난다고 말씀해주셨습니다.
단계로 나눈다는 말씀은 빈을 등록하고, 의존관계 주입을 할 때, 컨테이너에서 빈을 조회하여 주입시킨다. 이 부분은 이해가 됩니다.
결국에는 setter이든 생성자이든 단계가 나눠지거나 동시에 일어난다인가요?
A.
setter주입은 빈을 생성할 때 의존관계에 있는 빈들을 주입하지 않고 나중에 별도로 set 메서드를 사용하여
주입해야한다. 그래서 빈 등록 단계와 의존관계 주입이 분리가 된다고 말할 수 있다.

출처 : QnA 


생성자 주입을 택해야 하는 이유

private  MemberRepository memberRepository;
private  DiscountPolicy discountPolicy;

@Autowired
public void setMemberRepository(MemberRepository memberRepository) {
    this.memberRepository = memberRepository;
}

@Autowired
public void setDiscountPolicy(DiscountPolicy discountPolicy) {
    this.discountPolicy = discountPolicy;
}

@Override
public Order createOrder(Long memberId, String itemName, int itemPrice) {
    Member member = memberRepository.findById(memberId);
    int discountPrice = discountPolicy.discount(member, itemPrice);

    return new Order(memberId, itemName, itemPrice, discountPrice);
}
@Test
public void setterInjection() throws Exception {
    OrderServiceImpl orderService = new OrderServiceImpl();

    orderService.createOrder(1L, "a", 1000);
}

위와 같이 테스트를 작성하고 돌리면 결과가 어떻게 될까??

  • 결과적으로 NPE 발생
  • 왜????????
  • OrderServiceImpl MemberRepository와 DiscountPolicy가 필요함
  • 그러나 setter 주입으로 각각의 객체가 자동으로 의존관계 주입이 되지 않고 setter를 호출해야함
  • 따라서 OrderServiceImpl 코드를 보고나서 "아 이건 setter로 주입을 해줘야하는구나"라고 알 수 있음

정리

  • setter주입을 사용할 경우에 테스트를 순수 자바 코드로 실행할 경우
  • setter를 사용하여 MemberRepository와 DiscountPolicy 객체를 생성하지 않을 경우 NPE가 발생
  • setter 주입은 직접 OrderServiceImpl 코드를 보고 나서 알 수 있음

Lombok

해당 강의에서는 코드를 최적화할 수 있는 롬복 플러그인을 써보았다.

 

@Getter

@Setter

private 변수를 생성하게 되면 우리는 getter setter 메서드를 이용했다. 

항상 해당 코드를 작성하기 번거롭기 때문에 롬복에서는 위와 같은 어노테이션을 사용해 편하게 

getter setter 메서드를 만들어준다.

 

@ToString

위 어노테이션은 toString을 만들어준다.

 

@NoArgsConstructor

파라미터가 없는 기본 생성자를 생성

 

@RequiredArgsConstructor

final이 붙은 필드 값만 파라미터로 생성자를 생성

 

@AllArgsConstructor

모든 필드를 파라미터로 생성자를 생성

 


@Autowired를 사용하는데 조회 빈이 2개 이상 나올 때

  • DiscountPolicy의 하위 타입인 RateDiscountPolicy, FixDiscountPolicy 모두 빈에 등록
  • OrderServiceImpl 생성자 주입 시 DiscountPolicy에 2개의 빈이 조회됨
    • 발생
      • NoUniqueBeanDefinitionException
      • 로그 : expected single matching bean but found 2: fixDiscountPolicy,rateDiscountPolicy
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
    this.memberRepository = memberRepository;
    this.discountPolicy = discountPolicy;
}

그렇다면 위와 같은 예외를 발생시키지 않기 위해서는 3가지 방법이 있다.


@Autowired 필드 명 매칭

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

 

  • Autowired는 처음에 타입 매칭 시도
  • 조회된 빈이 여러 개일 경우
    • 필드 이름, 파라미터 이름으로 빈 이름을 추가 매칭함
  • 즉, 처음엔 DiscountPolicy로 매칭 시도
  • RateDiscountPolicy, FixDiscountPolicy 조회
    • rateDiscountPolicy, fixDiscountPolicy으로 "빈 이름"이 등록되어 있기 때문에
    • rateDiscountPolicy에 RateDiscountPolicy 빈이 주입

@Qualifier

//OrderServiceImpl.class

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


//RateDiscountPolicy.class

@Qualifier("mainDiscountPolicy")
public class RateDiscountPolicy{
	//code
}
  • 빈 이름을 변경 X (헷갈리지 말자)
  • 클래스 이름을 그대로 사용하면서, 이 해당 빈이 대표하는(?) 이름이라고 생각하면 된다.
  • Autowired와 비슷하게 처음엔 @Qualifier끼리 매칭
  • 없으면 빈 이름 매칭
  • 또 없으면 NoSuchBeanDefinitionException 발생
  •  
  • 단점
    • 사용하는 모든 곳에 @Qualifier 붙여야함

@Primary

  • 조회된 여러 빈 중에 우선순위가 제일 높음
  • RateDiscountPolicy 클래스에 @Primary 어노테이션을 넣으면 DiscountPolicy에 RateDiscountPolicy 주입
  • Qualifier와 다르게 생성자 파라미터에 Primary 넣지 않아도 됨

Qualifier vs Primary

Qualfiier

  • 우선순위가 더 높음
  • 사용하는 곳마다 Qualifier
  • 코드 지저분

Primary

  • 우선순위를 높일 클래스에만 붙이면 됨
  • 코드 깔끔

Primary, Qualifier는 어디에 사용할까?

  • 자주 사용하는 메인 DB의 커넥션을 획득하는 스프링 빈
  • 특별한 기능으로 가끔 사용하는 서브 DB의 커넥션을 획득하는 스프링 빈
  • 메인 DB의 스프링 빈에 @Primary를 적용하여 편리하게 조회 가능
  • 여러 개의 DB가 있을 경우 @Qualifier를 이용하여 "secondDB", "thirdDB" 등 별칭을 이용하여 쉽게 구분 가능!

 


AnnotationConfigApplicationContext는 스프링 컨테이너다

ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class, DiscountService.class);
  • new AnnotationConfigApplicationContext() 을 통해 스프링 컨테이너 생성
  • AutoAppConfig.class, DiscountService를 파라미터로 넘기면서 해당 클래스를 스프링 빈으로 등록

 

 

Q.
DiscountService는 @Component, 
@Configuration이
 없는데 어떻게 빈으로 등록됐으며, 생성자를 이용한 의존관계 주입을 할 수 있는걸까?
A.
스프링 컨테이너(AnnotationConfigApplicationContext)를 만들 때 파라미터를 넘기면, 해당 클래스를 특별하게 자동으로 스프링 빈으로 등록해준다고 한다.

 

출처 : QnA


조회한 빈이 모두 필요할 때, List || Map

조회한 빈이 모두 필요할 경우가 있을까?

  • 할인 서비스에서 해당 클라이언트에 해당하는 할인의 종류가 여러 개 있다고 생각해보자
  • 할인이라는 "전략"이 다양하게 있는 것
  • 전략 패턴을 매우 간단하게 구현할 수 있음
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);
        return discountPolicy.discount(member, price);
    }
}

/*
policyMap = {fixDiscountPolicy=hello.core.discount.FixDiscountPolicy@64a8c844, rateDiscountPolicy=hello.core.discount.RateDiscountPolicy@3f6db3fb}
policies = [hello.core.discount.FixDiscountPolicy@64a8c844, hello.core.discount.RateDiscountPolicy@3f6db3fb]
*/
  • Map
    • key : 등록된 빈의 이름
    • value : 빈 저장
  • List
    • 빈 저장

이렇게도 의존관계 주입이 가능하다는 것을 보고 매우 신기했다!!!

언젠가 유용하게 쓸 것 같다

 


REFERENCES

 

 

 

728x90
profile

자바생

@자바생

틀린 부분이 있다면 댓글 부탁드립니다~😀

검색 태그