관리 메뉴

제뉴어리의 모든것

[스프링 핵심 원리] 의존관계 자동 주입 본문

코드스테이츠

[스프링 핵심 원리] 의존관계 자동 주입

제뉴어리맨 2022. 8. 14. 01:52

의존관계 자동 주입 종류

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

 

생성자 주입

  • 이름 그대로 생성자를 통해서 의존 관계를 주입 받는 방법이다.
  • 특징 생성자 호출시점에 딱 1번만 호출되는 것이 보장된다. 불변, 필수 의존관계에 사용.
    불변, 필수란 final과 같은 키워드를 사용하는 멤버필드를 말하며,
    애플리케이션이 작동하는중에 변할리가 없는 객체들을 말한다.

  • 기본 코드
@Component
public class OrderServiceImpl implements OrderService {

 private final MemberRepository memberRepository;
 private final DiscountPolicy discountPolicy;
 
 @Autowired //의존성 자동 주입, 하지만 현재 생성자가 1개이므로 생략 가능
 public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) 
 {
 	this.memberRepository = memberRepository;
 	this.discountPolicy = discountPolicy;
 }
}

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

 

 

수정자 주입(setter 주입)

  • setter라 불리는 필드의 값을 변경하는 수정자 메서드를 통해서 의존관계를 주입하는 방법이다.
  • 특징
    - 선택, 변경 가능성이 있는 의존관계에 사용.
    - 자바빈 프로퍼티 규약의 수정자 메서드 방식을 사용하는 방법이다.
    프로퍼티 규약이라는게, 그냥 멤버변수를 public으로 두지 않고 private으로 두고 get~, set~ 라는 식으로 메소드로 해당 멤버변수를 접근하도록 하는 패턴을 말한다.

    - 수정자라고 해서 의존성주입이 나중에 되거나 그런것이 아니다. @Autowired가 붙어 있기 때문에 따로 옵션을 주지 않는한 생성자 의존성 주입과 똑같이 그냥 빈 생성하고, 의존성 주입단계에서 똑같이 주입된다.

    + 생성자 주입과 수정자 주입 차이
    생성자 주입은 생성자로 주입하는것이기에 당연히 빈생성시 말고는 호출할 수가 없다.
    그러나 수정자는 코드상에서 호출하여 주입이 가능하다.

  • 기본 코드
package hello.core.scan.basic;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;

@ComponentScan
public class BasicTest {

    @Test
    @DisplayName("컴포넌트 스캔 테스트")
    void TestComponentScan() {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(BasicTest.class);

        String[] beanDefinitionNames = ac.getBeanDefinitionNames(); 
        for (String beanDefinitionName : beanDefinitionNames) { //등록 된 빈 출력
            BeanDefinition beanDefinition = ac.getBeanDefinition(beanDefinitionName);

            if (beanDefinition.getRole() == BeanDefinition.ROLE_APPLICATION) {
                System.out.println("등록된 빈 : "+ ac.getBean(beanDefinitionName));
            }
        }

        BeanInstance beanInstance = ac.getBean(BeanInstance.class);
        beanInstance.diInstanceLogic(); //의존성 주입된 객체의 로직 실행

        System.out.println("setter 의존성 주입");

        beanInstance.setDiInstance2(ac.getBean(DIInstance2.class)); //수정자 의존성 주입, new DIInstance2() 도 가능하다
        beanInstance.diInstanceLogic(); //의존성 주입된 객체의 로직 실행


	beanDefinitionNames = ac.getBeanDefinitionNames(); 
        for (String beanDefinitionName : beanDefinitionNames) { //등록 된 빈 출력
            BeanDefinition beanDefinition = ac.getBeanDefinition(beanDefinitionName);

            if (beanDefinition.getRole() == BeanDefinition.ROLE_APPLICATION) {
                System.out.println("등록된 빈 : "+ ac.getBean(beanDefinitionName));
            }
        }

    }
}

@Component
class BeanInstance{

    DIInterface diInterface;

    @Autowired
    public void setDiInstance2(DIInterface diInterface) {
        this.diInterface = diInterface;
    }

    public void diInstanceLogic() {
        diInterface.logic();
    }
}


interface DIInterface{
    void logic();
}

@Component
@Primary //같은 타입의 빈이 여러개일때 제일 우선적으로 의존성 주입될 객체라는 의미
class DIInstance1 implements DIInterface{

    public void logic() {
        System.out.println("DIInstance1.logic 호출");
    }
}


