본문 바로가기
Framework/Spring

[Spring] 스프링 핵심 원리 (기본) - 6. 의존관계 자동 주입

by pilgyeong 2023. 2. 4.

6. 의존관계 자동 주입


6.1 의존관계 주입 방법 (4가지)

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

생성자 호출 시점에 딱 한 번만 호출되는 것이 보장되고, 불변, 필수 의존관계에 사용한다. 또한, 생성자가 1개인 스프링 빈이라면 @Autowired를 생략해도 자동 주입된다.

6.1.2 수정자(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의 기본 동작은 주입할 대상이 없으면 오류가 발생한다. 주입할 대상이 없어도 동작하도록 만들고 싶다면, @Autowired(required = false)를 지정하면 된다.

 

6.1.3 필드 주입

@Component
public class OrderServiceImpl implements OrderService {
    @Autowired
    private MemberRepository memberRepository;
    @Autowired
    private DiscountPolicy discountPolicy;
}

필드에 바로 주입하는 방법으로서 코드가 간결하지만 DI 프레임워크가 없으면 아무것도 할 수 없게 되므로, 실제 코드에서는 사용하지 않는 것이 좋다. 테스트 코드 또는 스프링 설정을 목적으로 하는 @Configuration 같은 곳에서만 특별한 용도로 사용한다.

6.1.4 일반 메소드 주입

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

한 번에 여러 필드를 주입 받을 수 있지만, 일반적으로 사용하진 않는다.



6.2 옵션 처리

주입할 스프링 빈이 없어도 동작해야될 때가 있는 데, @Autowired만 사용하면 required 옵션의 기본값이 true로 되어 있어 자동 주입대상이 없으면 오류가 발생한다.

자동 주입 대상을 옵션으로 처리하는 방법

  • @Autowired(required=false): 자동 주입할 대상이 없으면, 수정자 메소드 자체가 호출 안 됨
  • org.springframework.lang.@Nullable: 자동 주입할 대상이 없으면 NULL이 입력된
  • Optional<>: 자동 주입할 대상이 없으면, Optional.empty가 입력됨



6.3 생성자 주입

최근 DI 프레임워크 대부분이 다음의 이유로 생성자 주입을 권장한다.

  • 의존관계 대부분은 애플리케이션 종료 전까지 변경할 일이 없다.
  • 수성자 주입을 사용하려면 메소드를 public으로 열어둬야 하는 데, 이는 좋은 설계가 아니다.
  • 필요한 의존관계가 누락될 때, 컴파일 오류로 쉽게 고칠 수 있다. (final키워드 사용 가능)

따라서, 개발할 때, 대부분의 경우 생성자에 final 키워드를 사용해서 만드는 데, 이를 간편하게 해주는 라이브러리 롬복(Lombok)이 존재한다.

@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;
}

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



6.4 의존관계 조회 빈이 2개 이상

@Autowired
private DiscountPolicy discountPolicy

타입으로 조회하기 때문에 ac.getBean(DiscountPolicy.class)와 유사하게 동작한다. 이렇게 타입으로 조회하면, 선택된 빈이 2개 이상인 경우에는 문제가 발생한다.

@Component
public class FixDiscountPolicy implements DiscountPolicy {}
@Component
public class RateDiscountPolicy implements DiscountPolicy {}

@Autowired는 타입으로 조회하기 때문에 선택된 빈이 2개 이상일 때, NoUniqueBeanDefinitionException 오류가 발생한다. 이때, 하위 타입으로 지정할 수도 있지만, DIP를 위반하고 유연성이 떨어지기 때문에 @Autowired에 필드명을 적용해서 해결한다.

6.4.1 @Autowired 필드명 매칭

@Autowired
private DiscountPolicy rateDiscountPolicy

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

6.4.2 @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로 등록한 이름을 적어준다. 만약 주입 시 @Qualifier로 등록한 이름이 없다면 빈 이름을 추가로 찾는다.

 

추가로, @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;
}

어노테이션에 상속은 없다. 단, 스프링이 여러 어노테이션을 모아 사용하는 기능을 제공할뿐이다.

6.4.3 @Primary 사용

@Component
@Primary
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로 우선 순위를 정해서 의존관계를 주입할 수 있다.

 

참고.
@Primary는 마치 기본값처럼 동작하는 것이고, @Qualifier는 매우 상세하게 동작하는 것이다. 두 경우 모두 사용할 때, 자동보다 수동이, 넓은 범위의 선택권보다는 좁은 범위의 선택권이 우선 순위가 높아서 @Qualifier의 우선권이 높다.



