[Spring] 의존관계 주입 방법

Ho·2022년 7월 21일
0

Spring

목록 보기
6/8

의존관계 주입 방법

의존관계 주입은 다음 네가지 방법으로 가능하다.

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

생성자 주입

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

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

특징

  • 생성자 호출 시점에 한번만 호출되는 것을 보장한다.
  • 불변, 필수 의존관계에 사용
  • 생성자가 한 개만 존재하면 @Autowired 생략 가능

수정자 주입(setter 주입)

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

특징

  • 선택, 변경 가능성이 있는 의존관계에 사용
  • 자바 빈 프로퍼티 규약의 수정자 메서드 방식을 사용

필드 주입

필드에 바로 주입하는 방법

@Component
public class OrderServiceImpl implements OrderService {

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

}

특징

  • 외부에서 변경이 불가능하므로 DI프레임워크 없이 사용할 수 없다.
  • 일반적으로 잘 사용하지 않는다.
  • 테스트코드, @Configuration 같은 곳에서 특별한 용도로만 사용
    ex) Configuration에서 수동 빈 등록 시, 자동 등록 빈을 필드로 가져와서 의존관계를 세팅할때 사용

일반 메서드 주입

일반 메서드를 통해 주입받는다.

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

특징

  • 한번에 여러 필드를 주입받을 수 있다.
  • 생성자주입, setter주입 등의 방법으로 대체가 가능하므로 일반적으로 잘 사용하지 않는다.

참고>

  • 의존관계 자동 주입은 스프링 컨테이너가 관리하는 스프링 빈이어야 동작한다.
    일반 클래스에서는 @Autowired가 동작하지 않는다.

옵션 처리

주입 받을 객체 타입의 스프링빈이 존재하지 않아도 동작해야하는 경우가 있다.
@Autowired만 사용한다면 기본 값은 @Autowired(required=true) 이므로 오류가 발생한다.

자동 주입 대상을 옵션으로 처리하는 방법

  • @Autowired(required=false) : 주입받을 스프링빈이 없으면 수정자 메서드 자체를 호출하지 않음
  • @Nullable : 주입 받을 스프링 빈이 없으면 null을 주입
  • Optional<> : 주입 받을 스프링 빈이 없으면 Optional.empty 를 주입
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("noBean2 = " + noBean3);
    }
}

TestBean을 스프링 빈으로 등록할 때 의존관계 주입을 위해 @Autowired를 호출한다.

출력 결과를 보면 Memeber 클래스는 스프링 빈으로 등록되지 않았기 때문에

  • setNoBean1() : @Autowired(required=false) 이므로 호출 x
  • setNoBean2() : 파라미터에 @Nullable을 사용하면 주입받을 스프링 빈이 없는 경우 null이 들어온다.
  • setNoBean3() : 파라미터 타입이 Optional이면 주입받을 스프링 빈이 없는 경우 Optional.empty가 들어온다.

생성자 주입의 장점

스프링을 포함한 DI 프레임워크 대부분이 생성자 주입을 권장한다.

불변

  • 대부분의 의존관계 주입은 한번 일어나면 애플리케이션 종료 시점까지 의존관계를 변경할 일이 없다.
  • 수정자 주입을 사용하면 setter 메서드가 public이므로 변경 가능성이 있다.
  • 생성자 주입은 생성시에 한 번만 호출되므로 변경 가능성이 없다.

final

생성자 주입을 사용하면 필드에 fianl 을 사용할 수 있다.

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

final을 사용하면 생성자에서 초기화 하는 부분이 없는 경우 오류를 컴파일 단계에서 알 수 있다. 또한 이후에 필드를 변경할 수 없다.

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

정리

  • 생성자 주입 방식은 프레임워크에 의존하지 않고 자바 언어의 특징을 잘 살리는 방법이다.
  • 기본적으로 생성자 주입을 사용하고 필수 값이 아닌 경우에 수정자 주입을 옵션으로 부여하면 된다.
  • 필드 주입은 사용하지 않는게 좋다.

@Autowired("필드명"), @Qualifier, @Primary

@Autowired는 의존 관계를 주입할 스프링 빈을 타입으로 조회한다. 스프링 빈을 조회할 때 같은 타입의 빈이 두 개 이상일 때 NoUniqueBeanDefinitionException 오류가 발생한다.

해결 방법

  • @Autowired 필드명 매칭
  • @Qualifier 사용
  • @Primary 사용

@Autowired 필드명 매칭

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

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

타입으로 조회한 스프링빈이 두개인 경우 파라미터의 이름과 일치하는 이름의 스프링 빈을 주입한다.

@Qualifier

@Qualifier 는 추가 구분자를 붙여주는 방법이다. 빈 이름을 바꾸는 것은 아니다.

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

}

빈 등록시에 @Qualify로 이름을 부여한다.

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

주입할 때에도 같은 이름의 @Qualifier를 파라미터에 지정하여 조회한 스프링 빈이 여러개일 때 한가지를 매칭할 수 있다.

같은 이름의 @Qualifier가 사용된 스프링빈 매칭에 실패하면 해당 이름을 가진 스프링 빈을 추가로 찾는다.

다음과 같이 애노테이션을 직접 만들어서 사용할 수도 있다.

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

이렇게 여러 애노테이션을 모아서 새로운 애노테이션을 만들어 사용하는 기능은 스프링이 지원하는 기능이다.

@Primary

@Primary는 우선순위를 정하는 방법이다. @Autowired시에 여러 빈이 매칭되면 @Primary가 우선권을 가진다.

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

같은 타입의 스프링 빈이 두개 등록되고 생성자에서 주입하는 경우

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

@Primary가 사용된 빈이 주입된다.

@Primary 는 아무것도 지정되지 않은 필드나 생성자 파라미터에 주입할 스프링 빈이 여러개인 경우 우선적으로 @Primary 가 주입되는 것이다.
@Primary 가 사용된 빈이 있고 @Qualifier사용된 빈이 있을 때
필드나 생성자에서 @Qualifier를 지정하면 @Qualifier가 우선권을 가진다.


List, Map 을 활용한 의존관계 주입

어떤 타입의 스프링빈을 모두 조회하여 사용해야하는 경우가 있다.
예를들어 할인 서비스를 제공하는데 클라이언트가 그 종류를 선택하는 경우
스프링을 사용하면 전략 패턴(Strategy Pattern)을 매우 간단하게 구현할 수 있다.

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

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

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

DiscountService 는 스프링 빈으로 등록될 때

  • Map에는 DiscountPolicy 타입의 모든 스프링 빈을 <스프링빈 이름, 스프링빈 인스턴스> 를 <key,value>로 가지는 Map을 주입받는다.
  • List에는 해당 타입의 모든 스프링빈이 List로 주입된다.

DiscountServicediscount메서드를 활용하면 클라이언트 코드에서 어떤 할인 정책의 스프링 빈 객체를 사용할지를 정할 수 있다.

이렇게 동적으로 객체의 행위를 정하여 행위를 유연하게 확장할 수 있도록 하는 것을 전략 패턴이라고 한다.

0개의 댓글