[스프링 핵심원리 기본편] 의존관계 자동 주입

흑수·2022년 2월 4일
1

김영한씨의 스프링 핵심 원리 - 기본편 강의를 듣고 공부 겸 정리하는 글입니다.

다양한 의존관계 주입 방법

지금까지는 @Autowired 어노테이션을 이용해서 의존 관계 주입을 했습니다.

이 방법 외에도 총 4가지의 방법이 있다고 해요.
돌이켜 보면 번개장터 면접 때에도 의존 관계를 주입하는 다른 방법을 설명하라고 했었는데 이 부분이었어요.

바로 생성자 주입, 수정자 주입(setter), 필드 주입, 일반 메소드 주입 입니다!


생성자 주입

말 그대로 생성자를 통해서 의존관계를 주입하는 방법입니다.
지금까지 했던 바로 그 방법인데요,,
생성자 호출 시점에 딱 한번만 호출하는 것이 보장되기에 불변하거나 필수적인 의존관계에 사용합니다.

즉, 필드를 변경시키지 않는다는 이야기에요.

생성자는 여러개가 존재할 수 있는데 딱 1개 존재한다면 @Autowired를 생략해도 자동으로 주입 됩니다.

public class OrderServiceImpl implements OrderService {
    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;
    
    // 이렇게 추가적인 생성자가 존재할 때는 생략 불가
    public OrderServiceImpl() {
    }
    
    // 이렇게 생성자 하나 있을 때는 생략 가능
    @Autowired 
    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }
}

수정자 주입

엔티티를 만들때마다 조금 이따 이야기할 롬복을 이용해 getter와 setter를 설정해주곤 했는데요.
이 setter를 이용하는 것이 바로 수정자 주입입니다.

setter: 클래스의 필드를 변경시키는 수정자 메소드

이 수정자 주입은 생성자 주입과는 달리 선택, 변경 가능성이 있는 의존관계에서 사용합니다.

예를 들어, 메모리 저장소를 사용하다 실제 디비를 사용한다던지.. 구현체를 중간에 바꾸는 경우를 이야기 하는 것 같아요. 물론 그런 일은 적다고 합니다.

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

필드 주입

필드에 바로 주입하는 것을 이야기합니다. 코드를 보면 굉장히 깔끔하지만, 인텔리제이에서 이런식의 코드를 쳐보면 권장하지 않는다고 경고 문구가 나옵니다.

또 이렇게 필드 주입을 해버리면 외부에서 변경하기가 힘드므로 테스트하기가 까다롭습니다. 생성자 주입처럼 외부 Config파일에서 변경도 불가하고 수정자 주입처럼 중간에 수정도 못하니..

결론은 사용하지 말자 ! 입니다.

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

참고로 순수한 자바 코드에서는 @Autowired 이용 불가입니다.

일반 메서드 주입

일반 메서드를 통해 주입합니다. 이 또한 일반적으로 사용하지 않습니다!

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

옵션 처리

그렇다면 의존 관계를 주입할 때, 스프링 빈이 없다면 어떤 일이 벌어질까요?

당연히 오류가 발생합니다!!

예를 들어 @Component가 붙지 않은 일반적인 클래스를 주입하는 상황을 가정해보겠습니다.

일반적인 클래스는 스프링 빈 등록이 되지 않기 때문에 당연히 오류가 날 수 밖에 없습니다. 이러한 상황에서 @Autowired를 무시하기 위한 옵션 값이 있습니다.

  • @Autowired(required = false) : 기본값은 true
  • org.springframework.lang.@Nullable : 자동 주입 대상 없으면 null 입력
  • Optional<> : 자동 주입 대상 없으면 Optional.empty 입력

//호출 안됨
@Autowired(required = false)
public void setNoBean1(Member member) {
    System.out.println("setNoBean1 = " + member);
}

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

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

이 세가지 방법을 통해 오류를 막을 수 있습니다.

결과

setNoBean2 = null
setNoBean3 = Optional.empty

생성자 주입을 선택해라!

그냥 위 네가지 방법중에서 생성자 주입을 이용하도록 해요!!

생성자 주입의 특징이 뭐였는지 기억 나시나요?


  • 불변

의존관계 주입이 한 번 일어나면 앱 종료시까지 변경할 일 거의 없음 (오히려 변하면 안됨)
수정자 주입 역시 메소드를 public으로 열어두어야 함
변경되면 안되는 다른 메소드를 public으로 열어두는 것은 좋은 설계 방법이 아님

  • 누락

의존관계 주입이 누락됐을 경우에 순수 자바를 이용한다면 컴파일러 오류가 발견되지 않고 실행이 됨

OrderServiceImpl 내에 생성자 주입 x, 수정주 주입 o => 의존관계가 자동으로 주입 x

void createOrder() {
      OrderServiceImpl orderService = new OrderServiceImpl();
      orderService.createOrder(1L, "itemA", 10000);
}

하지만 결과는 NPE(Null Pointer Exception)
생성자 주입을 했더라면, 컴파일 오류가 발생합니다. 즉, 오류를 바로 찾아낼 수 있다는 이야기!!

final 키워드를 입력하게 되면 컴파일 시점에 오류를 바로 찾을 수 있습니다.

private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;

@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy
    discountPolicy) {
    this.memberRepository = memberRepository;
    // discountPolicy는 할당 x
}

이렇듯, final 키워드를 입력했기에 discountPolicy도 값을 할당 받아야하는데 생성자를 보면 그렇지 않죠? 이것이 컴파일 시점에 오류를 잡게끔 해줍니다.

