본문 바로가기
Develop/Spring

[Spring/기본편] 의존관계 자동 주입

by J-rain 2024. 3. 19.

의존관계 자동 주입

  • 생성자에  @Autowired  를 지정하면, 스프링 컨테이너가 자동으로 스프링 빈을 찾아서 주입한다.
  • 생성자에 파라미터가 많아도 다 찾아서 자동으로 주입한다.
  • 이때 기본 조회 전략은 타입이 같은 빈을 찾아서 주입한다.
    •  getBean(MemberRepository.class)  와 동일하다고 이해하면 된다.

중복 등록과 충돌

Case1 - 자동 빈 등록 vs 자동 빈 등록 → 오류 ( ConflictingBeanDefinitionException ) 예외 발생

Case2 - 수동 빈 등록 vs 자동 빈 등록 → 수동 빈 등록이 자동 빈 등록 오버라이딩 해버린다.

 

 

다양한 의존관계 주입 방법

4가지 방법이 있다.

  • 생성자 주입
  • 수정자 주입(setter 주입)
  • 필드 주입
  • 일반 메서드 주입

생성자 주입

  • 특징
    • 생성자 호출시점에 딱 1번만 호출되는 것이 보장된다.
    • 불변,필수 의존관계에 사용
@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  생략해도 자동 주입 된다.

 

수정자 주입(setter 주입)

  • setter라 불리는 필드의 값을 변경하는 수정자 메서드를 통해 주입하는 방법
  • 특징
    • 선택, 변경 가능성이 있는 의존관계에 사용
    • 자바빈 프로퍼티 규약의 수정자 메서드 방식을 사용
@Component
public class OrderServiceImpl implements OrderService {

    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

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

@Autowired 의 기본 동작은 주입할 대상이 없으면 오류가 발생하지만 대상 없이도 동작하게 하려면 @Autowired(required = false) 로 지정하면 동작한다.

 

필드 주입

  • 필드에 바로 주입하는 방법
  • 특징
    • 코드가 간결하지만 외부에서 변경이 불가능해서 테스트하기 어려운 단점이 있다.
    • DI 프레임워크가 없으면 아무것도 할 수 없다.
    • 사용 하지말자!

일반 메서드 주입

  • 일반 메서드를 통해서 주입 받을 수 있다.
  • 특징
    • 한번에 여러필드를 주입 받을 수 있다.
    • 일반적으로 잘 사용하지는 않는다.

참고: 당연한 이야기지만 의존관계 자동 주입은 스프링 컨테이너가 관리하는 스프링 빈이어야 동작한다. 스프링 빈이 아닌 일반 자바(Member 같은) 클래스에서 @Autowired 를 적용해도 동작하지 않는다.

 

 

 

 

옵션 처리

주입할 스프링 빈이 없어도 동작해야 할 때가 있다.

 @Autowired  만 사용하면 required  옵션의 기본값이  true  로 되어 있어서 오류가 발생한다.

자동 주입 대상을 옵션으로 처리하는 방법은 다음과 같다.

  •  @Autowired(required = false)  : 자동 주입할 대상이 없으면 수정자 메서드 자체가 호출x
  •  org.springframework.lang.@Nullable  : 자동 주입할 대상이 없으면 null이 입력된다.
  •  Optional<>  : 자동 주입할 대상이 없으면  Optional.empty  가 입력된다.
//호출 안됨
@Autowired(required = false)
public void setNoBean1(Member noBean1) {
	System.out.println("setNoBean1 = " + noBean1);
}

//null 호출
@Autowired
public void setNoBean2(@Nullable Member noBean2) {
	System.out.println("setNoBean2 = " + noBean2);
}

//Optional.empty 호출
@Autowired(required = false)
public void setNoBean3(Optional<Member> noBean3) {
	System.out.println("setNoBean3 = " + noBean3);
}

/*
출력결과
setNoBean2 = null
setNoBean3 = Optional.empty
*/
  •  Member  는 스프링 빈이 아니다.
  • setNoBean1() 은  @Autowired(required = false) 이므로 호출 자체가 안된다.

 

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;
}
//...
}
  • 필수 필드인  discountPolicy  에 값이 설정이 안되어있고 누락되었다. 자바는 컴파일 시점에 다음 오류를 발생시킨다.
    •  java: variable discountPolicy might not have been initialized 

