Spring

의존 관계 주입(DI, Dependency Injection)

dev-rootable 2023. 5. 9. 22:01

의존 관계 주입은 왜 하는 것일까

웹 애플리케이션을 개발할 때 비즈니스 요구 사항을 개발하기 위해 컨트롤러, 서비스, 리포지토리 등으로 분리하여 설계한다. 이것은 Spring MVC 모델을 따른 것으로, 서로 분리되어 각자의 역할에만 충실하도록 개발하기 위함이다.

 

일반적으로 객체가 필요할 때는 new를 사용하는데, 웹에서 사용자의 클릭 한 번으로도 프로젝트 규모에 따라 정말 많은 객체의 호출이 발생하기도 한다. 이것을 해결하기 위해 스프링 컨테이너에서 필요한 객체들을 싱글톤 객체로서 등록하고 연결하는 역할을 수행하는 것이다. 여기서 연결한다는 것은 스프링에서 명시된 Annotation 또는 생성자를 보고 해당 객체에서 필요한 객체 즉, 의존성(의존 관계)를 주입해 주는 것이다.

 

결과적으로 개발자가 직접 연결해야 하는 수고를 덜어 생산성을 높이고, 유연하고 확장성 높은 개발을 하도록 돕는다. 또한, JVM 메모리를 절약할 수 있어 GC(Garbage Collector)의 부담을 덜어준다.

 

📌 의존 관계 주입 방법

1. 생성자 주입

생성자를 통해 의존 관계를 주입 받는 방법

 

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

 - 불변 특징 : 생성 후에 변경이 불가능하다.

 - 필수 특징 : final 을 사용할 수 있어 초기 값을 넣도록 강제하는 효과가 있다.

 

@Component
public class OrderServiceImpl implements OrderService {

    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

    @Autowired //생성자 1개는 생략 가능
    public OrderServiceImpl(MemberRepository memberRepository, @MainDiscountPolicy DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }
    
    ...
 }

 

//Lombok을 통해 간결해진 코드
@Component
@RequiredArgsConstructor //final이 붙은 필드를 모아서 생성자 주입을 수행
public class OrderServiceImpl implements OrderService {

    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;
    
    ...
 }

 

 

2. 수정자 주입(Setter 주입)

setter 를 통해 의존 관계를 주입하는 방법

 

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

 - 주입하려는 값이 Bean으로 등록되지 않아도 사용 가능

 - 자바빈 프로퍼티 규약(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;
    }
}
Autowired의 기본 동작은 주입할 대상이 없으면 오류가 발생한다. 주입할 대상이 없어도 동작하게 하려면 
@Autowired(required = false)로 지정하면 된다.

 

3. 필드 주입(권장x)

필드에 바로 주입하는 방법

 

 - 간결한 코드

 - 외부에서 변경이 불가능하여 테스트 용이성이 떨어짐

 - DI 프레임워크가 없으면 아무 것도 할 수 없다.

   => 순수한 자바 테스트 코드에서는 동작하지 않고, @SpringBootTest 처럼 스프링 컨테이너를 통합한 경우에만 가능

 

@Component
public class OrderServiceImpl implements OrderService{

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

 

📌 생성자 주입을 선택해야 하는 이유

1. 불변

  • 대부분의 의존 관계는 애플리케이션 종료 전까지 변하면 안된다. (최초 생성 때 1회 호출됨)
  • 수정자 주입은 setter 를 public으로 열어둬야 하므로 변경 가능성이 있다.

2. 누락

  • 의존 관계 주입 데이터를 누락했을 때, 컴파일 오류를 주기 때문에 누락되는 상황이 발생하지 않는다.
  • 수정자 주입은 프레임워크에서 돌아갈 때, 컴파일 오류를 띄워주지 않고, 실행 후 NPE를 보여준다.
  • 순수한 자바 코드로 테스트할 수 있다.

3. final 키워드

