의존관계 자동 주입

udonehn·2023년 11월 3일
0

다양한 의존관계 주입 방법

의존관계 주입에는 크게 4가지 방법이 있다.

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

생성자 주입

  • 생성자를 통해 의존관계를 주입받는 것으로, 생성자 호출 시점에 딱 1번만 호출되는 것이 보장된다.
  • 불변, 필수 의존관계에 사용된다.
    • 값은 호출 시에만 주입되기 때문에 불변이다.
    • final 키워드를 사용하기 때문에 null을 허용하지 않으므로 필수적이다.
  • 생성자가 딱 1개만 있으면 @Autowired를 생략해도 자동주입이 가능하다.
@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;
	}
}

수정자 주입(setter 주입)

  • 필드의 값을 변경하는 setter 수정자 메서드를 통해 의존관계를 주입한다.
  • 선택, 변경 가능성이 있는 의존관계에 사용된다.
  • @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;
	}
}

필드 주입

  • 코드가 간결하지만 외부에서 값을 변경할 수 없어 테스트가 어렵다는 단점이 있다.
  • 특수한 경우에만 사용하고 일반적으로는 사용하지 않는다.
@Component
public class OrderServiceImpl implements OrderService {
    @Autowired
    private MemberRepository memberRepository;
    @Autowired
    private DiscountPolicy discountPolicy;
}

일반 메서드 주입

  • @Autowired를 일반 메서드에도 적용해 주입받을 수 있다.
  • 일반적으로 잘 사용하지 않는다.
@Component
public class OrderServiceImpl implements OrderService {

    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

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

옵션 처리

@Autowired의 required 옵션의 기본값이 true로 설정되어 있기 때문에 주입할 빈은 반드시 스프링 컨테이너에 등록이 되어있어야 한다.

주입할 스프링 빈이 없어도 다음 세 가지 방법을 사용해 작동시킬 수 있다.

  1. @Autowired(required=false): 자동 주입할 대상이 없으면 수정자 메서드 자체가 호출되지 않는다.
  2. org.springframework.lang.@Nullable: 자동 주입할 대상이 없으면 null이 입력된다.
  3. Optional<>: 자동 주입할 대상이 없으면 Optional.empty가 입력된다.

test폴더의 core 밑에 autowired 패키지를 생성하고, 그 밑에 AutowiredTest 클래스를 생성한다.

AutowiredTest

public class AutowiredTest {

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

    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 setNoBean1(Optional<Member> noBean3){
            System.out.println("noBean3 = " + noBean3);
        }
    }
}

실행결과

noBean2 = null
noBean3 = Optional.empty

setNoBean1은 호출되지 않았다.

생성자 주입을 선택해라!

  • 일반적으로 의존관계 주입은 한 번 일어나면 애플리케이션 종료시점까지 변경되는 일이 없기 때문에, 생성자 주입을 권장한다.
  • 프레임워크에 의존하지 않고, 순수한 자바 언어의 특징을 잘 살리는 방법이다.
  • final 키워드를 사용할 수 있기 때문에 생성자에서 값이 설정되지 않는 오류를 컴파일 시점에 막을 수 있다.

수정자 주입을 사용하였을 때의 단점을 예시로, 어떤 클래스가 다른 어떤 클래스와 의존 관계에 있는지 확인하기 위해서는 그 클래스의 코드를 자세히 살펴보아야 하는 수고가 필요하다. 하지만 생성자 주입을 사용한다면 해당 객체를 생성할 때에 생성자 코드만 보고도 어떠한 클래스와 의존관계가 있고, 어떠한 의존관계 주입이 필요한지 쉽게 알 수 있다.

컴파일 오류가 세상에서 가장 빠르고 좋은 오류다.

롬복과 최신 트랜드

Lombok 라이브러리에서 제공하는 에노테이션을 사용하면 getter/setter이나 ToString, 생성자를 자동으로 생성해 주기 때문에 코드를 간단하게 작성할 수 있다.
생성자를 1개만 두는 경우에 @Autowired를 사용하여 생성자 코드를 직접 작성하지 않고도 @RequiredArgsConstructor를 사용해 생성자를 자동으로 만들 수 있는 것이다.

  • Lombok을 사용하기 위해 build.gradle 파일을 수정한다.