@Component
class DIInstance2 implements DIInterface{

    public void logic() {
        System.out.println("DIInstance2.logic 호출");
    }
}

 

  • 결과 화면

결과 화면을 보면 setter 의존성 주입 이후

diInterface 멤버 필드에 다른 의존성이 주입된것이 보인다.

그리고 이러한것이 가능한 이유는, diInterface 필드가 final이 아니기 때문이다.

필수 의존성은 final로 선언을 해두는것이 안전한것 같다.

개발자의 혹시 모를 실수로 의존성이 교체되거나, 알맞은 빈을 등록하지 않아 문제가 생길 수 도 있기 때문이다.

 

 

 

참고: @Autowired 의 기본 동작은 주입할 대상이 없으면 오류가 발생한다. 주입할 대상이 없어도 동작하게 하려면 @Autowired(required = false) 로 지정하면 된다.

 

 

필드 주입

이름 그대로 필드에 바로 주입하는 방법이다.

 

- 특징

  • 코드가 간결해서 많은 개발자들을 유혹하지만 외부에서 변경이 불가능해서 테스트 하기 힘들다는 치명적인 단점이 있다.
  • DI 프레임워크가 없으면 아무것도 할 수 없다.
  • 사용하지 말자!
    - 애플리케이션의 실제 코드와 관계 없는 테스트 코드
    - 스프링 설정을 목적으로 하는 @Configuration 같은 곳에서만 특별한 용도로 사용

- 기본 코드

@Component
public class OrderServiceImpl implements OrderService {

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

 

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

 

테스트코드에서 memberRepository, discountPolicy 변수에 객체를 할당해줄 방법이 없다는것이다.

할당하지 않으면 null이므로 테스트를 할 수가 없다.

그럼 테스트를 위해 또 setter 메소드들을 만들어 줘야한다.

결국 안 사용하는게 답이다.

 

 

일반 메서드 주입

  • 일반 메서드를 통해서 주입 받을 수 있다.
  • 특징
    - 한번에 여러 필드를 주입 받을 수 있다.
    - 일반적으로 잘 사용하지 않는다.

  • 기본 코드
@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;
 	}
}

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

 

 

옵션 처리

주입할 스프링 빈이 없어도 동작해야 할 때가 있다.

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

 

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

  • @Autowired(required=false) : 자동 주입할 대상이 없으면 수정자 메서드 자체가 호출 안됨.
    생성자에서는 사용할 수 없다.
    즉, 생성자 주입 에서는 해당 옵션을 사용할 수 없다. 애노테이션을 붙인다고 해도 적용되지 않는다.


  • org.springframework.lang.@Nullable : 자동 주입할 대상이 없으면 null이 입력된다.
    생성자의 매개변수에 사용 가능하다.

  • Optional<> : 자동 주입할 대상이 없으면 Optional.empty 가 입력된다.
    생성자의 매개변수에 사용 가능하다.

    참조 : https://www.inflearn.com/questions/214902
 

생성자 주입의 경우엔 @Autowired(required=false)를 쓸 수 없는건가요? - 인프런 | 질문 & 답변

안녕하세요 최고의 강의 항상 잘 듣고 있습니다 : ) 궁금한게 있어 질문드립니다 @Autowired(required=false) : 자동 주입할 대상이 없으면 수정자 메서드 자체가 호출 안됨 라고 강의에서 언급하셨는데,

www.inflearn.com

 

  • 기본 코드
@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은 스프링 전반에 걸쳐서 지원된다. 예를 들어서 생성자 자동 주입에서 특정 필드에만 사용해도 된다.

 

 

롬복 사용

롬복은 비효율적인 코드의 반복을 줄여주는 라이브러리이다.

롬복 적용방법은 간단한 구글링만 해도 찾을 수 있다.

다음은 롬복에서 사용되는 간단한 몇가지 애노테이션만 설명하겠다.

 

  • @RequiredArgsConstructor

    - 적용 전 코드
@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;
 }
}

 

- 적용 후 코드

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

@RequiredArgsConstructor 애노테이션은
final 키워드가 붙은, 필수로 초기화가 되어야 하는 멤버필드들의 생성자 코드를 컴파일 시점에 자동으로 생성 시켜줌.

실제 class 를 열어보면 다음 코드가 추가되어 있는 것을 확인할 수 있다.

 

- 정리

