BackEnd/Spring

[Spring] 의존관계 자동 주입

샤아이인 2022. 2. 5.

내가 공부한것을 올리며, 중요한 단원은 저 자신도 곱씹어 볼겸 상세히 기록하고 얕은부분들은 가겹게 포스팅 하겠습니다.

 

1. 옵션 처리

이번시간에는 옵션처리를 공부하였다. 이는 주입할 스프링 빈이 없을때도 동작하도록 하는 과정이였다.

기본적으로 @Autowired는 required = true 이기 때문에 자동 주입대상이 없으면 오류가 발생한다.

 

3가지 방식으로 처리가 가능했다.

1) @Autowired(required = false) : 자동 주입할 대상이 없으면 수정자 메서드 자체가 호출 안되는 방식

2) org.springframework.lang.@Nullable : 자동 주입할 대상이 없으면 null이 입력된다.

3) Optional<> : 자동 주입할 대상이 없으면 Optional.empty 가 입력된다. (자바8 부터 가능한 문법)

 

테스트 코드를 작성해서 확인해 보자 (영한선생님과는 조금 다르게 TestBean 클래스를 바로 등록한것이 아닌, Config를 만들어 등록하였다)

코드에서 자동 주입하려 하는 member는 스프링 빈이 아니다!

public class AutowiredTest {

    @Test
    void AutowiredOption() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(NoBeanAppConfig.class);
    }

    @Configuration
    @ComponentScan
    static class NoBeanAppConfig{
    }

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

}
 

스캔의 범위는 autowired 패키지 이하가 될것이다.

결과 또한 noBean1은 주입할 대상이 없었기 때문에 메서드 호출조차 되지 않았다. => 출력문도 안나옴

나머지 2, 3은 호출되었으며, Null을 보여준다.

 

2. 생성자 주입을 선택해라!

대부분의 경우 생성자 주입을 사용하면 된다. 생성자 주입은 불변 한다는 장점이 있기 때문이다.

 

● 불변

대부분의 의존관계 주입은 한번 일어나면 애플리케이션이 끝날때까지 의존관계를 변경할일이 없다. 오히려 의존관계는 어플리케이션 종료 전까지 바뀌면 안된다.

 

또한 Setter방식을 이용하면 public으로 메서드가 노출되기 때문에 누군가 실수로 변경할수 있다. 이는 좋은 설계가 아니다.

 

● 누락

스프링 프레임 워크가 아닌! 순수 자바 코드로 단위 테스트를 하는 경우가 많이 있다.

이럴때 실수로 필요한 의존관계를 설정하지 않는 오류를 사전에 컴파일 오류로 방지할 수 있다.

 

생성자 주입을 사용하면 필드에 final 키워드를 사용할 수 있다.

그래서 생성자에서 혹시라도 값이 설정되지 않는 오류를 컴파일 시점에 막아준다. 다음 코드를 보자.

@Component
public class OrderServiceImpl implements OrderService {
    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

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

discountPolicy는 값이 생성자를 통해 지정되지 않고 있다. 하지만 discountPolicy는 final로 지정되어있기 때문에 값을 갖어야 하며, 이는 생성자를 통해서 받아야 한다. 생성자로 생성된 이후에서는 변경이 불가능 하다.

 

3. 롬복 (Lombok)

이번 강의에서는 처음으로 롬복을 사용해보는 시간이였다.

 

이전에 맴버변수들을 final로 선언후 생성자로 의존성을 주입해야 한다는 부분이 많았다.

문제는 개발자는 이또한 귀찮자는 점 이다. 필드 주입처럼 편하게 사용하는 방법이 없을까? 라는 점에서 사용하는 것이 롬복이다.

 

다음 코드를 살펴보자.

@Component
public class OrderServiceImpl implements OrderService {
    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

    // @Autowired 생성자가 딱 1개라 생략 가능
    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }
}

 

위의 코드는 생성자가 딱 1개라 @Autowired가 생략이 가능하다.

 

여기에 롬복을 적용해 보자.

롬복이 제공하는 @RequiredArgsConstructor 기능을 사용하면 final이 붙은 필드를 모아서 생성자를 만들어 준다.

 

따라서 다음 코드와같이 만들 수 있다.

@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;
}
 

이후 OrderServiceImpl의 java 코드를 확인해 보면 생성자가 구현되있음을 알수 있었다.

 

4. 스프링 빈이 2개 이상

@Autowired는 Type으로 조회한다.

만약 다음과 같이 DiscountPolicy의 하위 타입인 FixDiscountPolicy , RateDiscountPolicy 이 둘다 스프링 빈으로 등록되어 있다면 어떤 문제가 발생할까? 다음 코드와 같이 변경해 보자.

@Component
public class FixDiscountPolicy implements DiscountPolicy {}

@Component
public class RateDiscountPolicy implements DiscountPolicy {}
 

이후 테스트를 진행하면 오류가 발생한다.

public class AutoAppConfigTest {

    @Test
    void basicScan(){
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class);

        MemberService bean = ac.getBean(MemberService.class);
        Assertions.assertThat(bean).isInstanceOf(MemberService.class);
    }
}
 

NoUniqueBeanDefinitionException 이 발생해 버렸다.

다음 코드를 보자
@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;
    }

    // 생략...
}

OrderServiceImpl은 @Component있어 스캔이 되는데, 생성자를 보면 DiscountPolicy discountPolicy 를 받고 있다.

이때 위에서 생성한 2개의 bean 중에 어떤 빈을 선택해야 하는지 알수가 없어 오류가 발생하게 된다!

 

● 추가사항

AutoAppConfigTest ==> AutoAppConfig 클래스

CoreApplicationTests ==> CoreApplication 클래스