@RequiredArgsConstructor를 사용해 다음과 같이 작성할 수 있다.

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

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

테스트를 위해 DiscountPolicy를 상속받는 FixDiscountPolicyRateDiscountPolicy 둘 다 스프링 빈으로 등록해 본다. 이 경우, DiscountPolicy에 의존하는 다른 클래스는 의존관계 주입시 오류가 발생하게 된다. (NoUniqueBeanDefinitionException)
수동 빈 등록으로 해결할 수 있지만, 자동 빈 등록시에도 해결할 수 있는 방법이 있다.

@Autowired 필드 명, @Qualifier, @Primary

조회 대상 빈이 2개 이상일 때에는 다음과 같이 해결할 수 있다.

  • @Autowired 필드 명 매칭
  • @Quilifier -> @Quilifier끼리 매칭 -> 빈 이름 매칭
  • @Primary 사용

@Autowired 필드 명 매칭

@Autowired는 타입 매칭을 시도하고, 매칭 결과가 두 개 이상 있다면 필드 이름 또는 파라미터 이름으로 빈 이름을 추가 매칭한다.

DiscountPolicy로 조회하면 두 개의 빈이 조회되어 오류가 발생한다.

    @Autowired
    private final DiscountPolicy discountPolicy;

이 때, 필드 명을 구체 타입으로 지정하면 오류가 발생하지 않는다.

    @Autowired
    private final DiscountPolicy discountPolicy;

@Quilifier 사용

@Qualifier은 추가 구분자를 제공한다. 만약 해당 이름의 Qulifier가 없다면 같은 이름의 빈 이름으로 조회한다. Qualifier는 추가 구분자를 제공하는 것으로, 빈 이름을 변경하는 것이 아님을 기억한다.

다음 예시는 mainDiscountPolicy라는 이름으로 구분자를 추가하는 예시이다.

빈 등록시 @Quilifier는 다음과 같이 사용한다.

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

의존관계 주입 시 @Quilifier는 다음과 같이 사용한다.

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

@Primary 사용

의존관계 주입 시에 여러 빈이 매칭된다면 @Primary 애노테이션이 있는 빈이 우선권을 갖는다.

@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy{

@Primary와 @Qualifier

@Primary와 @Qualifier모두 존재할 때에는 좀 더 상세하게 지정한 @Qualifier가 우선권을 갖는다. 따라서 일반적으로 사용하는 클래스는 @Primary를 사용하고, 특별한 경우에 사용할 때에만 @Qualifier를 사용하는 것이 바람직하다.

애노테이션 직접 만들기

생략

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

해당 타입의 스프링 빈이 모두 필요한 경우에는 다음과 같이 생성자 주입을 할 수 있다.

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

다형성을 유지하며 동적인 빈 주입이 필요할 때 사용한다.

DiscountService의 메서드로 다음과 같은 예시가 있다.

        public int discount(Member member, int price, String discountCode) {
            DiscountPolicy discountPolicy = policyMap.get(discountCode);
            return discountPolicy.discount(member, price);
        }

discount메서드는 원하는 DiscountPolicy 빈을 사용하기 위해 discountCode를 파라미터로 받는다. 이를 통해 상황에 맞게 동적으로 빈을 사용할 수 있다.

자동, 수동의 올바른 실무 운영 기준

일반적으로 편리한 자동 빈 등록을 사용하는 것이 좋다. 하지만 애플리케이션이 큰 영향을 끼치는 부분이나 다형성이 중요한 부분에서는 수동 등록을 고려해 볼 수 있다.

본 포스팅은 김영한 강사의 스프링 핵심 원리 강의를 수강하고 요약한 것으로, 해당 강의의 영상 및 강의자료를 참고하였습니다.

profile
안녕하세요. 만나서 반갑습니다.

0개의 댓글

관련 채용 정보