생성자 주입을 제외한 나머지 주입 방식은 모두 생성자 이후에 호출되므로, 필드에 final 키워드를 사용할 수 없다. 오직 생성자 주입 방식만 사용가능

 

 

 

 

정리

  • 기본으로 생성자 주입을 사용하고, 필수 값이 아닌 경우에는 수정자 주입 방식을 옵션으로 부여하면 된다. 생성자 주입과 수정자 주입을 동시에 사용 불가하다.
  • 항상 생성자 주입을 선택하고 가끔 옵션이 필요하면 수정자 주입을 선택하자 필드 주입 사용x

 

 


조회한 빈이 모두 필요할 때, List, Map

의도적으로 해당 타입의 스프링 빈이 다 필요한 경우도 있다.

예를 들어 할인 서비스를 제공하는데, 클라이언트가 할인의 종류(rate, fix)를 선택할 수 있다고 가정해보자.

 

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

    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);
        }
    }
}
  • 로직 분석
    • DiscountService 는 Map으로 모든 DiscountPolicy 를 주입받는다. 이때 fixDiscountPolicy, rateDiscountPolicy가 주입된다.
    • discount() 메서드는 discountCode로 “fixDiscountPolicy” 가 넘어오면 map에서 fixDiscountPolicy 스프링 빈을 찾아서 실행한다. 물론 rateDiscountPolicy → rateDiscountPolicy 동일하다.
  • 주입 분석
    • Map <String, DiscountPolicy> : map의 키에 스프링 빈의 이름을 넣어주고, 그 값으로 DiscountPolicy 타입으로 조회한 모든 스프링 빈을 담아준다.
    • List <DiscountPolicy> : DiscountPolicy 타입으로 조회한 모든 스프링 빈을 담아준다.
    • 만약 해당하는 타입의 스프링 빈이없으면, 빈 컬렉션이나 Map을 주입한다.

 

의존관계 자동, 수동 올바른 실무 운영 기준

결론부터 이야기하면, 스프링이 나오고 점점 자동을 선호하는 추세

스프링은 @Component 뿐만 아니라 @Controller , @Service , @Repository 처럼 계층에 맞추어 일반적인 애플리케이션 로직을 자동으로 스캔할 수 있도록 지원한다. 또한 최근 스프링 부트는 컴포넌트 스캔을 기본으로 사용하며 스프링 부트의 다양한 스프링 빈들도 조건이 맞으면 자동으로 등록하도록 설계했다.

개발자 입장에서 스프링 빈을 하나 등록할 때 @Component 만 넣어주면 끝나는 일을 @Configuration 설정 정보에 가서 @Bean 을 적고, 객체를 생성하고, 주입할 대상을 일일이 적어주는 과정은 너무 번거롭다. 그리고 관리할 빈이 많아서 설정 정보가 커지면 설정 정보를 관리하는 것 자체가 부담이 된다. 결정적으로 자동 빈 등록을 사용해도 OCP, DIP를 지킬 수 있다. → 사기

하지만 수동 빈 등록을 사용할 상황도 충분히 있음~

특히 스프링 부트가 아니라 내가 직접 기술 지원 객체를 스프링 빈으로 등록한다면 수동으로 등록해서 아래처럼 명확하게 드러내는 것이 좋다.

@Configuration
public class DiscountPolicyConfig {

@Bean
public DiscountPolicy rateDiscountPolicy() {
	return new RateDiscountPolicy();
}

@Bean
public DiscountPolicy fixDiscountPolicy() {
	return new FixDiscountPolicy();
}
}

댓글