[기본기] 7-6. 의존관계 자동 주입을 하는 방법들, 옵션 처리

khyojun·2022년 10월 5일
1
post-thumbnail

본 게시글은 김영한님의 스프링 핵심 원리 기본편을 정리한 글입니다.


📌 의존관계 주입 방법의 종류

우리가 이때까지 의존관계 주입하는 방법으로 지금까지는 우리가 생성자 주입을 사용을 했었는데 사실은 이거 하나만 있는 것은 아니고 다른 여러가지 방법이 있다고 한다.

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

이렇게 4가지가 있다고 하는데 하나씩 알아보자.

🔍 생성자 주입

이때까지 우리가 사용했었던 방법이다. 생성자 주입의 장점은 이게 다른 주입 방법들에 대한 예시들을 보면 잘 드러나기에 뒤로가면서 얘기를 하겠다.
우선 생성자 주입의 특성은 다음과 같다.

  • 생성자 호출 시점때 단 1번만 호출하는게 보장이 된다는 것
  • 불변, 필수 의존관계에 사용

이렇게 2가지인데 솔직히 처음 이것을 보고는 음... 무슨 말이지? 라는 생각이 들었는데 위에 말했던 것처럼 다른 주입 방법의 예시들을 보게 되면 이 말을 이해할 수 있었으니 뒤로 가면서 더 자세히 알아보자.

📂 예제 코드

public class OrderServiceImpl implements OrderService{
   
    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

    // 생성자가 1개이면 @Autowired 필요 없음
    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) { // impl 입장에서는 외부에서 의존성 주입 DI
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
   }

생성자가 만약 1개라면 @Autowired를 작성하지 않아도 된다고 한다. 그리고 계속 우리가 봤던 방법이지만 우리가 여기서 기억을 또 해야할 것은 final이라는 키워드를 잘 기억하고 가자.

🔍 수정자(setter) 주입

수정자 주입은 이제 setter를 활용하여 주입을 시켜주는 방법인데 위의 예시와는 당연히 달라지며 이제 다른 코드들도 수정을 조금씩 해줘야 했었다. 그리고 생각할 것이 생성자 주입은 이제 객체를 생성하게 되면 그때 주입을 시켜주지만 setter 주입과 같은 경우는 객체가 생성이 다 되고 난 후에 주입을 시켜줄 수가 있어서 final 키워드는 작성하지 못한다는 점이 있다.

수정자 주입의 특성은 다음과 같다.

  • 선택, 변경 가능성이 있는 의존관계에 사용
  • 자바빈 프로퍼티 규약의 수정자 메서드 방식을 사용을 하는 방법이다.

우리가 또 생각해 볼 것이 선택적이고 변경이 가능하다는 것... 이것은 어찌보면 OCP를 어기는 것이 될 수도 있는것이 수정은 하면 안되고 확장에만 열려있는 방식을 선호하기에 이것만 사용하기에는 좋은 객체 지향 설계방향이라는 맞지 않을 수 있다.

