Spring

[스프링 핵심 원리] 06. 의존관계 자동 주입

binning 2025. 1. 21. 18:26

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

생성자 주입

- 생성자 호출시점에 딱 1번만 호출되는 것이 보장된다.

- 불변, 필수 의존관계에 사용

- 생성자가 딱 1개만 있으면 @Autowired를 생략해도 자동 주입된다.

 

@Component
public class OrderServiceImpl implements OrderService {

    private MemberRepository memberRepository;
    private DiscountPolicy discountPolicy;

    @Autowired
    public void setDiscountPolicy(DiscountPolicy discountPolicy) {
        this.discountPolicy = discountPolicy;
    }

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

수정자 주입

- 선택, 변경 가능성이 있는 의존관계에 사용

- 자바빈 프로퍼티 규약의 수정자 메서드 방식을 사용하는 방법이다.

 

@Component
public class OrderServiceImpl implements OrderService {

    @Autowired
    private MemberRepository memberRepository;
    @Autowired
    private DiscountPolicy discountPolicy;
}

필드 주입

- 외부에서 변경이 불가능해서 테스트 하기 힘들다.

- 애플리케이션의 실제 코드와 관계 없는 테스트 코드, @Configuration 같은 곳에서만 특별한 용도로 사용.

 

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

일반 메서드 주입

- 한번에 여러 필드를 주입 받을 수 있다.

- 일반적으로 잘 사용하지 않는다.

 

2. 옵션 처리

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

public class AutowiredTest {

    @Test
    void AutowiredOption() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(TestBean.class);
    }

    static class TestBean {
    
        //Member는 스프링 빈이 아닌 상태이다.
        @Autowired(required = false)
        public void setNoBean1(Member noBean1) {
            System.out.println("noBean1 = " + noBean1);
        }

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

        @Autowired
        public void setNoBean3(Optional<Member> noBean3) {
            System.out.println("noBean3 = " + noBean3);
        }
    }
}

- @Autowired(required=false): 자동 주입할 대상이 없으면 수정자 메서드 자체가 호출 안된다.

- @Nullable: 자동 주입할 대상이 없으면 null이 입력된다.

- Optional<>: 자동 주입할 대상이 없으면 Optional.empty가 입력된다.

 

3. 생성자 주입을 선택해라

- 최근에는 스프링을 포함한 DI 프레임워크 대부분이 생성자 주입을 권장한다.

- 대부분의 의존관계 주입은 한번 일어나면 애플리케이션 종료시점까지 의존관계를 변경할 일이 없다.

- final 키워드를 사용할 수 있다. 그래서 생성자에서 값이 설정되지 않는 오류를 컴파일 시점에서 막아준다.

> 기본으로 생성자 주입을 사용하고, 가끔 옵션이 필요하면 수정자 주입을 사용하면 된다.

 

4. 롬복 (생성자 주입을 편리하게 하는 방법)

@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {

    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;
}

- 롬복 라이브러리가 제공하는 @RequiredArgsConstructor기능을 사용하면 final이 붙은 필드를 모아서 생성자를 자동으로 만들어 준다.

- 최근에는 생성자를 딱 1개 두고, @Autowired를 생략하는 방법을 주로 사용한다. 여기에 @RequiredArgsConstructor를 함께 사용하면 코드를 깔끔하게 사용할 수 있다.

- 롬복 라이브러리는 @Getter, @Setter과 같은 기능도 제공한다.

 

5. 조회 빈이 2개 이상

DiscountPolicy의 하위 타입인 FixDiscountPolicy, RateDiscountPolicy를 둘다 스프링 빈으로 선언해보자.

> NoUniqueBeanDefinitionException오류가 발생한다. 해결방법은 아래와 같다.

@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy rateDiscountPolicy) {
    this.memberRepository = memberRepository;
    this.discountPolicy = rateDiscountPolicy;
}

- @Autowired는 타입 매칭을 시도하고, 이때 여러 빈이 있으면 필드 이름, 파라미터 이름으로 빈 이름을 추가 매칭한다.

@Autowired
public OrderServiceImpl(MemberRepository memberRepository, @Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy) {
    this.memberRepository = memberRepository;
    this.discountPolicy = discountPolicy;
}
@Component
@Qualifier("mainDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy {}

- @Qualifier끼리 매칭하는 방법이다.

- @Qualifier는 @Qualifier를 찾는 용도로만 사용하는게 명확하고 좋다.

@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
    this.memberRepository = memberRepository;
    this.discountPolicy = discountPolicy;
}
@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy {}

- @Autowired시에 여러 빈이 매칭되면 @Primary가 우선권을 가진다.

- @Primary보다 좁은 범위인 @Qualifier이 우선권이 높다.

> 메인 데이터베이스의 스프링 빈은 @Primary를 적용하고, 서브 데이터베이스의 스프링 빈은 @Qualifier를 지정해서 명시적으로 획득하는 방식으로 사용하면 코드를 깔끔하게 유지할 수 있다.

 

6. 애노테이션 직접 만들기

@Qualifier("mainDiscountPolicy")는 컴파일시 타입 체크가 안된다. 애노테이션을 직접 만들어서 문제를 해결할 수 있다.

@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Qualifier("mainDiscountPolicy")
public @interface MainDiscountPolicy {
}
@Component
@MainDiscountPolicy
public class RateDiscountPolicy implements DiscountPolicy {}
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, @MainDiscountPolicy DiscountPolicy discountPolicy) {
    this.memberRepository = memberRepository;
    this.discountPolicy = discountPolicy;
}

 

7. 조회한 빈이 모두 필요할 때 (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(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);
        }
    }
}

- Map<String, DiscountPolicy>: map의 키에 스프링 빈의 이름을 넣어주고, 그 값으로 DiscountPolicy타입으로 조회한 모든 스프링 빈을 담아준다.

- List<DiscountPolicy>: DiscountPolicy타입으로 조회한 모든 스프링 빈을 담아준다.