Back-end

07. 의존 관계의 주입

-운- 2023. 11. 5. 22:10

의존 관게 주입의 종류

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

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 생략 가능.(스프링 빈에서만)

 

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

 

3. 필드 주입

 

필드에 바로 주입하는 방법

외부에서 변경 불가능, 테스트하기 어려움.

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

그냥 사용하지 말자.

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

 

4. 일반 메서드 주입

 

일반 메서드를 통해 주입

한번에 여러 필드 주입받을 수 있느나 잘 사용 X

@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만 사용하면 오류가 발생,

  • @Autowired(required=false) : 자동 주입할 대상이 없으면 수정자 메서드 자체가 호출 안됨
  • org.springframework.lang.@Nullable : 자동 주입할 대상이 없으면 null이 입력된다.
  • Optional<> : 자동 주입할 대상이 없으면 Optional.empty 가 입력된다.
//호출 X
@Autowired(required = false)
public void setNoBean1(Member member) {
        System.out.println("setNoBean1 = " + member);
        }
//null 호출
@Autowired
public void setNoBean2(@Nullable Member member) {
        System.out.println("setNoBean2 = " + member);
        }
//Optional.empty 호출
@Autowired(required = false)
public void setNoBean3(Optional<Member> member) {
        System.out.println("setNoBean3 = " + member);
        }

setNoBean2 = null

setNoBean3 = Optional.empty

setNoBean1() 은 @Autowired(required=false) 이므로 호출 자체가 안된다.

 

생성자 주입을 사용하자.

수정자 의존관계일 때

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;
    }
    //...
}
@Test
void createOrder() {
        OrderServiceImpl orderService = new OrderServiceImpl();
        orderService.createOrder(1L, "itemA", 10000);
        }

NPE(Null Point Exception)이 발생하는데, memberRepository, discountPolicy 모두 의존관계 주입이 누락되었기 때문

생성자 주입을 사용하면 컴파일 오류가 발생하고 IDE에서 어떤 값을 주입해야하는지 알 수 있다.

 

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

필수인 DiscountPolicy 값 설정을 안한 것을 볼 수 있는데, 컴파일 시점에서 오류를 발생시킨다.

컴파일 오류가 세상에서 제일 좋은 오류임을 기억하자.

 

롬복과 최신 트렌드

저 위에 DiscountPolicy 값을 설정했다고 했을 때 이를 최적화해보자.

생성자가 1개이면 Autowired를 생략할 수 있다.

따라서 코드는

@Component
public class OrderServiceImpl implements OrderService {
    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;
    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy
            discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }
}

여기서 이제 롬복을 적용하자.

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

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

이게 최종이다. 간결하다.

. 롬복이 자바의 애노테이션 프로세서라는 기능을 이용해서 컴파일 시점에 생성자 코드를 자동으로 생성해준다. 실제 class 를 열어보면 다음 코드가 추가되어 있는 것을 확인할 수 있다.

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

 

최근엔 생성자 1개로하고 Lombok 라이브러리를 추가하는 방법을 사용해 깔끔한 코드를 쓴다.

 

조회 빈이 2개 이상이면?

@Autowired 는 타입(Type)으로 조회한다.

스프링 빈 조회에서 학습했듯이 타입으로 조회하면 선택된 빈이 2개 이상일 때 문제가 발생한다.

DiscountPolicy 의 하위 타입인 FixDiscountPolicy , RateDiscountPolicy 둘다 @Component를 통해 스프링 빈으로 선언해 보고

@Autowired
private DiscountPolicy discountPolicy

로 의존관계 주입을 실행하면

NoUniqueBeanDefinitionException 오류가 발생한다.

말 그대로 하나의 빈이 아닌 2개가 발견되었다는 것으로, 이를 어떻게 해결하는지 차근차근 알아보자.

 

 

1. @Autowired 필드 명 매칭

@Autowired
private DiscountPolicy rateDiscountPolicy

 

필드 명이 rateDiscountPolicy 이므로 정상 주입된다.

 

@Autowired 매칭 정리

1) 타입 매칭

2) 타입 매칭의 결과가 2개 이상일 때 필드 명, 파라미터 명으로 빈 이름 매칭

 

2. @Qualifier

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

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

 

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

 

생성자 자동 주입 예시

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

 

수정자 자동 주입을 사용할 때도 동일하게 하면 된다.

@Bean을 사용할 때도 동일하게 하면 된다.

@Bean
@Qualifier("mainDiscountPolicy")
public DiscountPolicy discountPolicy() {
	return new ...
}

 

3. @Primary

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

@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy {}

@Component
public class FixDiscountPolicy implements DiscountPolicy {}

 

이렇게 사용하면 RateDiscountPolicy에 우선권이 있는 것이다.

이렇게 하고 실행하면 별 문제 없이 동작한다.

 

 

애노테이션 직접 만들기

 

@Qualifier("mainDiscountPolicy") 이렇게 문자를 적으면 컴파일시 타입 체크가 안된다. 다음과 같은 애노테이션을 만들어서 문제를 해결할 수 있다

package hello.core.annotataion;
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)
@Documented
@Qualifier("mainDiscountPolicy")
public @interface MainDiscountPolicy {
}

 

@Component
@MainDiscountPolicy
public class RateDiscountPolicy implements DiscountPolicy {} ```
```java
//생성자 자동 주입
@Autowired
public OrderServiceImpl(MemberRepository memberRepository,
 @MainDiscountPolicy DiscountPolicy discountPolicy) {
 this.memberRepository = memberRepository;
 this.discountPolicy = discountPolicy;
}
//수정자 자동 주입
@Autowired
public DiscountPolicy setDiscountPolicy(@MainDiscountPolicy DiscountPolicy
discountPolicy) {
 this.discountPolicy = discountPolicy;
}

이렇게 사용하면 @Qualifier를 @MainDiscountPolicy로 사용하여 타입 체크를 하도록 할 수 있다.

 

 

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

해당 타입의 스프링 빈이 다 필요한 경우

ex) 할인 서비스를 제공하는데, 클라이언트가 할인의 종류(rate, fix)를 선택할 수 있다고 가정해보자.

스프링을 사용하면 소위 말하는 전략 패턴을 매우 간단하게 구현할 수 있다

 

(사실 List, Map은 이해가 조금 되지 않지만 이해한 바로 최대한 써보자.)

package hello.core.autowired;
import hello.core.AutoAppConfig;
import hello.core.discount.DiscountPolicy;
import hello.core.member.Grade;
import hello.core.member.Member;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import
        org.springframework.context.annotation.AnnotationConfigApplicationContext;
import java.util.List;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
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;
        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);
            System.out.println("discountCode = " + discountCode);
            System.out.println("discountPolicy = " + discountPolicy);
            return discountPolicy.discount(member, price);
        }
    }
}

DiscountService는 Map으로 모든 DiscountPolicy 를 주입받는다.

( fixDiscountPolicy , rateDiscountPolicy 주입)

discount () :  discountCode로 "fixDiscountPolicy"가 넘어오면 map에서 fixDiscountPolicy 스프링 빈을 찾아서 실행 “rateDiscountPolicy”가 넘어오면 rateDiscountPolicy 스프링 빈을 찾아서 실행

 

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

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

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

'Back-end' 카테고리의 다른 글

09. 빈 스코프  (1) 2023.11.14
08. 빈 생명주기 콜백  (0) 2023.11.05
06. 컴포넌트 스캔  (1) 2023.10.31
05. 싱글톤 컨테이너  (0) 2023.10.31
04. 스프링 컨테이너와 스프링 빈  (1) 2023.10.07