Programming/Spring

Spring - 의존관계 자동 주입

잇(IT) 2023. 6. 11. 22:30

- 다양한 의존관계 주입 방법

1. 생성자 주입

2. 수정자 주입(setter 주입)

3. 필드 주입

4. 일반 메서드 주입


- 생성자 주입

 

1. 생성자 호출시점에 딱 1번만 호출되는 것이 보장된다.

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

@Component
//@RequiredArgsConstructor // 생성자 주입을 자동으로 생성해준다. final이 가진 것을 파라미터로 받는 생성자를 만들어준다.
public class OrderServiceImpl implements OrderService {

    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

    @Autowired
    public OrderServiceImpl(MemberRepository memberRepository, @MainDiscountPolicy DiscountPolicy discountPolicy) {
//        System.out.println("memberRepository = " + memberRepository);
//        System.out.println("discountPolicy = " + discountPolicy);
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }

- private final 로 변수가 주어지면 반드시 초기화 해야한다.

- 생성자가 딱 1개만 있으면 @Autowired를 생략해도 자동 주입 된다. 물론 스프링 빈에만 해당한다


- 수정자 주입(setter 주입)

 

1. setter라 불리는 필드의 값을 변경하는 수정자 메서드를 통해서 의존관계를 주입하는 방법이다.

2. 선택, 변경 가능성이 있는 의존관계에 사용

3. 자바빈 프로퍼티 규약의 수정자 메서드 방식을 사용하는 방법이다.

@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. 필드에 바로 주입하는 방법이다.

2. 사용하지 않는 것이 일반적으로 좋다.

3. 애플리케이션의 실제 코드와 관계 없는 테스트 코드

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

 

- 순수한 자바 테스트 코드에는 당연히 @Autowired가 동작하지 않는다. @SpringBootTest 처럼 스프링 컨테이너를 테스트에 통합한 경우에만 가능하다.


- 일반 메서드 주입

 

1. 일반 메서드를 통해서 주입 받을 수 있다.

2. 한번에 여러 필드를 주입 받을 수 있다.

3. 일반적으로 잘 사용하지 않는다.

 

- 의존관계 자동 주입은 스프링 컨테이너가 관리하는 스프링 빈이어야 동작한다. 스프링 빈이 아닌 Member 같은 클래스에서 @Autowired 코드를 적용해도 아무 기능도 동작하지 않는다.


- 옵션 처리

 

- 자동 주입 대상을 옵션으로 처리하는 방법은 다음과 같다.

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

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

3. Optional<> : 자동 주입할 대상이 없으면 Optional.empty 가 입력된다

//호출 안됨
@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);
}

- 현재 Member는 스프링 빈이 아니다.

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

setNoBean2 = null
setNoBean3 = Optional.empty

- 출력 결과는 위와 같다.

 

-@Nullable, Optional은 스프링 전반에 걸쳐서 지원된다. 예를 들어서 생성자 자동 주입에서 특정 필드에만 사용해도 된다.

 

* 결론적으로 생성자 주입을 사용하는 것이 좋다.

 

가끔 옵션이 필요하면 수정자 주입을 선택한다. 


- 롬복

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

- 롬복 라이브러리가 제공하는 @RequiredArgsConstructor 기능을 사용하면 final이 붙은 필드를 모아서 생성자를 자동으로 만들어준다. (다음 코드에는 보이지 않지만 실제 호출 가능하다.)


- 조회 빈이 2개 이상 - 문제

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

@Autowired
private DiscountPolicy discountPolicy

- 타입으로 조회하기 때문에, 마치 다음 코드와 유사하게 동작한다. (실제로는 더 많은 기능을 제공한다.)

ac.getBean(DiscountPolicy.class)

 

@Component
public class FixDiscountPolicy implements DiscountPolicy {}

@Component
public class RateDiscountPolicy implements DiscountPolicy {}

- 위처럼 두개의 클래스를 스프링 빈으로 등록하게 되면 NoUniqueBeanDefinitionException오류가 발생한다.


- @Autowired 필드 명, @Qualifier, @Primary

 

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

