AtraFelis's Develop Diary

[Spring] 의존관계 자동 주입 (스프링 핵심원리 - 기본편 Section8) 본문

Programming/Spring

[Spring] 의존관계 자동 주입 (스프링 핵심원리 - 기본편 Section8)

AtraFelis 2025. 1. 19. 21:22

스프링 핵심 원리 - 기본편 강의를 수강하며 작성한 글입니다.
Section 8. 의존관계 자동 주입

 

의존관계 주입 방법

Section 7. 컴포넌트 스캔 정리 글에서 잠깐 언급했던 의존관계 주입 방법에 대해 조금 더 자세히 설명하고자 한다.

생성자 주입

이전까지 해왔던 것처럼 생성자를 이용해 의존관계를 주입하는 방식이다.

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

이렇게 생성자 위에 @AutoWired를 붙이면 자동으로 의존관계 주입이 완료된다.

생성자 주입에는 중요한 특징이 하나 있는데, 호출 시점에 단 한 번만 실행된다는 것이다. 당연하지만, 생성자는 일반적인 메소드처럼 호출하여 사용할 수 없다! 즉, 누군가가 실행 중 이 의존관계를 변경할 수 없다는 것을 의미한다.

생성자 주입은 불변하거나, 필수적인 의존관계를 주입해야 할 경우 사용한다고 할 수 있다.

그리고 대부분의 경우 실행 중, 동적으로 의존 관계를 변경할 일은 거의 없다. 그렇기에 특수한 경우가 아니라면, 밑에서 언급할 다른 자동 관계 주입 보다도 이 생성자 주입을 권장한다.

💡AutoWired 생략

생성자가 유일한 경우에는 @AutoWired를 생략해도 자동으로 주입된다.

💡생성자 주입을 권장하는 또 다른 이유

1. 테스트 중 컴파일 오류
테스트 시 컴파일 오류를 일으켜 이 클래스에 어떤 의존관계가 필요한지 직관적으로 알 수 있다. 하지만 수정자 주입을 사용할 경우에는 클래스 생성 시 컴파일 오류를 일으키지 않고, 실행했을 때 Null Pointer Exception 오류를 발생시킨다.

생성자 주입의 경우

수정자 주입의 경우


2. final 키워드

final 키워드를 사용할 수 있게 된다. final 키워드가 사용되었을 경우, 실수로 값을 설정하지 않았더라도 컴파일 오류를 일으켜 잘못 되었음을 바로 알려준다. 수정자를 사용할 경우에는 final 키워드를 사용하지 못한다.

 

수정자 주입

수정자, 즉, Setter를 이용해 의존관계를 주입하는 방법이다.

@Component  
public class OrderServiceImpl implements OrderService {  

    private MemberRepository memberRepository;  
    private DiscountPolicy discountPolicy;  

    @AutoWired
    public void setDiscountPolicy(DiscountPolicy discountPolicy) {  
        this.discountPolicy = discountPolicy;  
    }  

    @AutoWired
    public void setMemberRepository(MemberRepository memberRepository) {  
        this.memberRepository = memberRepository;  
    }
}

이렇게 setXXX라는 이름의 수정자 메서드에 @AutoWired를 붙여 의존관계 주입을 할 수 있다.

생성자와 달리 수정자는 개발자가 호출하여 사용할 수 있으므로, 선택, 변경 가능성이 있는 의존관계에 주로 사용한다. 하지만, 생성자 주입에서 언급했다시피 실행 중 동적으로 의존 관계를 변경할 일은 거의 없다.

게다가 수정자 주입을 사용하게 될 경우, 개발자가 임의로 변경해서는 안 되는 의존관계를 변경해버리는 참사가 발생할 수 있다.

 

필드 주입

필드에 바로 @AutoWired를 붙여 의존관계를 주입하는 방법이다.

@Component  
public class OrderServiceImpl implements OrderService {  

    @AutoWired
    private MemberRepository memberRepository;  

    @AutoWired
    private DiscountPolicy discountPolicy;
}

코드 자체가 매우 간결해져 많이 사용할 것 같지만, 실제로는 권장하지 않는 방식이다.

이렇게 사용하면 테스트를 하기 어렵다. 보통 테스트 코드는 스프링을 사용하지 않고 순수한 자바 코드를 이용하는 것이 좋다.

@Test
void fieldInjectionTest() {
    OrderServiceImpl orderService = new OrderService();

    // 의존관계를 주입할 방법이 없다.

    orderService.createOrder(1L, "itemA", 10000);
}

이렇게 OrderService에 테스트하고 싶은 의존관계를 주입하여 활용하고 싶지만, 개발자가 원하는데로 의존관계를 주입할 수 있는 방법이 없다.

