[Spring] 의존관계 자동 주입

노유성·2023년 7월 17일
0
post-thumbnail

의존관계 주입

생성자 주입

지금껏 알아본 것처럼 생성자를 통해서 의존관계를 주입받는 방법이다.
생성자 호출 시점에 딱 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개만 있음녀 @Autowired를 생략해도 자동으로 주입된다.

수정자 주입

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

setter라 불리는 필드의 값을 변경하는 수정자를 통해 DI를 하는 방법이다. java bean 규약을 지키는 방법이다.

참고로 @Autowired는 주입을 하려는 대상이 없으면 오류를 발생한다. 그래서 만약에 주입할 대상이 없어도 오류가 발생하지 않게 하려면 @Autowried(required = false)로 지정하면 된다.

필드 주입

@Component
public class OrderServiceImpl implements OrderService{

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

말 그대로 필드에 바로 주입을 하는 방법이다. 코드가 간결하다는 장점이 있지만 변경이 불가능해서 테스트 하기 힘들다는 단점이 있다.

예를 들어서,

@Test
void test() {
    OrderServiceImpl orderService = new OrderServiceImpl();
    orderService.createOrder(1L, "itemA", 10000);
}

다음과 같은 테스트를 진행하려고 한다면

NullPointerException이 발생한다. 그렇다고 해서 private인 필드에 클라이언트가 DI도 할 수 없기 때문에 난감하다. 그래서 필드 주입은 가급적이면 사용하지 않는다.

일반 메소드 주입

한 번에 여러 필드를 주입할 수 있다는 특징이 있다. 하지만 일반적으로 사용하지 않는다. 왜냐면 생성자 주입이 상위호환이기 때문이다.

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

옵선 처리

주입할 스프링 빈이 없어도 동작을 해야하는 순간이 있다. 하지만 @Autowired는 required = ture 이므로 자동 주입할 대상이 없으면 오류를 발생한다. 그래서 자동 주입 대상을 옵션으로 처리하는 방법이 있다.

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

}
// Member는 bean이 아닌데 의존관계를 주입받으려고 하는 상황
static class TestBean {
    @Autowired(required = false) // 의존관계가 없으면 메소드가 호출이 안 됨
    public void setNoBean1(Member noBean1) {
        System.out.println("noBean1 = " + noBean1);
    }

    @Autowired // 호출은 되지만 null이 입력됨
    public void setNoBean2(@Nullable Member noBean2) {
        System.out.println("noBean1 = " + noBean2);
    }

    @Autowired
    public void setNoBean3(Optional<Member> noBean3) {
        System.out.println("noBean3 = " + noBean3);
    }
}

required = false로 지정하면은 주입할 의존 관계가 없으면 메소드가 호출이 되지 않는다. @Nullable 키워드를 사용하면 적절하지 않은 값이 할당되면 null을 할당하고 Optional로 감싸면은 null에 대해서 Optional.empty를 반환한다.

테스트 실행결과는 다음과 같다.

결국엔 생성자 주입

과거에는 수정자, 필드 주입도 사용했지만 요즘은 대부분 생성자 주입을 사용한다.
그 이유는 의존관계는 한 번 주입하면 변경할 일이 거의 없고 오히려 변경을 해서는 안 된다.

수정자 주입을 사용하면 setter를 public으로 열어놓아야하기 때문에 실수로 변경할 수도 있다. 이는 좋은 방법이 아니기에 딱 1번만 호출하고 변경할 일이 거의 없게 설계하기 위해 생성자 주입을 사용한다.

또 실수로 DI를 까먹고 하지 않는 경우도 있을 수 있는데 모든 주입을 통틀어 생성자 주입만이 필드에 final 키워드를 붙힐 수 있기 때문에 대부분 생성자 주입을 이용한다.

롬복

대부분의 개발에 있어서 대부분 다 불변이고 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;
        this.discountPolicy = discountPolicy;
    }

롬복을 이용하면 위와 같이 길고 번거로운 코드도