📂 예제 코드

    private MemberRepository memberRepository;
    private DiscountPolicy discountPolicy; 
 // setter로 지정을 해보면 final 이 되지 않고 그리고 객체가 아직 생성된 후 부를 수 있기 때문에 final 못 씀
    @Autowired
    public void setMemberRepository(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

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

맞다! 테스트하기 위하여서는 당연한 얘기이겠지만 AppConfig에 있는 설정파일에서 return 값도 조정을 하여야 한다는 것 잊지 말자.

  • 참고 사항으로 자바빈 프로퍼티 규약이라고 위 특성에서 얘기를 했는데 그게 무엇이냐?

    자바에서는 과거부터 필드의 값을 직접 변경하지 않고, setXXX, getXXX 이라는 메서드를 통하여서 값을 읽거나 수정하는 규칙을 만들었는데 이것이 자바빈 프로퍼티 규약이다.
    그니까 한 마디로 Setter, Getter를 사용하는 방식이라는 얘기인 것 같다.

🔍 필드 주입

필드 주입이다. 어찌 보면 가장 간단하게 작성이 가능하고 개발자 입장에서는 가장 편한 방법일지도 모르지만 단점이 생각보다 있다. 필드 주입의 특성은 다음과 같다.

  • 외부에서 변경이 불가능하여 테스트를 하기 힘들다는 단점이 있다.
  • DI 프레임워크가 없다면 아무것도 할 수가 없다.
  • 사용 X (이정도까지 적어놓으신 거 보면 진짜 사용하면 안되는 방법인가 보다.)
    • 애플리케이션의 실제 코드와 관계 없는 테스트 코드에서 사용
    • 스프링 설정을 목적으로 하는 @Configuration 같은 곳에서만 특별히 사용

이제 예시와 테스트 코드를 보면서 어떠한 문제가 있어서 이렇게 사용을 하지 말라고 하신 건지 한 번 확인해보자.

📂 예제 코드

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

어떻게 너무 간단하다. 근데 간단한 만큼 문제도 있다.

📂 테스트 코드

    @Test   DI컨테이너 없이 그냥 하게 되면 주입시켜주는 친구가 없어서 memberRepository에도 없고 DiscountPolicy도 없어서 NPE가 뜸
    void fieldInjectionTest(){
        OrderServiceImpl orderService = new OrderServiceImpl();
        Order itemA = orderService.createOrder(1L, "itemA", 10000);
        System.out.println("itemA = " + itemA);

    }

주석에서 작성한대로 이렇게 그냥 실행을 시켜버리면 문제가 당연히 일어난다. createOrder할때 이제 뭐 memberRepository도 없고 뭐 DiscountPolicy도 없어서 주입을 시켜줄라고 해도 뭐 어찌할 방법이 없다. 그래서 보게 되면 결국 수정자 주입처럼 주입을 할 수밖에 없다는 사실에 이마를 탁 치고 간다. 그니까 되도록 이 방식은 사용을 하면 안되겠다.

🔍 일반 메서드 주입

일반 메서드 주입 방식은 되게 뭐라고 해야되지 정말 순수한 물과 같다라고 할까? 뭐가 다른 것인지는 모르겠지만 순수한 느낌이 강하다.

특성

  • 한번에 여러 필드를 주입 받을 수 있다.
    private  MemberRepository memberRepository;
   
    private  DiscountPolicy discountPolicy;
   @Autowired // 메서드로 부르는 방식은 거진 생성자 주입이랑 별로 다를게 없음...
   public void init(MemberRepository memberRepository, DiscountPolicy discountPolicy){
       this.memberRepository=memberRepository;
      this.discountPolicy=discountPolicy;
    }

음.. 생긴 것만 봐서는 생성자 주입이랑 다를게 딱히 없는데? 라는 느낌이 되게 강하다. 그래서 일반적으로 잘 사용을 하지 않는다고 한다.

💥 결론은 생성자 주입이다.

이게 위에서 하나씩 차근차근 봤는데 생성자 주입을 하지 않는 방법에서는 뭔가 하나씩 문제인 듯 한 것들이 하나씩 있다. 그래서 실제로도 생성자 주입을 많이 사용을 한다고 하는데 우선 가장 중요한 것이 불변 한다는 특성, 실수 방지를 해준다는 차원에서 많이 사용을 하게 되는 것 같다.

✔ 불변

불변한다는 특성에서 찾아보게 되면 이전에 봤었던 수정자(Setter) 주입과 같은 것은 이게 외부에서 수정이 가능해버리면 다수의 인원이 다같이 작업을 하는 프로젝트를 진행할때 함부로 접근하여서 고치게 되면 나도 모르게 문제가 발생할 수 있다는 가능성이 있는것이 생각보다 되게 끔찍한 문제인 거 같다. 그리고 심지어 public으로 열어서 동업하는 사람 입장에서도 이것을 하나 하나 다 살펴보고 유지 보수를 해나가야되는 것이 우욱... 하는 느낌이 강하다. 그리고 애초에 생성자 주입은 객체 생성할 때 단 1번만 호출이 되므로 그 다음부터는 우리가 걱정할 필요가 없다는 것이 가장 큰 것 같다.

✔ 실수 방지

실수 방지라고 하는 것은 어찌 보면 우리가 오류같은 것을 찾을때 우리가 뭘 실수했는지 잘 찾고 오류들을 수정할 수 있으면 좋지만 다른 방법들은 애초에 꼬이고 꼬이다 보면 좀 발견하기 어려울 수도 있다. 그래서 생성자 주입 방식에서는 애초에 코드에 실수를 하거나 오류가 발생하는 것을 컴파일 오류로 일어날 수 있게 하는 방법이 있다.

       Member member = new Member(1L, "memberA", Grade.VIP);
       OrderServiceImpl orderService = new OrderServiceImpl();
        orderService.createOrder(1L, "itemA", 10000);    

뭐 이런 코드가 있다고 하였을때 생성자 주입을 하게 되면 애초에 다른 주입 방법과 달리 코드에서부터 컴파일 오류를 일으켜 어떤 것을 주입을 시켜줘야 했는지 바로 파악할 수 있다 그리고 테스트하는 상황에 맞게 어떠한 것을 주입시켜줄지 본인이 선택도 가능하다.

        MemberRepository memberRepository = new MemoryMemberRepository();
        DiscountPolicy discountPolicy = new RateDiscountPolicy();
        Member member = new Member(1L, "memberA", Grade.VIP);
        OrderServiceImpl orderService = new OrderServiceImpl(memberRepository, discountPolicy);
        memberRepository.save(member);

다음과 같이 설정도 해줄 수 있다. 그리고 또 중요한 거 위에서 언급하였듯이 final이라는 키워드를 사용할 수 잇다는 것이 너무나도 중요하다. 그래서 우리가 설계를 할 때부터 애초에 필수로 들어가야하는 의존관계에 대한 것을 설정해주지 않는다면 컴파일 오류를 발생시켜서 이런 실수들을 방지시켜준다는 측면이 있다.

    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;  

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

이러면 당연히 오류가 일어나야 하는데 final 작성하지 않으면 또 이런 부분들 실수하는 것을 나중에 가서 알아차릴 수도 있으니 컴파일 오류를 띄워주는 final이라는 키워드를 사용할 수 있는 것도 주요한 특성 중 하나이다.

여기서 언급하여준 것이 컴파일 오류는 세상에서 가장 빠르고, 좋은 오류다!라는 말씀을 해주셨는데 그 말도 맞는것이 애초에 나중에 코드 길어져서 오류 잡으려고 막 살펴보다가 NPE터져서 막 잘못 찾는것보다 이렇게 컴파일 오류를 띄워서 빠르게 코드를 수정할 수 있도록 알려주는것이 훨씬 좋을 거 같다.

그래서 결론은 되도록 생성자 주입을 사용하되 선택적으로 사용해야할 경우에만 생성자 주입을 사용하자!

📌 옵션 처리 방식

위에서 주입들을 다 하긴 하였지만 실제로 실무에서 뭐 디폴트 로직같은 것들이 있는데 막 어떨 때는 사용해야하고 어떨때는 사용을 하지 않는 경우도 있다고 한다. 그래서 뭐 빈이 등록이 되지 않아도 동작을 해야할 때가 있다고 한다.

처리 방식은 다음과 같다.

  • @Autowired(required = false)로 지정을 하여 수정자 메서드 자체가 호출이 안되게 한다.
  • @Nullable을 활용하여 자동 주입할 대상이 없으면 null이 입력되도록 한다.
  • Optional <> 을 활용하여 Optional.empty가 입력되도록 한다.

예시 코드

   @Test
    void optionTest(){
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(OptionTest.class);
    }


    static class OptionTest{
        @Autowired(required = false)
        public void setNoBean(Member member){
            System.out.println("setNoBean = " + member);
        }

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

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

    }

위 코드에서 required = true로 하면 다으모가 같은 오류가 일어난다. Member라는 bean 자체가 없기 때문이다.

옵션 처리를 통하여 다음과 같이 출력이 된다는 것을 알 수 있다.

애초에 setNoBean자체는 Member가 빈으로 등록이 되지 않았으니 호출조차 하지 않았다는 것을 알 수 있다. 이게 required=false를 하였을때 일어나는 효과로 알 수 있었다.

오늘의 결론

생성자 주입이 짱이다. 그러나 필요에 따라 수정자 주입도 사용하자.

옵션 처리 방식을 통하여 스프링 빈이 없어도 동작을 해야할 때를 대비하자.

출처

  1. 김영한님의 스프링 핵심 원리 기본편(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)
profile
코드를 씹고 뜯고 맛보고 즐기는 것을 지향하는 개발자가 되고 싶습니다

0개의 댓글