[스프링 핵심 원리 - 기본편] 7. 의존관계 자동 주입

HJ·2022년 7월 31일
0

김영한 님의 스프링 핵심 원리 - 기본편 강의를 보고 작성한 내용입니다.
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8/dashboard


1. 의존관계 주입 4가지 방법

1-1. 생성자 주입

@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;
    }
}
  • 생성자 호출시점에 딱 1번만 호출되는 것이 보장된다

  • 불변, 필수 의존관계에 사용

  • 생성자가 딱 1개만 있으면 @Autowired를 생략해도 자동 주입 된다

  • 스프링 빈을 등록하면서 자동으로 의존관계까지 주입됨


1-2. 수정자 주입 (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) 로 지정


1-3. 필드 주입

@Component
public class OrderServiceImpl implements OrderService {

    @Autowired
    private MemberRepository memberRepository;

    @Autowired
    private DiscountPolicy discountPolicy;
}
  • 필드에 바로 주입하는 방법

  • 외부에서 변경이 불가능해서 테스트 하기 힘들다는 치명적인 단점이 있다

  • DI 프레임워크가 없으면 아무것도 할 수 없다

  • 사용하지 말자!

    • 애플리케이션의 실제 코드와 관계 없는 테스트 코드에서는 사용

    • 스프링 설정을 목적으로 하는 @Configuration 같은 곳에서만 특별한 용도로 사용


1-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;
    }
}
  • 한번에 여러 필드를 주입 받을 수 있지만 잘 사용하지 않는다



2. 옵션처리 3가지 방법

  • 일반적으로 의존관계 자동 주입은 스프링 컨테이너가 관리하는 스프링 빈이어야 동작한다

  • 하지만 주입할 스프링 빈이 없어도 동작해야 하는 경우가 존재

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


public class AutowiredTest {

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

    }

    static class TestBean {

        @Autowired(required = false)
        // Member는 스프링 컨테이너에 없음
        public void setNoBean1(Member noBean1) {
            System.out.println("noBean1 = " + noBean1);
        }

        @Autowired
        // Member는 스프링 컨테이너에 없음
        public void setNoBean2(@Nullable Member noBean2) {
            System.out.println("noBean2 = " + noBean2);
        }

        @Autowired
        // Member는 스프링 컨테이너에 없음
        public void setNoBean3(Optional<Member> noBean3) {
            System.out.println("noBean3 = " + noBean3);
        }
    }
}

  • @Autowired(required=false) : 자동 주입할 대상이 없으면 수정자 메소드 자체가 호출 안됨

    • 호출이 되지 않기 때문에 결과가 출력되지 않음
  • @Nullable : 자동 주입할 대상이 없으면 null이 입력된다
  • Optional<> : 자동 주입할 대상이 없으면 Optional.empty 가 입력된다



3. 생성자 주입을 사용하는 3가지 이유

3-1. 불변

  • 대부분의 의존관계 주입은 한번 일어나면 애플리케이션 종료시점까지 의존관계를 변경할 일이 없다

  • 오히려 대부분의 의존관계는 애플리케이션 종료 전까지 변하면 안된다

  • 수정자 주입을 사용하면, setXxx 메서드를 public으로 열어두어야 한다

  • 누군가 실수로 변경할 수도 있고, 변경하면 안되는 메서드를 열어두는 것은 좋은 설계 방법이 아니다

  • 생성자 주입은 객체를 생성할 때 딱 1번만 호출되므로 이후에 호출되는 일이 없다. 따라서 불변하게 설계할 수 있다


3-2. 누락

class OrderServiceImplTest {

    @Test
    void createOrder() {

        MemoryMemberRepository memberRepository = new MemoryMemberRepository();
        memberRepository.save(new Member(1L, "name", Grade.VIP));

        OrderServiceImpl orderService = new OrderServiceImpl(memberRepository, new FixDiscountPolicy());
        Order order = orderService.createOrder(1L, "itemA", 10000);
        Assertions.assertThat(order.getDiscountPrice()).isEqualTo(1000);
    }
}
  • 구현 객체 자체를 순수 자바 코드로 단위 테스트를 진행하는 경우 임의로 객체를 넣어서 테스트할 수 있음

  • 생성자 주입을 사용하면 주입 데이터를 누락 했을 때 컴파일 오류가 발생하고 IDE에서 바로 어떤 값을 필수로 주입해야 하는지 알 수 있다


3-3. final 키워드 사용 가능

  • final 키워드를 사용하면 생성자에서 값이 설정되지 않는 오류를 컴파일 시점에 막아준다

  • 수정자 주입을 포함한 나머지 주입 방식은 모두 생성자 이후에 호출되므로, 필드에 final 키워드를 사용할 수 없다. 오직 생성자 주입 방식만 final 키워드를 사용할 수 있다




4. 조회 빈이 2개 이상인 경우

  • @Autowired는 타입으로 조회한다

  • @Autowired private DiscountPolicy discountPolicyac.getBean(DiscountPolicy.class) 는 유사하게 동작

  • DiscountPolicy의 하위 타입은 FixDiscountPolicy , RateDiscountPolicy 가 있기 때문에 둘 다 스프링 빈으로 등록하고 의존관계 자동 주입을 실행하면 NoUniqueBeanDefinitionException 오류가 발생




