이전 글인 컴포넌트 스캔에서 살펴보았듯 자동으로 스프링 빈을 등록하려면 어디선가 객체 인스턴스를 주입해야 할 필요가 있게 됩니다. Spring에서는 개발자가 SOLID를 지키며 객체 인스턴스를 알아서 주입하도록 @Autowired
애너테이션을 준비했습니다.
애너테이션 적용 위치
@Autowired
애너테이션은 필드, 생성자, 메서드에 적용할 수 있습니다. 해당 애너테이션이 적용되면 스프링 컨테이너가 스프링 빈을 생성한 후에 @Autowird
애너테이션이 적용된 필드, 생성자, 메서드를 확인해 필요한 스프링 빈을 주입합니다.
@Autowired
애너테이션의 동작 원리에 대해 살펴보려면 나중에 본 블로그에서 다루거나 다른 블로그의 좋은 글들을 봐주시기 바랍니다. (#1, ...)
생성자 주입
@Component
// @RequiredArgsConstructor
public class TestBean {
private final NecessaryBean necessaryBean;
@Autowired
TestBean(NecessaryBean necessaryBean) {
this.necessaryBean = necessaryBean;
}
}
생성자는 객체 인스턴스를 만들 때 딱 한 번, 반드시 실행됩니다. 따라서 스프링 빈이 생성될 때 생성자는 반드시 실행됩니다.
이 때문에 생성자를 통한 의존관계 주입 방법은 필요로 하는 스프링 빈이 애플리케이션 실행 중에 변하지 않으며 꼭 필요한 의존관계인 경우에 사용합니다.
객체 지향 프로그래밍에서의 중요한 개념인 다형성을 구현하는 것 중 하나인 오버로딩 기법으로 여러 개의 생성자를 만들 수 있습니다. Spring Framework 4.3 이상부터 @Autowired
적용 시 생성자가 하나인 경우에는 애너테이션을 생략할 수 있습니다. (참고)
그러나 생성자가 여러 개 존재하는 경우 스프링 컨테이너가 스프링 빈을 만들 때 어떤 생성자를 호출해야 하는지 모르기 때문에 @Autowired 애너테이션은 각 생성자 중 어느 하나에만 반드시 존재해야 하며 여러 개의 생성자에 존재할 수 없습니다.
Lombok 라이브러리를 사용하면 생성자를 개발자가 만들지 않아도 Lombok이 컴파일 시점에 바이트 코드를 조작하여 생성자를 만들어 줍니다. @NoRequiredArgsConstructor
는 매개변수가 없는 생성자(디폴트 생성자)를, @RequiredArgsConstructor
는 final
키워드가 지정된 필드를 매개변수로 하여 값을 넣는 생성자를, @AllRequiredArgsConstructor
는 모든 필드를 매개변수로 하여 값을 넣는 생성자를 알아서 만들어 줍니다.
메서드 주입
@Component
public class TestBean {
private NecessaryBean necessaryBean;
private BeanBin beanBin;
@Autowired
public void setNecessaryBean(NecessaryBean necessaryBean) {
this.necessaryBean = necessaryBean;
}
@Autowired
public void init(NecessaryBean necessaryBean, BeanBin beanBin) {
this.necessaryBean = necessaryBean;
this.beanBin = beanBin;
}
}
수정자 주입
애플리케이션 실행 중에 의존관계 수정이 필요하다고 판단되면 수정자(Setter)를 사용해 의존관계를 변경하도록 할 수 있습니다.
일반 메서드 주입
수정자 주입은 자바 빈 프로퍼티 규약을 준수해 메서드를 만들기로 약속해 한 번에 하나의 필드만 변경할 수 있습니다. 일반 메서드 주입을 사용하면 이러한 규약이 없기에 여러 개의 필드를 변경할 수 있습니다.
메서드 주입 선택 시 주의사항으로는 스프링 빈 생성 후 의존관계 주입 시 @Autowired
가 적용된 메서드가 실행된다는 점과 주입할 빈이 스프링 컨테이너에 없다면 예외가 발생한다는 점입니다. 스프링 빈 생성 후 의존관계 주입 시 이러한 메서드의 실행을 방지하려면 @Autowired
의 required
속성을 false
로 설정해야 합니다.
필드 주입
@Component
public class TestBean {
@Autowired
private NecessaryBean necessaryBean;
}
필드에 바로 @Autowired
애너테이션을 적용하면 소스 코드가 간결하고 편리합니다. 그러나 객체 지향 프로그래밍에서의 중요한 개념 중 하나인 캡슐화를 지키기 위해 외부 클래스에서 필드를 변경하기 어렵습니다. 이를 해결하기 위해 수정자를 사용하면 되지만 굳이 수정자를 만든다면 수정자에 @Autowired
애너테이션을 적용하면 되지 굳이 필드에 적용해야 하는가에 대한 고민이 생깁니다.
또 테스트 코드 작성 시 스프링 등에 의존해 작성하는 것보다 순수한 자바 코드로 작성하면 여러 부가 기능을 실행하는 시간이 줄어 보다 빠르게 테스트할 수 있어 보통 순수한 자바로 테스트 코드를 작성하게 됩니다. 이때 필드에 @Autowired
를 적용하게 되면 테스트 코드(외부 클래스)에서 이 필드에 접근 또는 변경할 수 없으므로 테스트 시 NullPointerException이 발생할 수 있습니다.
이러한 이유로 필드 주입은 가급적 사용을 지양해야 합니다. 보통 필드 주입은 @SpringBootTests
또는 @Configuration
애너테이션이 적용된 클래스에서 주로 사용합니다.
많은 이들이 생성자 주입을 권장한다
- 불변
- 대부분의 의존관계 주입은 애플리케이션 종료 전까지 변함이 없습니다.
- 일반적으로 수정자 주입은 메서드의 접근 지정자로
public
을 선택하기에 다른 개발자가 실수로 필드 값을 변경할 수 있습니다.
- 누락
- 수정자 주입을 선택하면 어떤 인스턴스가 필요한지 몰라 일부분에서 NullPointerException이 발생할 수 있습니다.
- 생성자 주입을 선택하면 필드에
final
키워드를 적용할 수 있어 생성자에서 값이 설정되지 않는 오류를 컴파일 시점에 방지할 수 있습니다.
스프링 빈이 여러 개인 경우
@Autowired
는 기본적으로 타입으로 스프링 빈을 찾습니다. 만약 해당 타입의 스프링 빈이 여러 개라면 NoUniqueBeanDefinitionException 예외가 발생하게 됩니다. 이를 해결하기 위해 하위 타입으로 지정할 수 있으나 SOLID에서 D인 "추상화에 의존해야지, 구체화에 의존하면 안 된다"의 의존 관계 역전 원칙(DIP, Dependency Inversion Principle)을 위반하고 유연성이 떨어집니다.
스프링 빈이 여러 개일 때 이러한 원칙을 준수하며 하나의 스프링 빈을 선택하고자 할 때 다음의 방법을 택할 수 있습니다.
필드명 매칭
@Autowired
는 이전에 서술했듯 기본적으로 타입으로 스프링 빈을 찾습니다. 찾은 스프링 빈이 만약 두 개 이상이라면 필드 또는 파라미터 이름으로 스프링 빈을 찾아봅니다.
Spring Framework 6.1, Spring Boot 3.2 이상에서는 매개변수 이름을 파싱하지 않으므로 필드명 매칭을 사용하면 정상 동작하지 않고 예외가 발생하게 됩니다. (#1, ...)
@Quilifier
@Component
@Qualifier("rateDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy {}
@Component
@Qualifier("fixDiscountPolicy")
public class FixDiscountPolicy implements DiscountPolicy {}
// @Qualifier("rateDiscountPolicy")가 적용된 클래스의 인스턴스(스프링 빈)를 주입
@Autowired
public OrderServiceImpl(MemberRepository memberRepository,
@Qualifier("rateDiscountPolicy") DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
@Quilifier
애너테이션을 클래스에 적용하고 해당 클래스를 필요로 하는 클래스에도 @Quilifier
를 적용하면 해당 클래스를 주입합니다.
@Quilifier
를 스프링 빈의 이름을 지정하는 것이라 오해할 수 있지만 스프링 빈 이름을 지정하는 것이 아닌 주입 시 추가 식별자를 제공하는 것입니다.
// @Qualifier("mainDiscountPolicy")가 적용된 클래스를 찾고,
// 없으면 mainDiscountPolicy라는 이름의 스프링 빈을 찾음
@Autowired
public OrderServiceImpl(MemberRepository memberRepository,
@Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
만약 @Quilifier
가 적용된 클래스 없이 해당 클래스를 호출한다면 지정된 값의 이름으로 등록된 스프링 빈을 찾습니다.
애너테이션의 값이 문자열이기 때문에 컴파일 시 타입 체크가 되지 않아 실행 중 문제를 초래할 수 있습니다. 이를 해결하려면 별도의 애너테이션을 만들 수 있습니다.
애너테이션 직접 만들어 사용하기
@Component
@Qualifier("mainnDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy {
// ...
}
@Quilifier("mainnDiscountPolicy")
이 적용된 클래스가 없어도, mainnDiscountPolicy
라는 이름의 스프링 빈이 없어도 컴파일은 정상적으로 되나 해당하는 빈이 없기 때문에 실행 중 예외가 발생하게 됩니다.
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Qualifier("rateDiscountPolicy")
public @interface RateDiscountPolicyQualifier {
String value() default "";
}
@Component
@RateDiscountPolicyQualifier
public class RateDiscountPolicy implements DiscountPolicy {
// ...
}
@Autowired
public OrderServiceImpl(MemberRepository memberRepository,
@RateDiscountPolicyQualifier DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
이처럼 별도의 애너테이션을 만들어 적용하면 실수로 잘못된 값을 입력해 실행 중 예외가 발생하는 것을 예방할 수 있습니다.
그러나 이런 애너테이션을 무분별하게 재정의하면 유지보수면에서 좋지 않습니다. 되도록 제공되는 애너테이션을 활용하는 것이 좋습니다.
참고로 본래 자바에서의 애너테이션은 메타 애너테이션 외 애너테이션을 정의하려는 애너테이션 위에 적용할 수 없습니다. 위 소스 코드의 @Qualifier("rateDiscountPolicy")
처럼 애너테이션 위에 애너테이션을 적용하는 것은 스프링에서 제공하는 기능입니다.
@Qualifier
를 사용 시 의존관계를 변경하고 싶다면 클래스를 수정해야 하므로 아쉽게도 SOLID에서 O인 "소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야 한다"의 개방-폐쇄 원칙(OCP, Open-Closed Principle)을 위반하게 됩니다.
@Primary
@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy {}
@Component
@Qualifier("fixDiscountPolicy")
public class FixDiscountPolicy implements DiscountPolicy {}
// @Primary가 적용된 RateDiscountPolicy 클래스의 인스턴스가 rateDiscountPolicy라는 이름의
// 스프링 빈으로 생성돼 해당 빈이 주입됨
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
여러 개의 스프링 빈을 찾을 때 @Primary
애너테이션이 적용된 클래스가 우선됩니다.
// @Primary가 적용된 빈이 있다 하더라도 @Qualifier가 우선됨
@Autowired
public OrderServiceImpl(MemberRepository memberRepository,
@Qualifier("fixDiscountPolicy") DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
다만, 빈을 찾을 때 @Qualifier
를 적용하면 @Qualifier
가 적용된 스프링 빈을 먼저 찾고 없으면 @Primary
가 적용된 스프링 빈을 선택합니다.
같은 타입의 빈이 여러 개 필요할 때
같은 타입의 빈이 여러 개 필요한 경우 List나 Map으로 스프링 빈을 담아 다형성을 유지하며 동적으로 스프링 빈을 선택해 사용할 수 있습니다.
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);
int rateDiscountPrice = discountService.discount(member, 20000, "rateDiscountPolicy");
assertThat(rateDiscountPrice).isEqualTo(2000);
}
static 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;
System.out.println("policyMap = " + policyMap);
System.out.println("policies = " + policies);
}
public int discount(Member member, int price, String discountCode) {
DiscountPolicy discountPolicy = policyMap.get(discountCode);
return discountPolicy.discount(member, price);
}
}
}