의존관계 주입은 크게 4가지 방법이 있다.
생성자를 통해서 의존 관계를 주입 받는 방식으로 생성자 호출시점에 딱 1번만 호출되는 것이 보장된다.
불변, 필수 의존관계에 사용된다.
@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;
}
}
DI된 스프링 빈들이 final 키워드를 붙여 필수값인 경우 생성자 주입을 통해 의존관계를 주입한다.
생성자가 딱 1개만 있다면, @Autowired
를 생략해도 자동 주입 된다. 이는 스프링 빈에만 해당된다.
생성자 주입은 스프링 빈에서 해당 클래스를 생성하는 시점에 생성자를 호출할 수 밖에 없는데 해당 생성자에 파라미터를 전달하기 위해서 의존관계를 주입해줘야 한다.
자바 관례인 getter/setter 중 setter를 통해 필드의 값을 수정하는 방식으로 의존관계를 주입하는 방법이다.
@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
애노테이션을 부여해서 의존관계를 주입한다. 애노테이션이 부여되지 않으면 Null
상태이다. 수정자 주입은 아직 의존관계로 주입되어야 할 스프링 빈이 등록되지 않아 Null
상태일 때도 사용이 가능하다.
참고로,
@Autowired
의 기본동작은 주입할 대상이 없으면 오류가 발생한다. 주입할 대상이 없어도 동작하려면@Autowired(required = false)
로 지정하면 된다.
필드에 바로 주입하는 방식이다.
@Configuration
같은 곳에서만 특별한 용도로 사용됨.@Component
public class OrderServiceImpl implements OrderService {
@Autowired
private MemberRepository memberRepository;
@Autowired
private DiscountPolicy discountPolicy;
}
일반 메서드를 통해서 주입받을 수 있다.
한번에 여러 필드를 주입 받을 수 있지만, 일반적으로 잘 사용하지 않는다.
// 대부분의 경우 생성자 주입이나 수정자 주입으로 다 해결됨.
@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
가 입력된다.package hello.core.autowired;
import hello.core.member.Member;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.lang.Nullable;
import java.util.Optional;
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) { // 자동 주입할 대상이 없으면 null이 입력된다.
System.out.println("noBean2 = " + noBean2);
}
@Autowired
public void setNoBean3(Optional<Member> noBean3) { // 자동 주입할 대상이 없으면 Optional.empty가 입력된다.
System.out.println("noBean3 = " + noBean3);
}
}
}
실행 결과는 다음과 같다.
noBean2 = Null
noBean3 = Optional.empty
noBean1
은 스프링 빈이 아니다. 따라서 setNoBean1()
은 @Autowired(required = false)
이므로 호출 자체가 안된다.noBean2
는 해당 빈을 찾을 수 없지만, @Nullable
애노테이션이 있기 때문에 Null을 주입한다.noBean3
는 Optional<>
파라미터이므로 Optional.empty
를 주입한다.과거에는 수정자 주입과 필드 주입을 많이 사용했지만, 최근에는 스프링을 포함한 DI 프레임워크 대부분이 생성자 주입을 권장한다. 그 이유는 다음과 같다.
프레임워크에 의존하지 않고 순수 자바로 단위 테스트를 하는 경우에 수정자 의존관계인 또는 필드 의존관계인 경우 @Autowired
가 프레임워크 안에서 문제가 있을 경우 오류를 발생한다. 순수 자바로 짤 경우 실행은 되지만, NullPointException
이 발생한다. 의존관계 주입이 누락되었기 때문이다.
생성자 주입을 사용하면 주입 데이터가 누락 됐을 때 컴파일 오류가 발생한다. 그리고 IDE에서 바로 어떤 값을 필수로 주입해야 하는지 알 수 있다.
@Test
void createOrder() {
OrderServiceImpl orderService = new OrderServiceImpl();
orderService.createOrder(1L, "itemA", 10000);
}
정리하자면,
최근에는 생성자를 딱 1개 두고, Lombok 라이브러리의 @RequiredArgsConstructor
를 사용해서 @Autowired
를 생략하는 방법을 주로 사용한다. 이 방법을 사용하면 기능은 다 제공하면서, 코드는 깔끔하게 사용된다.
@Component
@RequiredArgsConstructor // final이 붙은 필드를 모아 생성자를 자동으로 만들어 줌.
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
}
@Autowired
는 타입으로 조회한다. 같은 타입의 빈이 둘 이상 등록될 경우 문제가 생길 수 있다.
@Component
public class FixDiscountPolicy implements DiscountPolicy {}
@Component
public class RateDiscountPolicy implements DiscountPolicy {}
public class AutoAppConfigTest {
@Autowired
private DiscountPolicy discountPolicy;
...
}
위와 같은 코드가 있다고 가정하면, NoUniqueBeanDefinitionException
오류가 발생한다.
NoUniqueBeanDefinitionException : No qualifying bean of type
'hello.core.discount.DiscountPolicy' available: expected single matching bean
but found 2: fixDiscountPolicy,rateDiscountPolicy
오류메시지를 확인해보면, 어떤 빈이 겹치는지 상세하게 나타난다.
이런 문제를 해결하기 위한 방법은 @Autowired
가 부여된 필드의 이름, @Qualifer
, @Primary
를 사용해 해결할 수 있다.
@Autowired
필드 명, @Qualifer
, @Primary
@Autowired
필드 명 매칭public class AutoAppConfigTest {
// 기존 코드
// @Autowired
// private DiscountPolicy discountPolicy;
// 변경 코드
@Autowired
private DiscountPolicy rateDiscountPolicy;
...
}
@Autowired
는 타입 매칭을 우선적으로 시도하고, 여러 빈이 있으면 필드 이름, 파라미터 이름으로 빈 이름을 추가 매칭한다.
위의 변경 코드처럼 rateDiscountPolicy
와 같이 필드명을 빈 이름으로 변경하게 되면 정상 주입된다.
정리하자면, @Autowired
는 우선 타입 매칭을 시도하고 타입 매칭의 결과가 2개 이상일 때 필드명, 파라미터명으로 빈 이름을 매칭한다.
@Qualifer
@Qualifer
는 추가 구분자를 붙여주는 방법이다. 주입시 추가적인 방법을 제공하는 것이지 빈 이름을 변경하는 것은 아니다.
// 다음과 같이 빈 등록시 @Qualifer를 붙여준다.
@Component
@Qualifer("mainDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy {}
@Component
@Qualifer("fixDiscountPolicy")
public class FixDiscountPolicy implements DiscountPolicy {}
// 주입시에 @Qualifer를 붙여주고 등록한 이름을 적어준다.
@Autowired
public OrderServiceImpl(MemberRepository memberRepository,
@Qualifer("mainDiscountPolicy") DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
OrderServiceImpl
빈에 의존관계 주입 시 할인 정책은 @Qualifer
로 등록된 이름을 통해 매칭하게 된다.
필드 뿐 아니라 생성자 및 수정자에도 파라미터에 해당 애노테이션을 부여해서 매칭할 수 있다.
Component
뿐 아니라 @Bean
을 직접 등록하는 경우에도 동일하게 사용가능하며, 매칭되는 빈을 찾지 못한다면 NoSuchBeanDefinitionException
예외가 발생한다.
@Primary
사용@Primary
는 이름 뜻 그대로 우선순위를 정하는 방법이다. @Autowired
를 통해 여러 빈이 매칭될 경우 해당 애노테이션이 우선권을 가진다.
@Component
@Primary // rateDiscountPolicy가 우선권을 가지게 되어 이 빈이 자동 주입된다.
public class RateDiscountPolicy implements DiscountPolicy {}
@Component
public class FixDiscountPolicy implements DiscountPolicy {}
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
@Primary
는 @Qualifier
와 달리 양쪽 모두에 애노테이션을 붙여주지 않아도 된다는 장점이 있고 상대적으로 더 많이 쓰인다.
두 애노테이션 간의 우선순위는 어느 것이 더 높을까?
@Primary
는 기본값 처럼 동작하고, @Qualifier
는 매우 상세하게 동작한다. 스프링은 자동보다는 수동이, 넓은 범위의 선택권 보다는 좁은 범위의 선택권이 우선순위가 높기 때문에 @Qualifier
가 우선순위를 가진다.
@Qualifier
를 사용하다보면 지정한 이름이 오타가 발생해도 컴파일시 타입 체크가 안된다.
이런 경우 애노테이션을 직접 만들어서 해결할 수 있다.
package hello.core.annotation;
import org.springframework.beans.factory.annotation.Qualifier;
import java.lang.annotation.*;
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER,
ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Qualifier("mainDiscountPolicy")
public @interface MainDiscountPolicy {
}
이 애노테이션은 필드, 메소드, 파라미터, 타입, 애노테이션타입에 전부 사용 가능하다. Retention에 옵션으로 런타임으로 두었기 때문에 런타임시에 동작한다. @Qualifier
지정을 했기 때문에 해당 동작 역시 수행한다.
@Component
@MainDiscountPolicy
public class RateDiscountPolicy implements DiscountPolicy { ... }
@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
@MainDiscountPolicy
private final DiscountPolicy discountPolicy;
...
}
📌 주의!
이대로 실행해보면 롬복의@RequiredArgsConstructor
때문에 에러가 발생한다. 애노테이션 생성자 주입방식을 사용한다면 발생하지 않을 에러이다. 이는 해당 롬복 애노테이션이 애노테이션을 포함해서 생성자를 만들지 않기 때문에 사용자 정의 애노테이션 뿐 아니라@Qualifier
를 사용했어도 에러가 발생할 것이다. 따라서 롬복 설정을 해줘야 한다.
src/main/java/lombok.config
생성- 파일 내에 다음 설정을 적용한다.
lombok.copyableAnnotations += org.springframework.beans.factory.annotation.Qualifier lombok.copyableAnnotations += hello.core.annotation.MainDiscountPolicy
- IntelliJ인 경우 out 폴더 삭제. gradlew인 경우 clean 실행
- 다시 컴파일
참고로 애노테이션에는 상속이라는 개념이 없다. 스프링은 이처럼 여러 애노테이션을 모아서 사용하는 기능을 지원해준다.
자동주입을 위해 스프링 빈을 검색 시 2개 이상 나오는 경우 하나를 골라 주입하는 방법은 어떤지 알아보았다. 반대로, 의도적으로 같은 타입의 스프링 빈이 모두 필요할 때가 있다.
// 할인 정책을 동적으로 변경 - 클라이언트가 할인의 종류를 선택하는 경우
이럴 때 스프링을 사용하면 전략 패턴을 매우 간단하게 구현할 수 있다.
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 discount = 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);
}
}
}
discount()
에서 인자로 받은 discountCode를 스프링 빈의 이름으로 policyMap에서 꺼내 걸맞는 할인 정책을 반환하고 이 정책의 discount()
를 호출한다.
DiscountService
는 생성자가 Map, List 타입이니 해당 컬렉션의 지네릭 타입에 매칭되는 모든 빈들이 주입된다. Map의 키에 스프링 빈의 이름을 넣어주고, 그 값으로 DiscountPolicy
타입으로 조회한 모든 스프링 빈들을 담아주는 것이다.
만약, 해당 타입에 맞는 빈이 없다면 빈 컬렉션이나 Map을 주입한다.
결론적으론 편리한 자동 기능을 최대한 기본으로 사용하자.
스프링이 나오고 시간이 갈수록 점점 자동을 선호하는 추세라고 한다. @Component
뿐만 아니라 @Controller
, @Service
, @Repository
처럼 계층에 맞추어 일반적인 애플리케이션 로직을 자동으로 스캔할 수 있도록 지원한다. 거기에 더해 최근 스프링 부트는 컴포넌트 스캔을 기본으로 사용하고, 스프링 부트의 다양한 스프링 빈들도 조건이 맞으면 자동으로 등록하도록 설계했다.
개발자 입장에서 스프링 빈을 하나 등록할 때 @Component
만 넣어주면 되는 상황에서 직접 @Configuration
설정 정보에 가서 @Bean
을 적고, 객체를 생성하고, 주입할 대상을 일일이 작성하는 과정은 상당히 번거로울 뿐더러 관리할 빈이 많아져 설정 정보가 커질 수록 이를 관리하는 것 자체가 소모적이다.
게다가 자동 빈 등록 기능을 사용해도 OCP, DIP 원칙을 지킬 수 있다.
애플리케이션은 크게 업무 로직과 기술 지원 로직으로 나눌 수 있다.
애플리케이션에서 광범위하게 영향을 미치는 기술 지원 객체는 수동 빈으로 등록해서 설정 정보에 바로 나타나게 하는 것이 유지보수에 좋다.
좀 전의 예시 처럼 의존관계 자동 주입, 조회한 빈이 모두 필요할 땐 수동 빈으로 등록하거나 또는 자동으로 등록하면서 특정 패키지에 같이 묶어두는게 좋다.
// 자동 등록 시 패키지 구조
ㄴ discount
ㄴ DiscountPolicy.java
ㄴ FixDiscountPolicy.java
ㄴ RateDiscountPolicy.java
ㄴ member
ㄴ order
...
// 수동 빈 등록 시 별도의 설정 정보로 만든다.
@Configuration
public class DiscountPolicyConfig {
@Bean
public DiscountPolicy rateDiscountPolicy() {
return new RateDiscountPolicy();
}
@Bean
public DiscountPolicy fixDiscountPolicy() {
return new FixDiscountPolicy();
}
}