void fieldInjectionTest() {
    OrderServiceImpl orderService = new OrderServiceImpl(new MemoryMemberRepository, new fixDiscountPolicy());
}
void fieldInjectionTest() {
    OrderServiceImpl orderService = new OrderService();
    orderService.setMemberRepository(new MemoryMemberRepository());
    orderService.setMemberRepository(new FixDiscountPolicy());
}

생성자 주입이나 수정자 주입이었다면, 이렇게 원하는 객체를 선언하여 주입 후 테스트할 수 있었을 것이다.

하지만 필드 주입을 하면 이렇게 순수한 자바코드로만 테스트를 할 수 있는 방법이 사라진다. 결국 테스트를 위해 클래스 내부에 수정자를 따로 선언하여 사용하게 되는데, 이럴 거면 애초에 수정자 주입을 사용하는 것이 낫지 않겠는가?

그리고 위에서 언급했듯 대부분의 경우 생성자 주입을 권장하기 때문에, 결국 생성자 주입을 사용하는 것이 낫다는 결론으로 귀결된다.

 

일반 메서드 주입

일반적으로는 잘 사용하지 않지만, 일반 메서드에도 의존관계를 주입할 수 있다.

@Component  
public class OrderServiceImpl implements OrderService {  

    private MemberRepository memberRepository;  
    private DiscountPolicy discountPolicy;  

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

하지만 생성자 주입, 수정자 주입을 통해 대부분의 상황을 커버할 수 있으므로 잘 사용하지 않는다.

 

옵션 처리

주입할 스프링 빈이 컨테이너에 존재하지 않는 경우에도 동작해야할 때가 있다. 변수에 null 값이 들어가 있어도 직접적으로 사용하지 않는다면 동작하는 것처럼 의존 관계도 이러한 경우가 있는 것이다.

💡 생성자 주입에 이 옵션을 적용하면 어떻게 되나요?

기술적으로는 가능하지만, 생성자 주입의 경우에는 이 옵션이 권장되지 않는다.

위에서 말했듯, 생성자 주입은 불변, 필수적인 의존관계를 주입할 때 사용한다. 그런데 이 옵션을 주면 의존성이 없을 때 null 값이 들어갈 수 있게 된다. 필수라고 했는데 이렇게 설정한다는 것 자체가 논리적으로 이상하다. 또 생성자는 객체가 생성될 때 한 번 호출된다. 이때 의존성을 주입하지 않았다는 것은 필요한 의존성이 없다는 의미가 되는데, 이 또한 논리적으로 맞지 않다.

애초에 이 옵션을 사용하고 싶다면 수정자 주입을 이용하면 된다. 권장되지 않는다 라고 하지만, 사실 사용하면 안 된다에 가깝다.

이러한 옵션 처리 방법에는 세 가지가 존재한다.

 

@AutoWired(required = false)

이렇게 @AutoWried 애노테이션에서 required 옵션을 false로 설정하는 방법이다. 이렇게 설정을 할 경우, 자동 주입할 대상이 없으면 메서드 자체가 호출이 되지 않는다.

@Autowired(required = false)  
public void setNoBean(Member noBean) {  
    System.out.println("noBean = " + noBean);  
}

@Nullable

자동 주입할 대상이 없다면, null이 입력된다.

@Autowired  
public void setNoBean(@Nullable Member noBean) {  
    System.out.println("noBean = " + noBean);  
}

Optional<>

자동 주입할 대상이 없다면, Optional.empty가 입력된다.

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

 

💡 Optinal<> 이 뭔가요? by ChatGPT o1

NullPointerException(NPE) 문제를 줄이고, “값이 있을 수도 있고 없을 수도 있는 상황”을 명시적으로 표현하기 위해 자바 8에서 도입된 클래스입니다.

  1. NullPointerException 방지: 메서드에서 null을 반환하거나, null을 받을 수 있는 상황을 명시적으로 표현합니다.
  2. 의도 명확화: “값이 없을 수도 있다”는 것을 Optional로 보여주어, 사용하는 쪽 코드가 필수로 검사 로직(isPresent 등)을 작성해야 하도록 만듭니다.

 

 

조회할 빈이 2개 이상일 경우

자동으로 의존 관계를 설정할 때 이런 문제가 발생할 수 있다.

@Component
public class FixDiscountPolicy implements DiscountPolicy { ... }
@Component
public class RateDiscountPolicy implements DiscountPolicy { ... }

이렇게 DiscountPolicy의 구현체 두개를 모두 등록했을 경우,

@AutoWired
private DiscountPolicy discountPolicy;

DiscountPolicy의 하위 타입인 두 구현체가 모두 조회되며 NoUniqueBeanDefinitionException 오류가 발생한다.

이러한 문제가 발생할 경우 어떻게 해결할 수 있는지를 알아보자.

 

하위 타입으로 지정

@AutoWired
private RateDiscountPolicy discountPolicy;

하위 타입으로 지정을 해주어도 되지만, 이렇게 해버린다면 지금까지 배워왔던 DIP를 위배하게 되어버린다. 또, 동일한 타입, 다른 이름일 경우에는 해결이 되지 않는다.

 

수동 등록

설정 파일을 만들어 @Bean으로 수동 등록을 해주어도 된다.

 

@Autowired 필드 명 매칭

@AutoWired에는 빈을 주입할 때의 절차가 존재한다.