기억하자! 컴파일 오류는 세상에서 가장 빠르고, 좋은 오류다!
기억하자! 컴파일 오류는 세상에서 가장 빠르고, 좋은 오류다!
기억하자! 컴파일 오류는 세상에서 가장 빠르고, 좋은 오류다!

참고: 오직 생성자 주입 방식만 필드에 final 키워드를 사용할 수 있습니다.

롬복과 최신 트렌드

롬복이라는 개발자를 더욱 편하게 만들어주는 라이브러리입니다.
@RequiredArgsConstructor 어노테이션을 통해 생성자를 생략할 수 있습니다.

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

조회 빈이 2개 이상 - 문제

@Autowired는 타입 기준으로 조회를 합니다. 즉, 하위 타입이 존재한다면 문제가 발생하게 된다는 거에요. (타입 조회시, 2개 이상)

ac.getBean(DiscountPolicy.class);

와 유사한 기능을 하기 때문이지요.

@Autowired
privated DiscountPolicy discountPolicy

@Component
public class FixDiscountPolicy implements DiscountPolicy {}

@Component
public class RateDiscountPolicy implements DiscountPolicy {}

하지만, 하위 타입으로 지정하는 것은 DIP를 위반하는 행동입니다.
구현체가 아닌 인터페이스를 참조해야 하니까요..!! 또한 유연성이 떨어지기도 합니다.

그럼 어떻게..? 🧐🧐🧐

@Autowired 필드명, @Qualifier, @Primary

총 세가지의 해결방법이 존재합니다.

  • @Autowired 필드명 매칭
  • @Qualifier -> @Qualifier끼리 매칭 -> 빈 이름 매칭
  • @Primary사용

자세한 방법을 알아봅시다


@Autowired 필드명

우선적으로 타입 매칭을 실시하고 여러 빈이 존재하면 이후에 필드명, 파라미터명을 보게 됩니다.

private DiscountPolicy discountPolicy

DiscountPolicy만 보면 하위 타입이 존재해 오류가 발생하니 필드명을 보게 됩니다.
이 때, 필드명을 rateDiscountPolicyfixDiscountPolicy로 바꾸게 된다면 이것으로 인식하게 됩니다.

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

Primary

우선순위를 정하는 방법입니다. @Autowired를 통해 여러 빈을 찾게 되면 @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;
}

위의 경우에는 RateDiscountPolicy가 주입됩니다,,

@Primary는 기본값처럼 동작하고 @Qualifier는 상세하게 동작하므로 후자가 우선권을 가집니다. (스프링은 자동보단 수동, 넓은 것보단 좁은 범위가 우선권)

애노테이션 직접 만들기

애노테이션을 만드는 방법이에요.

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

@Component
@MainDiscountPolicy
public class RateDiscountPolicy implements DiscountPolicy {}

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

가끔은 모든 빈이 필요할 수도 있습니다. fixDiscount, rateDiscount 등 유저가 할인 방법을 선택하게 하는 경우를 생각해보면 이해가 되지요.

Map<>을 이용해 이름 : 객체로 매핑을 하고, List에는 객체를 넣어주겠습니다.

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

Map을 통해 모든 discountPolicy를 주입받습니다. rateDiscountPolicy, fixDiscountPolicy가 들어가겠죠?
discount 메소드를 보면 들어오는 discountCode에 따라서 할인정책이 결정됩니다. 'rateDiscountPolicy'가 들어오면 rateDiscountPolicy 빈을 찾게됩니다.

new AnnotationConfigApplicationContext(AutoAppConfig.class,DiscountService.class);
  • new AnnotationConfigApplicationContext()를 통해 스프링 컨테이너 생성
  • AutoAppConfig.class, DiscountService.class를 스프링 빈으로 등록

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

편리한 자동 기능을 기본으로 사용하자!!
편리한 자동 기능을 기본으로 사용하자!!
편리한 자동 기능을 기본으로 사용하자!!

스프링이 나오고 시간이 더 흐르면서 자동을 선호하는 추세라고 하네요.
스프링은 @Component뿐만 아니라, @Controller, @Service, @Repository도 모두 스캔합니다.

@Component하나만 달아주면 끝날 일을 설정파일 가서 @Bean 달아주고 객체 생성하고 주입해주는 일은 여간 간편한 일은 아니에요.

결정적으로 자동으로 하게 되면 DIP, OCP를 모두 지킬 수 있답니다:)

그러면 수동 빈 등록은 언제 사용하면 좋을까?

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

  • 업무 로직 빈: 컨트롤러, 서비스, 레포지토리
  • 기술 지원 빈: 기술적인 문제나 공통 관심사(AOP), 주로 데이터 베이스 연결, 공통 로그 처리

업무 로직은 정말 많고, 어느정도 유사한 패턴이 존재합니다. 왜냐하면 컨트롤러 서비스 레포지토리를 개발하는 방법에 대해 어느정도 통일되어 있으니까요,,
이런 경우는 자동을 이용해주는게 좋습니다. 보통 문제가 발생해도 정형화 되어 있기 때문에 어디서 잘못 됐는지 찾아내기 쉽거든요.

기술 지원 로직은 업무 로직에 비해 그 수가 현저히 적습니다. 또한 애플리케이션 전체에 광범위하게 영향을 미치기 때문에 어디가 문제인지 찾기도 힘들어요. 그렇다는 것은? 수동 등록을 사용해주는게 좋다,, 이말입니다.

애플리케이션에 광범위하게 영향을 미치는 기술 지원 객체는 수동 빈으로 등록해서 딱! 설정 정보에 바로 나타나게 하는 것이 유지보수 하기 좋다.

profile
기록용

0개의 댓글