다양한 의존 관계 주입 방법
- 의존 관계 주입 4가지 방법
- 생성자 주입
- 수정자 주입(setter)
- 필드주입
- 일반 메서드 주입
- 생성자 주입
- 이름 그대로 생성자를 통해서 의존 관계를 주입받는 방법
- 지금까지 우리가 진행했던 방법이 바로 생성자 주입
- 특징
- 생성자 호출 시점에 딱 1번만 호출되는 것이 보장된다.
- “불변, 필수” 의존관계에서 사용
- 중요! 생성자가 딱 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의 기본 동작은 주입할 대상이 없으면 오류가 발생한다. 주입할 대상이 없어도 동작하게 하려면 @Autowired(requried=false)로 지정
- 참고. 자바빈 프로퍼티, 자바에서 과거부터 필드의 값을 직접 변경하지 않고, getter/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; } //... }
- 필드 주입
- 필드에 바로 주입하는 방법
- 특징
- 코드가 간결해서 많은 개발자들을 유혹하지만 외부에서 변경이 불가능해서 테스트하기 어렵다는 단점이 있다.
- DI 프레임워크가 없으면 아무것도 할 수 없다.
- 사용하지 말자.
- 애플리케이션의 실제 코드와 관계없는 테스트 코드에서만 사용
- 스프링 설정을 목적으로 하는 @Configuration같은 곳에서만 특별한 용도로 사용
@Component public class OrderServiceImpl implements OrderService { @Autowired private MemberRepository memberRepository; @Autowired private DiscountPolicy discountPolicy; //... }
- 일반 메서드 주입
- 일반 메서드를 통해서 주입 받을 수 있다.
- 특징
- 한번에 여러 필드를 주입 받을 수 있다.
- 일반적으로 잘 사용하지 않는다.
- 참고. 의존관계 자동 주입은 스프링 컨테이너가 관리하는 스프링 빈이어야 동작한다. 스프링 빈이 아닌 Member 같은 클래스에서 @Autowired 코드를 적용해도 아무 기능도 동작하지 않는다.
@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; } //... }
옵션 처리
- 주입할 스프링 빈이 없어도 동작해야 할 때가 있다.
-
@Autowired만 사용하면 required 옵션이 true로 되어 있어서 자동 주입 대상이 없으면 오류가 발생한다.
- 자동 주입 대상을 옵션으로 처리하는 방법은 다음과 같다
- @Autowired(required=false): 자동 주입할 대상이 없으면 수정자 메서드 자체가 호출 안된다.
- org.springframework.lang.@Nullable: 자동 주입할 대상이 없으면 null이 입력된다.
- Optional<>: 자동 주입할 대상이 없으면 Optional.empty가 입력된다.
-
예제, Member가 스프링 빈이 아니다.
@Autowired(required = false) public void setNoBean1(Member member) { System.out.println("setNoBean1 = " + member); } @Autowired public void setNoBean2(@Nullable Member member) { System.out.println("setNoBean2 = " + member); } @Autowired(required = false) public void setNoBean3(Optional<Member> member) { System.out.println("setNoBean3 = " + member); }
- setNoBean1()은 @Autowired(required=false)이므로 호출 자체가 안된다.
- 출력 결과
setNoBean2 = null setNoBean3 = Optional.empty
- 참고. @Nullable, Optional은 스프링 전반에 걸쳐서 지원된다. 예를 들어서 생성자 자동 주입에서 특정 필드에만 사용해도 된다.
생성자 주입을 선택해라!
- 과거에는 수정자 주입과 필드 주입을 많이 사용했다.
-
최근에는 스프링을 포함한 DI 프레임워크 대부분이 생성자 주입을 권장한다.
- 특징1. 불변
- 대부분의 의존관계 주입은 한번 일어나면 애플리케이션 종료시점까지 의존 관계를 변경할 일이 없다.
- 오히려 대부분의 의존관계는 애플리케이션 종료 전까지 변하면 안된다.(불변)
- 수정자 주입을 사용하면 setter 메서드를 public으로 열어두어야 한다.
- 누군가 실수로 변경할 수도 있고, 변경하면 안되는 메서드를 열어두는 것은 좋은 설계 방법이 아니다.
- 생성자 주입은 객체를 생성할 때 딱 1번만 호출되므로 이후에 호출되는 일이 없다. 따라서 불변하게 설계할 수 있다.
- 누락
- 프레임워크 없이 순수한 자바 코드를 단위 테스트 하는 경우에 수정자 의존관계인 경우
- 예제
- 수정자 주입
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; } //... }
- 테스트 코드
@Test void createOrder() { OrderServiceImpl orderService = new OrderServiceImpl(); orderService.createOrder(1L, "itemA", 10000); }
- NullPointExceptoin이 발생한다.
- Spring을 사용하지 않았기 때문에 의존관계 주입이 누락되었다.
- 수정자 주입
- final 키워드
- 생성자 주입을 사용하면 필드에 final 키워드를 사용할 수 있다.
- 그래서 생성자에서 혹시라도 값이 설정되지 않는 오류를 컴파일 시점에 막아준다.
- 예제
@Component public class OrderServiceImpl implements OrderService { private final MemberRepository memberRepository; private final DiscountPolicy discountPolicy; @Autowired public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) { this.memberRepository = memberRepository; } //... }
- 생성자에서 DisocuntPolicy를 세팅하지 않았다.
- 자바는 컴파일 시점에서 다음 오류를 발생시킨다.
java: variable discountPolicy might not have been initialized
- 컴파일 오류는 세상에서 가장 빠르고, 좋은 오류다!!
- 참고. 수정자 주입을 포함한 나머지 주입 방식은 모두 생성자 이후에 호출되므로, 필드에 final 키워드를 사용할 수 없다.
- 오직, 생성자 주입 방식만 final 키워드를 사용할 수 있다.
- 정리
- 생성자 주입 방식을 선택하는 이유는 여러가지가 있지만, 프레임워크에 의존하지 않고, 순수한 자바 언어의 특징을 잘 살리는 방법이기도 하다.
- 기본으로 생성자 주입을 사용하고, 필수 값이 아닌 경우에는 수정자 주입 방식을 옵션으로 부여하면 된다. 생성자 주입과 수정자 주입을 동시에 사용할 수 있다.
- 항상 생성자 주입을 선택해라! 그리고 가끔 옵션이 필요하면 수정자 주입을 선택해라. 필드 주입은 사용하지 않는게 좋다
롬복과 최신 트랜드
- 대부분이 불변이고, 다음과 같이 생성자에 final 키워드를 사용한다.
- 롬복을 활용하면 간편하게 생성자 주입을 할 수 있다.
- 예시
@Component @RequiredArgsConstructor public class OrderServiceImpl implements OrderService { private final MemberRepository memberRepository; private final DiscountPolicy discountPolicy; }
- @RequiredArgsConstructor 기능을 사용하면 final이 붙은 필드를 모아서 생성자를 자동으로 만들어준다.
조회 빈이 2개 이상일 때 해결 방법
- 문제 발생
-
@Autowired는 타입(Type)으로 조회한다.
@Autowired private DiscountPolicy discountPolicy;
- 타입으로 조회하기 때문에 마치 다음 코드와 유사하게 동작한다.
- 실제로는 더 많은 기능 제공
ac.getBean(DiscountPolicy.class);
- 스프링 빈 조회에서 학습했듯이 타입으로 조회하면 선택된 빈이 2개 이상일 때 문제가 발생한다.
-
DiscountPolicy 하위 타입인 FixDiscountPolicy, RateDiscountPolicy 둘 다 스프링 빈으로 선언
@Component public class FixDiscountPolicy implements DiscountPolicy {}
@Component public class RateDiscountPolicy implements DiscountPolicy {}
- NoUniqueBeanDefinitionException 오류가 발생한다.
- 이 때 하위 타입으로 빈을 주입 받을 수 있지만 DIP에 위배되며 유연성이 떨어진다.
-
-
- 해결 방법 1. @Autowired 필드명
- @Autowired는 타입 매칭을 시도하고, 이 때 여러 빈이 있으면 필드 이름, 파라미터 이름으로 빈 이름을 추가 매칭한다.
@Autowired private DiscountPolicy disocuntPolicy;
- 해당에 경우 필드 이름(discountPolicy)가 상위 타입과 동일하기 때문에 NoUniqueBeanDefinitionException 발생
@Autowired private DiscountPolicy rateDiscountPolicy;
- 해당에 경우 필드 이름(rateDiscountPolicy)이므로 등록된 빈 중 RateDiscountPolicy가 등록된다.
- 해당에 경우 필드 이름(discountPolicy)가 상위 타입과 동일하기 때문에 NoUniqueBeanDefinitionException 발생
- 타입 매칭을 먼저 시도하고, 그 결과에 여러 빈이 있을 때 추가로 동작하는 기능
- @Autowired는 타입 매칭을 시도하고, 이 때 여러 빈이 있으면 필드 이름, 파라미터 이름으로 빈 이름을 추가 매칭한다.
- 해결 방법 2. @Qualifier 사용
- @Qualifier는 추가 구분자를 붙여주는 방법이다.
-
주입 시 추가적인 방법을 제공하는 것인지 빈 이름을 변경하는 것은 아니다.
-
주입 시 @Qualifier를 붙여주고 등록한 이름을 적어준다.
@Component @Qualifier("mainDiscountPolicy") public class RateDiscountPolicy implements DiscountPolicy {}
@Component @Qualifier("fixDiscountPolicy") public class FixDiscountPolicy implements DiscountPolicy {}
-
생성자 자동 주입 예시
@Autowired public OrderService(MemberRepository memberRepository, @Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy) { this.memberRepository = memberRepository; this.discountPolicy = discountPolicy; }
-
수정자 자동 주입 예시
@Autowired public DiscountPolicy setDiscountPolicy(@Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy) { this.discountPolicy = discountPolicy; }
- @Qualifier로 주입할 때 @Qualifier(“mainDiscountPolicy”)를 못찾으면 어떻게 될까?
- 먼저 @Qualifier(“mainDiscountPolicy”) 가 붙은 빈을 찾는다.
- 없다면 mainDiscountPolicy라는 이름의 스프링 빈을 추가로 찾는다.
- 하지만 경험상 @Qualifier는 매칭하여 @Qualifier를 찾는 용도로만 사용하는게 면확하고 좋다.
- 해결 방법 3. @Primary
- @Primary는 우선순위를 정하는 방법이다.
-
@Autowired 시에 여러 빈이 매칭되면 @Primary 우선권을 가진다.
@Component @Primary public class RateDiscountPolicy implements DiscountPolicy {} @Component public class FixDiscountPolicy implements DiscountPolicy {}
- DiscountPolicy 타입에 생성자 주입은 우선순위가 높은 RateDiscountPolicy가 동작한다.
- @Primary vs @Qualifier
- @Qualifier는 빈 등록, 빈 주입시에 모두 사용하여 코드가 길어지는 불편한 점이 있다.
- 주 사용 빈은 @Primary로 등록하여 사용하고, 특별한 경우에 사용할 경우 @Qualifier를 사용하자
- 만약 Main DB의 커넥션을 획득하는 빈은 @Primary로 사용하고, Sub DB의 커넥션을 획득하는 빈은 @Qualifier를 지정한다.
- Sub DB의 커넥션 정보를 주입받는 경우에만 @Qualifier를 지정하여 사용할 수 있게끔 한다.
- 즉, 우선순위는 @Qualifier가 @Primary보다 높다.
- @Primary는 기본값처럼 동작하며, @Qualifier는 매우 상세하게 동작한다.
- 스프링은 좁은 범위의 선택권에 우선 순위를 높게 주기 때문에 빈을 주입 받는 곳에 @Qualifier가 있으면 해당 타입의 빈을 주입한다.
조회한 빈이 모두 필요할 때: 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, "test", Grade.VIP);
assertNotNull(discountService);
int rateDiscountPrice = discountService.discount(member, 10000, "rateDiscountPolicy");
assertEquals(rateDicountPrice, 1000);
}
static class DiscountService {
Map<String, DiscountPolicy> policyMap;
List<DiscountPolicy> policyList;
@Autowired
public DiscountService(Map<String, DiscountPolicy> policyMap, List<DiscountPolicy> policyList) {
this.policyMap = policyMap;
this.policyList = policyList;
}
public int discount(Member member, int price, String dicountPolicyCode) {
DiscountPolicy discountPolicy = policyMap.get(discountPolicyCode);
return discountPolicy.discount(member, price);
}
}
}
- 로직 분석
- DiscountService는 Map 또는 List로 모든 DiscountPolicy를 주입받는다. 이 때, fixDiscountPolicy, rateDiscountPolicy가 주입된다.
- discount() 메서드는 discountCode(key)로 해당하는 DiscountPolicy(value)를 찾아서 실행한다.
- 주입 분석
- Map<String, DiscountPolicy>: map의 키에 스프링 빈의 이름을 넣어주고, 그 값으로 DiscountPolicy 타입으로 조회한 모든 스프링 빈을 담아준다.
- List
: DiscountPolicy 타입으로 조회한 모든 스프링 빈을 담아준다. - 만약 해당하는 타입의 스프링 빈이 없으면, 빈 컬렉션이나 Map을 주입한다.
빈 주입 과정 - 어떻게 빈을 찾는가?
- Spring Bean LifeCycle
- 스프링 컨테이너 생성 -> 스프링 빈 생성 -> 의존성 주입 -> 초기화 콜백(빈의 의존관계 주입이 완료된 후 호출) -> 사용 -> 소멸전 콜백(빈이 소멸되기 직전 호출) -> 종료
- 빈 인스턴스 화 및 DI
- XML, Annotation 빈 정의를 스캔
- 빈 인스턴스 생성
- 빈 프로퍼티에 의존성 주입
- 검사
- Bean이 BeanNameAware 인터페이스를 구현 시, setBeanName() 호출
- Bean이 BeanClassLoaderAware 인터페이스 구현 시, setBeanClassLoader() 호출
- Bean이 ApplicationContextAware 인터페이스 구현 시 setApplicationContext() 호출
- 빈 생성 생명주기 콜백
- @PostConstruct -> Bean이 InitializingBean 인터페이스 구현 시 afterPropertiesSet() 호출 -> init-method 정의할 시 지정한 메소드 호출
- 빈 소멸 생명주기 콜백
- @PreDestroy -> Bean이 DisposableBean 인터페이스 구현 시 destroy() 호출 -> destroy-method 정의할 시 지정한 메소드 호출
- Scope이 Prototype일 경우 소멸 생명주기 콜백은 호출되지 않음
- 빈 생성 페이즈
- Instantiation: 스프링은 마치 우리가 수동으로 자바 객체를 생성할 때 처럼 빈 객체를 초기화 한다.
- Populating Properties: 객체를 초기화한 후 스프링은 Aware 인터페이스를 구현한 빈을 스캔하고 관련된 프로퍼티를 세팅하기 시작한다.
- Pre-Initialization: 스프링의 BeanPostProcessors가 이 페이즈에서 활용된다. postProcessBeforeInitialization() 메서드들이 그들의 잡을 한다. 또한 @PostConstruct가 달린 메서드가 그 후에 바로 실행된다.
- AfterPropertiesSet: 스프링은 InitializingBean 인터페이스를 구현한 빈들의 afterPropertiesSet() 메서드들을 실행한다.
- Custom Initialization: 스프링은 @Bean 어노테이션의 initMethod 어트리뷰트에 정의한 초기화 메서드를 트리거한다.
- Post-Initialization: 스프링의 BeanPostProcessors가 또 한 번 작동된다. 이 페이즈에서는 postProcessAfterInitialization()메서드를 트리거한다.
- BeanPostProcessor
@Nullable default Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { return bean; } @Nullable default Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { return bean; }
- 빈의 인스턴스가 만들어지는 라이프사이클에서 빈의 초기화 단계의 이전과 이후에 다른 부가적인 작업을 할 수 있게 해주는 인터페이스이다.
- postProcessBeforeInitialization 메소드와 postProcessAfterInitialization 메소드가 존재하는데 초기화 단계 이전과 이후에 해당 메서드가 실행되어 빈을 찾을 수 있다.
- AutowiredAnnotationBeanPostProcessor
public void processInjection(Object bean) throws BeanCreationException { Class<?> clazz = bean.getClass(); InjectionMetadata metadata = findAutowiringMetadata(clazz.getName(), clazz, null); try { metadata.inject(bean, null, null); } catch (BeanCreationException ex) { throw ex; } catch (Throwable ex) { throw new BeanCreationException( "Injection of autowired dependencies failed for class [" + clazz + "]", ex); } }
- BeanPostProcessor 에 구현 객체로 필드, 메서드, 클래스에 대한 의존관계를 주입해준다.
- processInjection 메서드를 보면 빈의 클래스 정보를 가져와 Metadata를 찾아 주입하는 역할을 한다.
- inject 메서드를 보면 리플렉션을 통해 빈을 주입한다.
- AbstractAutowireCapableBeanFactory
- BeanFactory는 BeanPostProcessor 타입의 빈을 꺼내 일반적인 빈들을 @Autowired 로 의존관계 주입이 필요한 빈들에게 @Autowired를 처리하는 로직을 적용한다.
** 출처: 인프런 스프링핵심원리 - 기본편, 김영한님 강연