@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {

    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

이렇게 짧고 간결하게 바꿀 수 있다. @RequiredArgsConstructor annotation은 final로 지정된 필드들을 모아 생성자로 만들어주는 annotaion이다.

이 뿐만 아니라,

@Getter
@Setter
public class HelloLombok {

    private String name;
    private int age;

    public static void main(String[] args) {
        HelloLombok helloLombok = new HelloLombok();
        helloLombok.setName("asdf");

        System.out.println(helloLombok.getName());
    }
}

@Getter, @Setter annotation을 붙히면 모든 필드에 대해서 getter, setter함수를 자동으로 만들어준다. ctrl + F12를 누르면은 현재 focus하고 있는 class의 메소드들을 전부 확인할 수 있는데 위 코드는 getter, setter를 명시하지 않았음에도 불구하고 롬복이

getter, setter를 다 만들어주었음을 알 수 있다.

조회 빈이 2개 이상일 때

@Autowired는 type으로 조회를 해서 의존관계를 주입을 해주기 때문에 type이 2개 이상일 때는 문제가 발생한다.

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

    MemberService memberService = ac.getBean(MemberService.class);

    Assertions.assertThat(memberService).isInstanceOf(MemberService.class);
}

위 테스트에서는 스프링 컨테이너를 부르고 있고

@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {

    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

OrderServiceImpl은 discountPolicy에 의존하고 있다. 그런데 우리가 구현한 구현체는

2개이다 이럴 경우에는 Autowired가 type이 2개이기에 에러를 만든다.

이를 해결할 수 있는 방법이 있다.

Autowired 필드명

@Autowired는 타입을 확인한 이후에 타입이 여러 개이면 그 다음은 필드명을 확인한다. 혹여나 필드명과 빈의 이름이 같다면 해당 빈을 주입한다.

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

위와 같이 생성자로 받는 인자의 이름을 특정 빈의 이름과 같게 만들어주면 테스트를 통과할 수 있다.

@Qualifier

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

위와 같이 빈에 qualifier로 별명을 붙혀주면은 이를 이용해서 DI에 주입을 할 때 어떤 컴포넌트를 주입할 건지 명시할 수 있다.\

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

이렇게 생성자에 명시해주면 특정 qualifier을 선택해서 주입할 수 있고 만약에 지정한 qualifier가 없으면은 그 다음에는 필드명을 확인해서 주입한다.

@Primary

Primary annotaion을 이름에서 직관적으로 알 수 있듯이 특정 빈을 여러 type의 빈들 중에서 기본값으로 설정하는 것이다. 특정 빈에 아래와 같이 Primary annotaion을 지정하면

@Component
@Qualifier("fixDiscountPolicy")
@Primary
public class FixDiscountPolicy implements DiscountPolicy {

위 2가지의 방법을 제외하고도 DI가 가능해진다.

Primary와 Qualifier중에 우선권은 Qualifier에게 있다.

모든 빈 조회

예를 들어 회원에게 3개의 할인 정책을 선택할 수 있는 선택지를 줄 때 3개의 할인정책에 해당하는 3개의 빈들을 보여주기 위해서는 3개의 빈을 모두 조회해야 한다. 어떻게 하면 될까?

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 i, String discountCode) {
        DiscountPolicy discountPolicy = policyMap.get(discountCode);
        return discountPolicy.discount(member, i);
    }
}

위와 같이 DiscountPolicy를 value로 하는 map 객체나 List 객체를 만들어서 자동의존관계 주입을 설정하면 Spring이 알아서 모든 빈에 대한 값들을 넘겨준다.

discount() 메소드 같은 경우에는 특정 정보들과 어떤 할인 정책을 사용할 것인지에 대한 정보를 받아서 key값에 대응하는 빈(구현체)의 할인 정책을 가져와서 할인을 해주는 프로세스를 만들 수 있다.

Custom annotaion

@Qualifier annotation을 이용해서 컴포넌트를 명시하면 문제점이 있다. 혹여나 오타가 발생할 경우 문자열에서 오타가 발생하기 때문에 테스트를 하기 전까지는 오타를 발견할 수 없다는 문제이다. 이런 문제를 해결하기 위해서 custom annotation을 사용한다.


Qualifier에서 사용하는 annotaion을 모두 가져와서

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

}

새로운 anntation을 만든다. Qualifier을 이용해 해당 annotaion을 명시한 후에 사용할 수 있다. 이렇게 annotaion을 만들면

@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 rateDiscountPolicy) {
        
        this.memberRepository = memberRepository;
        this.discountPolicy = rateDiscountPolicy;
    }

이렇게 사용할 수 있다.

profile
풀스택개발자가되고싶습니다:)

4개의 댓글

comment-user-thumbnail
2023년 7월 17일

잘봤습니다. 좋은 글 감사합니다.

1개의 답글
comment-user-thumbnail
2023년 7월 17일

저도 개발자인데 같이 교류 많이 해봐요 ㅎㅎ! 서로 화이팅합시다!

1개의 답글