위의 두 클래스 모두 @ComponentScan을 사용하고 있다. 그런데 왜 AutoAppConfigTest 만 테스트에 실패하는 것 일까?

 

CoreApplicationTests를 실행하게 되면 사실 차이가 하나 있는데, 스프링 부트가 컴포넌트 스캔을 하기 때문에 AppConfig도 컴포넌트 스캔의 대상이 됩니다. (AutoAppConfigTest의 경우 @Configuration을 제외했기 때문에 포함되지 않았습니다.)

 

결과적으로 여기에 있는 discountPolicy라는 빈도 스프링 컨테이너에 등록됩니다!

 

따라서 총 3개의 빈이 등록됩니다.

- fixDiscountPolicy

- rateDiscountPolicy

- discountPolicy(RateDiscountPolicy)

 

이런 상황에서 @Autowired DiscountPolicy discountPolicy를 실행하게 되면!

바로 @Autowired의 다음 기능인 이름으로 매칭이 적용됩니다.

그래서 AppConfig에 수동으로 등록한 discountPolicy라는 이름의 빈이 주입됩니다.

 

5. @Autowired 필드 명, @Qualifier, @Primary

조회 대상 빈이 2개 이상일 때 해결 방법

@Autowired 필드 명 매칭

@Qualifier => @Qualifier끼리 매칭 => 빈 이름 매칭

@Primary 사용

 

● @Autowired 필드 명 매칭

타입매칭했을때 빈이 여러개가 있으면 파라미터 이름으로 빈 이름을 추가 조회한다.

다음 코드와 같이 기존에 discountPolicy 를 => rateDiscountPolicy로 변경하면 정상적으로 의존성 주입이 완료된다.

@Autowired
private DiscountPolicy discountPolicy

// 변경 후
@Autowired
private DiscountPolicy rateDiscountPolicy
 

 

● @Qualifier

이 방식은 추가 구분자를 더하는 방식이다. 추가적인 방법을 제공하는 것 이지, 빈 이름을 바꾸는 것이 아니다!

 

사용하고 싶은 빈에 우선 @Qualifier를 지정해 주고

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

이후 사용하는 곳에서도 @Qualifier를 지정해주면 된다.

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

● @Primary

마지막 방식은 @Primary 를 붙여주는 방식이다. 가장 간단하고방식이라 할 수 있다.

@Autowired시 여러 빈이 매칭되면 @Primary 가 붙어있는 빈이 우선권을 갖게된다. 다음과 같이 사용하면 된다.

@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy {}
 

 

6. 애노테이션 직접 만들기

위에서 사용한 @Qualifier("mainDiscountPolicy") 과 같이 문자를 적으면 컴파일시 타입 체크가 안된다.

예를 들어 @Qualifier("maimDiscountPolicy") 과 같이 사용해도 컴파일 오류가 발생하지 않는다!

 

다음과 같은 애노테이션을 만들어서 문제를 해결할 수 있다.

package hello.core.annotation;

import org.springframework.beans.factory.annotation.Qualifier;

import java.lang.annotation.*;

@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Qualifier("mainDiscountPolicy")
public @interface MainDiscountPolicy {
}
 

이후 다음과 같이 적용하여 사용하면 된다.

@Component
@MainDiscountPolicy
public class RateDiscountPolicy implements DiscountPolicy {}
 
// ...

@Component
public class OrderServiceImpl implements OrderService{

    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

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

    // ...
}
 

위에서도 한번 언급한적이 있는데, 애노테이션에는 상속이라는 개념이 없다.

이렇게 여러 애노테이션을 모아서 사용하는 기능은 스프링이 지원해주는 기능이다.

@Qulifier 뿐만 아니라 다른 애노테이션들도 함께 조합해서 사용할 수 있다.

 

7. 빈을 모두 조회해야하는 경우, Map, List

스프링 빈이 전부 필요한 경우들이 있다.

클라이언트 입장에서 rate, fix를 선택하여 할일할 수 있다고 해보자. 이는 전략 패턴을 사용하면 되는데, 스프링을 사용하면 매우 간단하다.

 

다음 코드를 살펴보자.

// ... 생략

public class AllBeanTest {

    @Test
    void findAllBean() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class, DiscountService.class);
    }

    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;
            System.out.println("policyMap = " + policyMap);
            System.out.println("policies = " + policies);
        }
    }
}
 

위 코드의 실행 결과는 다음과 같다.

Map의 결과는 키 : 값의 쌍으로 출력되고 있으며, List의 결과로 값들이 출력되고 있다.

 

Map<String, DiscountPolicy> : map의 키에 스프링 빈의 이름을 넣어주고, 그 값으로 DiscountPolicy 타입으로 조회한 모든 스프링 빈을 담아준다.

List<DiscountPolicy> : DiscountPolicy 타입으로 조회한 모든 스프링 빈을 담아준다.

 

만약 해당하는 타입의 스프링 빈이 없으면, 빈 컬렉션이나 Map을 주입한다.

또한 의존성 자동 주입을 해주기 때문에 따로 더 설정할 것은 없다.

 

이를 다음과 같이 할인정책을 선택하는 방식으로 바꿔보자. 테스트 코드를 작성하였다.

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, "userA", 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> policies;

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

정상적으로 테스트가 실행된다.

'BackEnd > Spring' 카테고리의 다른 글

[Spring] 빈 스코프  (0) 2022.02.08
[Spring] 빈 생명주기 콜백  (0) 2022.02.07
[Spring] 컴포넌트 스캔  (0) 2022.02.04
[Spring] 싱글톤 컨테이너  (0) 2022.02.03
[Spring] 스프링 컨테이너와 스프링 빈  (0) 2022.02.01

댓글