6.5 조회한 빈이 모두 필요한 경우

public class AllBeanTest {
    @Test
    void findAllBean() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class, DiscountService.class);
        DiscountService discountService = ac.getBean(DiscountService.class);
        assertThat(discountService).isInstanceOf(DiscountService.class);

        Member member = new Member(1L, "userA", Grade.VIP);
        int discountPrice = discountService.discount(member, 10000, "fixDiscountPolicy");
        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;
        }

        public int discount(Member member, int price, String discountCode) {
            DiscountPolicy discountPolicy = policyMap.get(discountCode);
            return discountPolicy.discount(member, price);
        }
    }
}

가령, 할인 정책을 활용할 때, 클라이언트가 할인의 종류(rate, fix) 모두 선택할 수 있을 때가 있다. 이런 경우에는 해당 타입의 스프링 빈이 모두 필요하다.

  • Map<String, DiscountPolicy>: 맵의 키에 스프링 빈의 이름을 넣어주고, 그 값으로 DiscountPolicy 타입으로 조회한 모든 스프링 빈을 담아준다.
  • List<DiscountPolicy>: DiscountPolicy 타입으로 조회한 모든 스프링 빈을 담아준다. (만약 해당하는 타입의 스프링 빈이 없으면, 빈 컬렉션이나 맵을 주입함)

 

이해를 돕기 위해 위 코드는 다음 2가지로 나눌 수 있다.

  • new AnnotationConfigApplicationContext()를 통해 스프링 컨테이너를 생성한다.
  • AutoAppConfig.class, DiscountService.class를 파라미터로 넘기면서 해당 클래스를 자동으로 스프링 빈으로 등록한다.

 

정리하면, 스프링 컨테이너를 생성하면서, 해당 컨테이너에 동시에 AutoAppConfig, DiscountService를 스프링 빈으로 자동 등록한다.



6.6 자동/수동 올바른 실무 운영 기준

스프링은 @Component뿐만 아니라 @Controller, @Service, @Repository처럼 계층에 맞춰 애플리케이션 로직을 자동으로 스캔할 수 있도록 지원한다. 사실 설정 정보를 기반으로 애플리케이션을 구성하는 부분과 동작하는 부분을 명확하게 나누는 것이 이상적이지만, 개발자 입장에서 스프링 빈을 하나 등록할 때 @Component만 넣어주면 끝나는 일을 설정 정보를 위해 여러 과정을 번거롭게 해야 한다. 그래서, 점점 자동을 선호하는 추세이다.

 

애플리케이션은 크게 업무 로직과 기술 지원 로직으로 나눌 수 있다.

  • 업무 로직 빈: 웹을 지원하는 컨트롤러, 핵심 비즈니스 로직이 있는 서비스, 데이터 계층의 로직을 처리하는 리포지토리 등이 모두 업무 로직에 해당한다. 보통 비즈니스 요구사항을 개발할 때 추가되거나 변경된다.
  • 기술 지원 빈: 기술적인 문제나 공통 관심사(AOP)를 처리할 때 주로 사용한다. 데이터베이스 연결이나 공통 로그처리 처럼 업무 로직을 지원하기 위한 하부 기술이나 공통 기술이다.

업무로직은 숫자도 매우 많고, 한 번 개발해야 하면 컨트롤러, 서비스, 리포지토리 처럼 어느정도 유사한 패턴이 있다. 이 경우에는 자동 기능을 적극 활용하는 것이 좋다. 문제가 발생해도 어떤 곳에서 문제가 발생했는지 명확하게 파악하기 쉽기 때문이다.

 

반면, 기술 지원 로직은 업무 로직과 비교해서 그 수가 매우 적고, 보통 애플리케이션 전반에 걸쳐서 광범위하게 영향을 미친다. 그리고 문제가 발생했을 때, 기술 지원 로직은 적용이 잘 되고 있는지 아닌지 조차 파악하기 어려운 경우가 많다. 그래서, 이런 기술 지원 로직들은 가급적 수동 빈 등록을 사용해서 명확하게 드러내는 것이 좋다.

 

단, 스프링과 스프링 부트가 자동으로 등록하는 수 많은 빈들은 예외이다.

 

애플리케이션에 광범위하게 영향을 미치는 기술 지원 객체는 수동 빈으로 등록해서, 설정 정보에 바로 보이게 하는 것이 유지보수 관점에서 효율적이다.