  • 생성자를 강제하므로, 최초 1회만 값을 설정할 수 있고 값의 누락을 컴파일 시점에 막아준다.

 

생성자 주입을 기본으로 사용하고, 수정자 주입을 옵션으로 사용하는 방법을 추천

 

📌 동일 타입의 빈 중 원하는 빈 불러오기

@Autowired는 type으로 빈을 조회하기 때문에 동일 타입의 bean이 2개 이상 존재한다면 오류가 발생한다. 그렇다고 이것을 하위 타입으로 지정하는 것은 하위 클래스(구체 클래스)에 직접 접근해야 하므로 DIP를 위배하게 된다. 하지만 스프링은 이러한 문제를 해결할 방법을 구현해두었다.

 

1. @Autowired 필드명 매칭

@Autowired 를 사용한 필드에서 필드명을 구체 클래스의 이름으로 지정하는 것이다. 가장 유연하게 대처할 수 있고 상대적으로 변경이 적은 방법이라고 생각한다.

 

@Component
public class OrderServiceImpl implements OrderService{

    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

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

 

2. @Qualifier 사용

추가 구분자를 붙여주는 방법이다. 원하는 구체 클래스에 구분자를 적어 놓고, 필요한 곳에서 해당 애노테이션을 붙여 의도한 클래스가 주입되도록 하는 것이다.

 

@Component
@Qualifier("mainDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy{
...
@Component
public class OrderServiceImpl implements OrderService{

    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

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

만약, @Qualifier로 주입할 때, @Qualifier("mainDiscountPolicy")를 못 찾으면 mainDiscountPolicy라는 이름의 스프링 빈을 추가로 찾는다. 최종적으로 찾지 못했을 때는 NoSuchBeanDefinitionException 예외가 발생한다. 그래서 빈 등록용으로 사용하지 않고 @Qualifier끼리 찾는 용도로만 사용하는 것이 좋다.

 

3. @Primary

우선 순위를 정하는 방법이다. @Autowired에서 동일 타입으로 여러 빈이 매칭되면 우선권을 쥐어 준 클래스가 매칭되는 것이다. 사용하는 곳마다 구분자를 지정하지 않아도 되고, 직접 구체 클래스명으로 수정하지 않아도 되므로 간단하지만 우선 순위는 변할 수 있으므로 유연성은 떨어진다고 생각한다.

 

@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy{
...
우선권 : @Qualifier > @Primary

 

4. 직접 애노테이션 생성

@Qualifier("mainnDiscountPolicy") 같은 경우 구분자 이름에 오타가 발생해도 문자이므로 컴파일 타임에 체크되지 않는다. 이것은 애노테이션을 직접 생성하는 방법으로 해결할 수 있다.

 

@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER,
ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Qualifier("mainDiscountPolicy")
public @interface MainDiscountPolicy {
}
@Component
public class OrderServiceImpl implements OrderService{

    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

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

 

📌 동일 타입의 모든 빈 받기

Map<key, value>이나 List<타입> 를 통해 동일 타입의 빈을 컬렉션으로 다룰 수 있다.

 

Map의 경우 key로 스프링 빈의 이름을 넣어주고, 그 값으로 해당 타입으로 조회한 모든 스프링 빈을 담는다.

List도 해당 타입으로 조회한 모든 스프링 빈을 담는다.

 

public class AllBeanTest {

    @Test
    public void findAllBean() throws Exception{
        /*
        * 스프링 컨테이너는 생성자에 클래스 정보를 받는다.
        * 여기에 클래스 정보를 넘기면 해당 클래스가 스프링 빈으로 등록된다.
        */
        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;

        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); //fixDiscountPolicy
            System.out.println("discountPolicy = " + discountPolicy); //fixDiscountPolicy 객체

            return discountPolicy.discount(member, price);
        }
    }

}

 

❓ 의존 관계 주입에 대한 자동 vs 수동

애플리케이션은 업무 로직 빈기술 지원 빈으로 구분할 수 있다. 비즈니스 요구 사항을 개발하는 업무 로직 빈은 @Controller, @Service, @Repository 등 계층을 나눠서 패턴이 있는 개발을 하고, 그 수가 많으므로 @ComponentScan, @Autowired 등 자동을 많이 사용하는 것이 좋다.

 

기술 지원 빈은 기술적인 문제나 공통 관심사(AOP) 를 처리하는 광범위한 영향을 미치는 로직이다. 그래서 그 수가 적고, 적용이 잘 되고 있는지 여부를 파악하기 힘들다. 따라서, 기술 지원 빈은 가급적 수동 빈 등록으로 명확하게 들어내는 것이 유지 보수에 좋다. 또한, 동일 타입 빈들을 컬렉션으로 다룰 때도 어떤 빈들이 주입될지, 각 빈들의 이름이 무엇일지 코드만 보고 파악하기 힘들기 때문에 수동 빈으로 등록하거나 특정 패키지에 묶어두는 것이 좋다.

 

자동 등록을 적극 활용하고, 기술적인 문제처럼 명확하게 코드로 파악하기 어려운 로직은 수동 빈을 활용

 

Reference:

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8/dashboard

 

스프링 핵심 원리 - 기본편 - 인프런 | 강의

스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., - 강의 소개 | 인프런

www.inflearn.com