  1. 타입 매칭
  2. 동일 타입의 빈이 여러개 존재할 경우, 빈 이름과 필드 혹은 파라미터 이름으로 매칭
@AutoWired
private DiscountPolicy rateDiscountPolicy;

즉, 이렇게 필드 명을 선언했을 경우 우선적으로 필드 명과 동일한 이름의 스프링 빈이 주입된다는 의미이다. 이 예시에서는 rateDiscountPolicy라는 이름의 스프링 빈이 주입된다.

 

@Qualifier

추가 구분자를 붙여주는 방법으로, 스프링 빈에 별명을 하나 붙인다고 생각하면 편하다.

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

이렇게 @Qualifier를 붙여주고,

@AutoWired
@Qualifier("rateDiscountPolicy")
private DiscountPolicy rateDiscountPolicy;

주입 시에도 사용하고자 하는 @Qualifier를 붙여주면 된다. @Qualifier("rateDiscountPolicy")를 붙여주었으므로 RateDiscountPolicy가 주입된다.

 

💡 Qualifier의 단점

이렇게 @Qualifier를 사용할 경우에 단점이 하나 존재하는데, 이 애노테이션에 들어가는 구분자가 문자열이라는 것이다. 즉, 오타가 났을 경우에도 컴파일 에러를 발생시키지 않는다.

다만, 현재 이 글을 쓰는 기준, 구독 버전의 인텔리제이에서는 @Qualifier에 등록되지 않은 이름을 작성할 경우 경고를 뛰워준다. 커뮤니티 버전에서도 되는 기능인지는 모르겠다.

이것을 해결하는 방법은 @Qualifier를 포함하는 애노테이션을 하나 더 만드는 것이다.

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

이렇게 Qualifier를 재정의한 애노테이션을 하나 만든 후 사용하면 된다.

@AutoWired
@RateDiscountPolicy
private DiscountPolicy rateDiscountPolicy;

실제로 이렇게 컴파일 에러가 나타나는 것을 확인할 수 있다.

@Primary

Primary, 번역하면 주요한이라는 뜻이다. 즉, 이 애노테이션이 붙은 스프링 빈을 최우선으로 사용하라는 의미이다.

@Component
public class FixDiscountPolicy implements DiscountPolicy { ... }
@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy { ... }

이렇게 RateDiscountPolicy@Priamry를 붙일 경우, 같은 타입의 스프링 빈이 조회되었을 때, @Primary가 붙은 RateDiscountPolicy가 우선적으로 주입된다.

 

💡 Qualifier VS Primary

두 애노테이션이 경합하는 경우가 생길 수 있다.

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

이렇게 선언되어 있을 때,

@AutoWired
@Qualifier("fixDiscountPolicy")
private DiscountPolicy rateDiscountPolicy;

이렇게 의존관계를 주입한다면 어떻게 될까? @Priamry가 붙은 RateDiscountPolicy가 주입될까, 아니면 @Qualifier로 지정된 fixDiscountPolicy가 주입될까?

정답은 @Qualifier("fixDiscountPolicy")가 이긴다.

언제나 상세한 정보가 포함된 쪽이 우선권이 높다는 것을 기억하자.

 

Map으로 한 번에 주입받기

같은 타입의 스프링 빈이 여러개가 필요한 경우가 발생할 수 있다.

예를 들어, 사용자가 물건을 주문할 때 할인 정책을 선택할 수 있게끔 구현하고 싶다면, FixDiscountPolicy*와 *RateDiscountPolicy 두 스프링 빈 모두 주입할 필요가 있다.

이럴 때는 어떻게 해야할까?

static class DiscountService {  
    private final Map<String, DiscountPolicy> policyMap;

    @AutoWired
    public DiscountService(Map<String, DiscountPolicy> policyMap) {  
        this.policyMap = policyMap;  
        System.out.println("policyMap = " + policyMap);
    }  

    public int discount(Member member, int price, String discountCode) { ... }
}

policyMap = {fixDiscountPolicy=atrafelis.core.discount.FixDiscountPolicy@43b0ade, rateDiscountPolicy=atrafelis.core.discount.RateDiscountPolicy@5395ea39}

이렇게 출력문을 확인해보면, Map에 조회된 스프링 빈이 전부 주입된 것을 확인할 수 있다. key는 스프링 빈의 이름, value는 스프링 빈이 된다.

 


 

References

스프링 핵심 원리 - 기본편