@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;
}
...
}
@Component
가 달리 클래스를 빈에 등록한다.@Autowired
를 확인하고 스프링 컨테이너에서 필요한 빈을 주입한다.private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
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;
this.discountPolicy = discountPolicy;
}
...
}
@Autowired
를 생략해도 자동 주입 된다. 물론 스프링 빈에만 해당한다.@Autowired
를 통해 지정해주어야 한다.@Autowired(required = false)
로 지정하여야 한다.@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;
}
...
}
final
키워드가 없다.@Component
public class OrderServiceImpl implements OrderService{
@Autowired private MemberRepository memberRepository;
@Autowired private DiscountPolicy discountPolicy;
...
}
@Autowired
가 동작하지 않는다. @SpringBootTest
처럼 스프링 컨테이너를 테스트에 통합한 경우에만 가능하다.@Test
void fieldInjectionTest(){
OrderServiceImpl orderService = new OrderServiceImpl();
orderService.createOrder(1L, "itemA", 10000);
}
memberRepository
와 discountPolicy
는 모두 null
이다.memberRepository
에 접근할 경우 NPE가 발생할 수 밖에 없다.@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
를 사용하여 의존관계를 주입할 때 컨테이너에 아직 등록되지 않은 빈이 있을 수 있다.org.springframework.beans.factory.UnsatisfiedDependencyException: ...
@Autowired
만 사용하면 required
옵션의 기본 값이 true
이므로, 자동 주입 대상이 없으면 UnsatisfiedDependencyException
가 발생한다.@Autowired(required=false)
: 자동 주입할 대상이 없으면 수정자 메서드 자체가 호출 안된다.org.springframework.lang.@Nullable
: 자동 주입할 대상이 없으면 null이 입력된다.Optional<>
: 자동 주입할 대상이 없으면 Optional.empty 가 입력된다.코드
@Autowired(required = true)
public void setNoBean1(Member noBean1) {
System.out.println("noBean1 = " + noBean1);
}
결과
// 아무것도 출력되지 않는다
Member
는 스프링 빈이 아니므로, 컨테이너에 등록되어있지 않다.@Autowired
로 되어있는 메소드 자체를 호출하지 않는다.@Autowired
public void setNoBean2(@Nullable Member noBean2){
System.out.println("noBean2 = " + noBean2);
}
결과
noBean2 = null
Member
는 스프링 빈이 아니므로, 컨테이너에 등록되어있지 않다.@Autowired(required=false)
와 달리, null
을 반환한다.@Autowired
public void setNoBean3(Optional<Member> noBean3) {
System.out.println("noBean3 = " + noBean3);
}
결과
noBean3 = Optional.empty
Member
는 스프링 빈이 아니므로, 컨테이너에 등록되어있지 않다.Optional
클래스의 empty
를 반환한다.@Nullable
, Optional
은 스프링 전반에 걸쳐서 지원된다.
예를 들어서 생성자 자동 주입에서 특정 필드에만 사용해도 된다.
최근 스프링을 포함한 DI 프레임워크 대부분이 생성자 주입을 권장한다.
@Test
void creatOrder(){
OrderServiceImpl orderService = new OrderServiceImpl();
Order order = orderService.createOrder(1L, "itemA", 10000);
assertThat(order.getDiscountPrice()).isEqualTo(1000);
}
discountPolicy
, memberRepository
에 대한 의존관계가 누락되었기 때문이다.@Component
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
}
...
}
final
키워드를 사용할 수 있다.final
키워드를사용할 수 없다. 오직 생성자 주입 방식만 final
키워드를 사용할 수 있다.required = false
@Nullable
Optional<>
Test 코드
@Test
void creatOrder(){
MemoryMemberRepository memberRepository = new MemoryMemberRepository();
memberRepository.save(new Member(1L, "name", Grade.VIP));
OrderServiceImpl orderService = new OrderServiceImpl(new MemoryMemberRepository(), new FixDiscountPolicy());
Order order = orderService.createOrder(1L, "itemA", 10000);
assertThat(order.getDiscountPrice()).isEqualTo(1000);
}
MemoryMemberRepository
의 객체를 생성하였다.OrderServiceImpl
의 객체를 생성할 때, 생성자의 파라미터로 새로운 MemoryMemberRepository
객체를 생성하여 전달하였다.MemoryMemberRepository
객체가 생성되었는데 Member
조회가 어떻게 되는지 고민하였다.@Component
public class MemoryMemberRepository implements MemberRepository{
private static final Map<Long, Member> store = new HashMap<>();
...
}
MemoryMemberRepository
를 보면, 저장소가 static
으로 선언되어있다.static
으로 선언되었다는 것을 잊고, 서로 다른 MemoryMemberRepository
에 접근하였음에도 서로 다른 저장소에 접근 하는 것으로 생각하였다.@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;
}
...
}
@Component
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
...
}
@Autowired
를 생략할 수 있다@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
...
}
@RequiredArgsConstructor
기능을 사용하면 final이 붙은 필드를 모아서 생성자를 자동으로 만들어준다@Autowired
는 타입(Type)으로 조회한다@Autowired
private DiscountPolicy discountPolicy{...}
ac.getBean(DiscountPolicy.class)
@Component
public class FixDiscountPolicy implements DiscountPolicy {...}
@Component
public class RateDiscountPolicy implements DiscountPolicy {...}
DiscountPolcy
타입의 빈 2개를 등록하였다.@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService{
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
}
...
Caused by: org.springframework.beans.factory.NoUniqueBeanDefinitionException:
No qualifying bean of type 'hello.core.discount.DiscountPolicy' available:
expected single matching bean but found 2: fixDiscountPolicy,rateDiscountPolicy
...
NoUniqueBeanDefinitionException
오류가 발생한다.fixDiscountPolicy
, rateDiscountPolicy
총 2개의 빈이 matching되었다.조회 대상 빈이 2개 이상일 때 해결 방법
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy fixDiscountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = fixDiscountPolicy;
}
@Qualifier
는 추가 구분자를 붙여주는 방법이다. 주입시 추가적인 방법을 제공하는 것이지 빈 이름을 변경하는 것은 아니다.@Component
@Qualifier("mainDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy {...}
@Component
@Qualifier("fixDiscountPolicy")
public class FixDiscountPolicy implements DiscountPolicy {...}
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, @Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
@Qualifier
로 주입할 때 @Qualifier("mainDiscountPolicy")
를 못찾을 경우 mainDiscountPolicy
라는 이름의 스프링 빈을 추가로 찾는다. @Qualifier
를 찾는 용도로만 사용하는게 명확하고 좋다.@Primary
는 우선순위를 정하는 방법이다.@Autowired
시 여러 빈이 매칭 되면, @Primary
가 우선권을 갖는다.@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy {...}
@Component
public class FixDiscountPolicy implements DiscountPolicy {...}
@Primary
설정을 하면, 의존성 주입 시 수정할 부분이 없다.@Primary
를 적용@Qualifier
를 적용@Qualifier
의 지정 없이 스프링 빈 조회@Qualifier
의 명시적인 지정으로 스프링 빈 조회Qualifier
의 우선순위가 Primary
의 우선순위보다 높다.@Qualifier("mainDiscountPolicy")
와 같이 문자를 적을 경우, 컴파일 타임에 오류를 잡을 수 없다.@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Qualifier("mainDiscountPolicy")
public @interface MainDiscountPolicy {}
@Qualifier
어노테이션을 추가한다.@Component
@MainDiscountPolicy
public class RateDiscountPolicy implements DiscountPolicy {...}
@Qualifier
대신에 @MainDiscountPolicy
를 사용한다.public class OrderServiceImpl implements OrderService{
...
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, @MainDiscountPolicy DiscountPolicy discountPolicy) {
...
}
...
@MainDiscountPolicy
어노테이션을 추가한다.@Qulifier
뿐만 아니라 다른 애노테이션들도 함께 조합해서 사용할 수 있다. 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;
}
public int discount(Member member, int price, String discountCode) {
DiscountPolicy discountPolicy = policyMap.get(discountCode);
return discountPolicy.discount(member, price);
}
}
}
@Autowired
public DiscountService(Map<String, DiscountPolicy> policyMap, List<DiscountPolicy> policies) {
this.policyMap = policyMap;
this.policies = policies;
}
DiscountService
는 Map으로 모든 DiscountPolicy
를 주입받는다. String
과 DiscountPolicy
다.public int discount(Member member, int price, String discountCode) {
DiscountPolicy discountPolicy = policyMap.get(discountCode);
return discountPolicy.discount(member, price);
}
DiscountService
의 discount
메소드는 매개변수로 Member, 가격, 적용할 할인 정책을 전달 받는다.String
타입으로 해당 할인 정책을 Map에서 조회하여 해당 객체를 반환한다.discount
메소드를 호출하여 할인된 가격을 최종 반환한다.ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class, DiscountService.class);
DiscountService.class
뿐만 아니라, AutoAppConfig.class
또한 전달하여 필요한 스프링 빈이 등록되도록 한다.int discountPrice = discountService.discount(member, 10000, "fixDiscountPolicy");
int rateDiscountPrice = discountService.discount(member, 20000, "rateDiscountPolicy");
DiscountService
클래스의 discount
메소드를 호출 할 때 "fixDiscountPolicy"
를 전달하면 DiscountService
의 policyMap
을 조회하는데 key로 사용하게 된다."rateDiscountPolicy"
를 전달하면 DiscountService
의 policyMap
을 조회하는데 key로 "rateDiscountPolicy"
를 사용하게 된다.@Component
뿐만 아니라 @Controller
, @Service
, @Repository
처럼 계층에 맞추어 일반적인 애플리케이션 로직을 자동으로 스캔할 수 있도록 지원한다.웹을 지원하는 컨트롤러, 핵심 비즈니스 로직이 있는 서비스, 데이터 계층의 로직을 처리하는 리포지토리등이 모두 업무 로직이다. 보통 비즈니스 요구사항을 개발할 때 추가되거나 변경된다.
업무 로직은 숫자도 매우 많고, 한번 개발해야 하면 컨트롤러, 서비스, 리포지토리 처럼 어느정도 유사한 패턴이 있다. 이런 경우 자동 기능을 적극 사용하는 것이 좋다. 보통 문제가 발생해도 어떤 곳에서 문제가 발생했는지 명확하게 파악하기 쉽다.
기술적인 문제나 공통 관심사(AOP)를 처리할 때 주로 사용된다. 데이터베이스 연결이나, 공통 로그 처리 처럼 업무 로직을 지원하기 위한 하부 기술이나 공통 기술들이다.
기술 지원 로직은 업무 로직과 비교해서 그 수가 매우 적고, 보통 애플리케이션 전반에 걸쳐서 광범위하게 영향을 미친다. 그리고 업무 로직은 문제가 발생했을 때 어디가 문제인지 명확하게 잘 드러나지만, 기술 지원 로직은 적용이 잘 되고 있는지 아닌지 조차 파악하기 어려운 경우가 많다. 그래서 이런 기술 지원 로직들은 가급적 수동 빈 등록을 사용해서 명확하게 드러내는 것이 좋다.
애플리케이션에 광범위하게 영향을 미치는 기술 지원 객체는 수동 빈으로 등록해서 설정 정보에 바로 나타나게 하는 것이 유지보수 하기 좋다.
DiscountService
의 의존관계 자동 주입을 보게되면, Map<String, DiscountPolicy>
에 주입을 받는다. 어떤 빈들이 주입될 지, 각 빈들의 이름은 무엇일지 코드만 보고 한번에 쉽게 파악할 수 없다. 자동 등록을 사용하고 있기 때문에 파악하려면 여러 코드를 찾아봐야 한다.
이런 경우 수동 빈으로 등록하거나 또는 자동으로하면 특정 패키지에 같이 묶어두는게 좋다. 핵심은 딱 보고 이해가 되어야 한다.
즉, 다형성을 적극 활용하는 비즈니스 로직은 수동 등록을 고민해보아야 한다.
@Configuration
public class DiscountPolicyConfig {
@Bean
public DiscountPolicy rateDiscountPolicy() {
return new RateDiscountPolicy();
}
@Bean
public DiscountPolicy fixDiscountPolicy() {
return new FixDiscountPolicy();
}
}
설정 정보만 봐도 한눈에 빈의 이름은 물론이고, 어떤 빈들이 주입될지 파악할 수 있다. 그래도 빈 자동 등록을 사용하고 싶으면 파악하기 좋게 DiscountPolicy
의 구현 빈들만 따로 모아서 특정 패키지에 모아두는 것이 좋다.
출처: 인프런 스프링 핵심 원리 - 기본편 (김영한)
인프런 스프링 핵심 원리