5. 조회 빈이 2개 이상일 때 자동 주입 해결 방법

5-1. @Autowired 필드명 매칭

  • @Autowired 는 타입 매칭을 시도하고, 이때 여러 빈이 있으면 필드 이름, 파라미터 이름으로 빈 이름을 추가 매칭한다

@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }
  • FixDiscountPolicy , RateDiscountPolicy 가 둘 다 스프링 빈으로 등록되어 있는 경우 위의 코드를 실행하면

  • NoUniqueBeanDefinitionException 오류 발생

  • expected single matching bean but found 2: fixDiscountPolicy,rateDiscountPolicy


@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy rateDiscountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = rateDiscountPolicy;
    }
  • 생성자 파라미터의 이름을 discountPolicy ➜ rateDiscountPolicy

  • 파라미터 이름이 rateDiscountPolicy 이므로 정상 주입

  • 필드 명 매칭은 먼저 타입 매칭을 시도 하고 그 결과가 2개 이상일 때, 추가로 동작하는 기능이다


5-2. @Qualifier 추가 구분자 설정

  • @Qualifier 는 추가 구분자를 붙여주는 방법이다

  • 주입 시 추가적인 방법을 제공하는 것이지 빈 이름을 변경하는 것은 아니다

  • @Primary 보다 우선순위가 더 높음


@Component
@Qualifier("mainDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy { }
  • 스프링 빈 등록 시 @Qualifier 로 추가 구분자를 지정해준다

  • FixDiscountPolicy에는 @Qualifier("fixDiscountPolicy) 사용


public OrderServiceImpl(MemberRepository memberRepository,
                            @Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }
  • 의존관계 주입 시, @Qualifier를 붙여주고 등록한 구분자를 적어준다

  • @Qualifier 로 주입할 때 @Qualifier("mainDiscountPolicy") 를 못찾으면 mainDiscountPolicy라는 이름의 스프링 빈을 추가로 찾는다


5-3. @Primary 우선순위 정하기

  • @Primary 는 우선순위를 정하는 방법이다

  • @Autowired 시에 여러 빈이 매칭되면 @Primary 가 우선권을 가진다


@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy { }
  • 스프링 빈이 2개 이상 조회될 때, RateDiscountPolicy 가 주입되도록 우선순위 지정

public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }
  • 의존관계 주입 시, @Primary가 붙은 RateDiscountPolicy가 주입된다



6. 어노테이션 만들기

  • @Qualifier("mainDiscountPolicy") 처럼 문자를 적으면 컴파일 시 타입 체크가 안되기 때문에 어노테이션을 직접 만들어서 문제를 해결한다

  • 어노테이션에는 상속이라는 개념이 없다

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

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


6-1. 어노테이션 생성

@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Qualifier("mainDiscountPolicy")
public @interface MainDiscountPolicy {
}
  • Target : 어노테이션을 어디에 붙일 수 있는지 지정

6-2. 어노테이션 사용

@Component
@MainDiscountPolicy
public class RateDiscountPolicy implements DiscountPolicy { }

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



7. 조회한 빈이 모두 필요한 경우 ( List, Map )

7-1. 전체 코드

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

        int rateDiscountPrice = discountService.discount(member, 20000, "rateDiscountPolicy");
        assertThat(rateDiscountPrice).isEqualTo(2000);
    }

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

7-2. 코드 설명

ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class, DiscountService.class);
  • new AnnotationConfigApplicationContext() 를 통해 스프링 컨테이너를 생성
  • 스프링 컨테이너는 생성자에 클래스 정보를 받는다. 여기에 클래스 정보를 넘기면 해당 클래스가 스프링 빈으로 자동 등록
  • 위의 코드에서는 AutoAppConfig.class , DiscountService.class 를 파라미터로 넘기면서 해당 클래스를 자동으로 스프링 빈으로 등록

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<String, DiscountPolicy> : map의 키에 스프링 빈의 이름을 넣어주고, 그 값으로 DiscountPolicy 타입으로 조회한 스프링 빈을 담아준다

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

  • 담겨지는 스프링 빈은 fixDiscountPolicy , rateDiscountPolicy


DiscountService discountService = ac.getBean(DiscountService.class);
  • DiscountService는 Map으로 모든 DiscountPolicy 를 주입받는다. 이때 fixDiscountPolicy ,rateDiscountPolicy 가 주입된다

int discountPrice = discountService.discount(member, 10000, "fixDiscountPolicy");

public int discount(Member member, int price, String discountCode) {
            DiscountPolicy discountPolicy = policyMap.get(discountCode);
            return discountPolicy.discount(member, price);
        }
  • discount() 메서드는 discountCode로 "fixDiscountPolicy"가 넘어오면 map에서 fixDiscountPolicy 스프링 빈을 찾아서 실행한다
  • “rateDiscountPolicy”가 넘어오면 rateDiscountPolicy 스프링 빈을 찾아서 실행



참고>

  • 구현 객체로 이동하기 : Ctrl + Alt + B

  • @RequiredArgsConstructor : final이 붙은 필드들을 parameter로 갖는 생성자를 만들어준다

0개의 댓글