1. @Autowired 필드 명 매칭

2. @Qualifier @Qualifier끼리 매칭 -> 빈 이름 매칭

3. @Primary 사용


- @Autowired 필드 명 매칭

@Autowired
private DiscountPolicy discountPolicy

필드 명을 빈 이름으로 변경

@Autowired
private DiscountPolicy rateDiscountPolicy

- 필드 명이 rateDiscountPolicy 이므로 정상 주입된다. 필드 명 매칭은 먼저 타입 매칭을 시도 하고 그 결과에 여러 빈이 있을 때 추가로 동작하는 기능이다.

 

- @Autowired 매칭 정리

1. 타입 매칭

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


- @Qualifier 사용

1. @Qualifier 는 추가 구분자를 붙여주는 방법이다. 주입시 추가적인 방법을 제공하는 것이지 빈 이름을 변경하는 것은 아니다.

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

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

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

 

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

- 다음과 같이 직접 빈 등록시에도 @Qualifier를 동일하게 사용할 수 있다

 

- @Qualifier 정리

1. @Qualifier끼리 매칭

2. 빈 이름 매칭

3. NoSuchBeanDefinitionException 예외 발생

 


- @Primary 사용

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

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

@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy {}

@Component
public class FixDiscountPolicy implements DiscountPolicy {}

- rateDiscountPolicy가 우선권을 가진다.

 

- @Qualifier 의 단점은 주입 받을 때 다음과 같이 모든 코드에 @Qualifier 를 붙여주어야 한다는 점이다.

- 반면에 @Primary 를 사용하면 이렇게 @Qualifier 를 붙일 필요가 없다.

 

- 우선순위

1. @Primary 는 기본값 처럼 동작하는 것이고, @Qualifier 는 매우 상세하게 동작한다. 이런 경우 어떤 것이 우선권을 가져갈까? 스프링은 자동보다는 수동이, 넒은 범위의 선택권 보다는 좁은 범위의 선택권이 우선 순위가 높다. 따라서 여기서도 @Qualifier 가 우선권이 높다.


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

1. 의도적으로 정말 해당 타입의 스프링 빈이 다 필요한 경우도 있다.

2. 예를 들어서 할인 서비스를 제공하는데, 클라이언트가 할인의 종류(rate, fix)를 선택할 수 있다고 가정해보자. 스프링을 사용하면 소위 말하는 전략 패턴을 매우 간단하게 구현할 수 있다.

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 fixDiscountPrice = discountService.discount(member, 10000, "fixDiscountPolicy");

        assertThat(discountService).isInstanceOf(DiscountService.class);
        assertThat(fixDiscountPrice).isEqualTo(1000);

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

    }

    static class DiscountService {
        private final Map<String, DiscountPolicy> policyMap;
        // DiscountPolicy 인터페이를 구현한 클래스가 전부 객체로 들록된다
        // key = 빈 이름, value = 구현 클래스
        private final List<DiscountPolicy> policies;

        @Autowired
        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) {
        // discountCode에 들어오는 값에 따라 discountPolicy의 객체가 선택되고 해당 클래스의 discount 메서드가 실행된다.
            DiscountPolicy discountPolicy = policyMap.get(discountCode);
            return discountPolicy.discount(member, price);
        }
    }
}

- 하지만 해당 방법은 객체가 어떤 것들이 존재하는지 한 번에 파악하기 힘들기 때문에 명시적으로 하나씩 표현하는 것이 좋다.

 

 

 

 

 

 

 

 

 

 

 

 

출처 : 인프런 - 우아한 형제들 기술이사 김영한의 스프링 완전 정복 (스프링 핵심원리 - 기본 편)

728x90

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

Spring - 빈 스코프  (0) 2023.06.11
Spring - 빈 생명주기 콜백  (0) 2023.06.11
Spring - 컴포넌트 스캔  (0) 2023.06.10
Spring - 싱글톤 컨테이너  (0) 2023.06.10
Spring - 스프링 컨테이너와 스프링 빈  (1) 2023.06.10