최근에는 생성자를 딱 1개 두고, @Autowired 를 생략하는 방법을 주로 사용한다. 여기에 Lombok 라이브러리의 @RequiredArgsConstructor 함께 사용하면 기능은 다 제공하면서, 코드는 깔끔하게 사용할 수 있다.

 

 

조회되는 타입의 빈이 2개 이상일때

@Autowired를 사용한 의존성 주입은 기본적으로 타입을 이용한 조회이다.

AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(Config.class);
ac.getBean(DiscountPolicy.class)

마치 위에 ac.getBean(타입.class) 와 같은 기능을 하는 것이다.

 

그러므로, 스프링 컨테이너에 등록된 빈의 이름(Key)만 다를뿐 타입을 같을 수가 있다.

그런 경우, 아무런 대책 없이 @Autowired를 사용한다면 

"NoUniqueBeanDefinitionException" 과 같은 에러를 발생시킨다.

 

이럴 경우의 해결책은 다음과 같다.

  • @Autowired 필드 명
    @Autowired 는 타입 매칭을 시도하고, 이때 여러 빈이 있으면 필드 이름, 파라미터 이름으로 빈 이름을 추가 매칭한다.
    글로는 확 와닿지 않는다. 코드로 보자.

    - 일반적인 코드
@Autowired
private DiscountPolicy discountPolicy

 

       - 필드명을 특정 빈 이름(key) 으로 맞춰 준 코드

@Autowired
private DiscountPolicy rateDiscountPolicy;

DiscountPolicy는 인터페이스이고,
FixDiscountPolicy, RateDiscountPolicy 라는 구현체들이 존재할때 특정 구현체의 이름으로 필드명을 설정해주면  

위에서 말했다시피 @Autowired 는 일단 타입으로 찾고, 똑같은 타입이 여러개 일때, 그중에 필드명 혹은 메소드명으로 검색하여 본다.

(빈 이름은 따로 설정을 하지 않는이상, 메소드명, 클래스명의 맨앞만 소문자로 바꿔서 등록 하므로)

 

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

    - Qualifier가 적용된 Component들
@Component
@Qualifier("mainDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy {}
@Component
@Qualifier("fixDiscountPolicy")
public class FixDiscountPolicy implements DiscountPolicy {}

 

- @Qualifier를 적용한 의존성 주입

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

수정자 주입에도 사용 가능하다.

@Qualifier 로 주입할 때 @Qualifier("mainDiscountPolicy") 를 못찾으면 어떻게 될까? 그러면 mainDiscountPolicy라는 이름의 스프링 빈을 추가로 찾는다.

하지만 거기까지 가지 않도록 애초에 빈 이름을 구별되게 지정하든지 하자.

 

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

    - @Primary 사용예
@Component
@Primary //우선권을 가진다
public class RateDiscountPolicy implements DiscountPolicy {}
//@Primary 붙은 클래스 보다 우선권 낮음
@Component
public class FixDiscountPolicy implements DiscountPolicy {}
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
 	this.memberRepository = memberRepository;
 	this.discountPolicy = discountPolicy;
}

 

 

@Primary와 @Qualifier 우선순위

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

 

 

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

스프링 컨테이너에 등록된 특정 타입의 빈을 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);
 }
 }
}

 

자동빈 등록과 수동빈 등록은 각각 언제 사용해야하는가

기본적으로 자동빈 등록을 사용하되, 기술 지원 빈의 경우에 수동빈을 사용하는것이 유용하다.

왜냐하면, Controller, Service, Repository의 순서로 흘러가는 비즈니스 로직은 각각의 요구 사항에 따라 빈이 계속 증가되므로, 그 수많은 빈을 @Configuration이라는 한 클래스 안에 넣기는 너무 비대해지고, 너무 많을 경우 오히려 더 관리가 힘들어진다.

그러나, 기술지원 빈의 경우 빈의 갯수는 적지만 보다 광범위한 범위에서 작동되는 로직들이기에 따로 관리도 필요하고, 빈의 갯수도 적기에 @Configuratio 안에서 관리하기에 효율적이다.

 

+ 기술 지원 빈이란 :

기술적인 문제나 공통 관심사(AOP)를 처리할 때 주로 사용된다. 데이터베이스 연결이나, 공통 로그 처리 처럼 업무 로직을 지원하기 위한 하부 기술이나 공통 기술들이다

'코드스테이츠' 카테고리의 다른 글

[Section4][웹 애플리케이션 설계]  (0) 2022.10.18