ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Spring 핵심 원리 기본편 - 7. 의존관계 자동 주입
    Spring-Boot/스프링핵심원리 - 기본편 2022. 5. 30. 11:14

    다양한 의존 관계주입 방법

    의존관계 주입은 크게 4가지 방법이 있다.

    - 생성자 주입

    - 수정자 주입 (setter 주입)

    - 필드 주입

    - 일반 메서드 주입

     

    1. 생성자 주입

    - 이름 그대로 생성자를 통해서 의존 관계를 주입 받는 방법

    - 특징

         ㆍ생성자 호출 시점에 딱 1번만 호출되는 것이 보장

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

              불변 : setter메서드를 만들지않고 수정을 불가능하게 만들도록 한다

              필수 : 생성자에는 값을 다 채워 넣어야한다

    @Component
    public class OrderServiceImpl implements OrderService{
    
        private final MemberRepository memberRepository;	// final : 값이 존재해야한다.
        private final DiscountPolicy discountPolicy;
    
        @Autowired
        public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
            this.memberRepository = memberRepository;
            this.discountPolicy = discountPolicy;
        }
    }

    - 중요! 생성자가 딱 1개만 있으면 @Autowired를 생략해도 자동 주입 된다. * 물론 스프링 빈에만 해당

     

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

    - setter 라 불리는 필드의 값을 변경하는 수정자 메서드를 통해서 의존관계를 주입하는 방법

    - 특징

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

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

    @Component
    public class OrderServiceImpl implements OrderService{
    
        private MemberRepository memberRepository;
        private DiscountPolicy discountPolicy;
    	
        @Autowired( required = false )
        public void setDiscountPolicy(DiscountPolicy discountPolicy) {
            this.discountPolicy = discountPolicy;
        }
    
        @Autowired
        public void setMemberRepository(MemberRepository memberRepository) {
            this.memberRepository = memberRepository;
        }
    }

    - 참고 : @Autowired 의 기본 동작은 주입할 대상이 없으면 오류가 발생한다. 주입할 대상이 없어도 동작하게 하려면

    @Autowired( required = false ) 로 지정하면 된다

     

    - 참고 : 자바빈 프로퍼티, 자바에서 과거부터 필드의 값을 직접 변경하지 않고, setXxx, getXxx라는 메서드를 통해서 값을 읽거나 수정하는 규칙을만들었는데, 그것이 자바빈 프로퍼티 규약이다.

    * 자바빈 프로퍼티 규약 예시 *

    class Data {
        private int age;
        public void setAge(int age) {
            this.age = age;
        }
        
        public int getAge() {
            return age;
        }
    }

     

    3. 필드 주입

    - 이름 그대로 필드에 바로 주입하는 방법이다

    - 특징

         ㆍ코드가 간결해서 많은 개발자들을 유혹하지만 외부에서 변경이 불가능해서 테스트 하기 힘들다는

             치명적인 단점이 있다

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

         ㆍ사용하지말자!

               ㆍ애플리케이션의 실제 코드와 관계없는 테스트 코드

               ㆍ스프링 설정을 목적으로 하는 @Configuration 같은 곳에서만 특별한 용도로 사용

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

     

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

     

    - 참고 : 어쩌면 당연한 이야기이지만 의존관계 자동 주입은 스프링 컨테이너가 관리하는 스프링 빈이어야 동작한다.

     


    옵션 처리

    - 주입할 스프링 빈이 없어도 동작해야 할 때가 존재한다

    - 그런데 @Autowired만 사용하면 required 옵션 기본값이 true로 되어 있어서 자동 주입 대상이 없으면 오류 발생

     

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

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

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

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

     

    - 참고 : @Nullable, Optional은 스프링 전반에 걸쳐서 지원된다. 예를 들어 생성자 자동 주입에서 특정 필드에만 사용해 된다.

     


    생성자 주입을 선택해라!

    - 과거에는 수정자 주입과 필드 주입을 많이 사용했지만, 최근에는 스프링을 포함한 DI  프레임워크 대부분 생성자 주입을 권장한다.

     

    * 불변 *

    - 대부분의 의존관계 주입은 한번 일어나면 애플리케이션 종료시점까지 의존관계를 변경할 일이 없다. 오히려 대부분의 의존관계는 애플리케이션 종료 전까지 변하면 안된다. ( 불변해야 한다. )

    - 수정자 주입을 사용하면, setXxx 메서드를 public으로 열어두어야 한다

         ㆍ누군가 실수로 변경할 수 도 있고, 변경하면 안되는 메서드를 열어두는 것은 좋은 설계 방법이 아니다.

    - 생성자 주입은 객체를 생성할 때 딱 1번만 호출되므로 이후에 호출되는 일이 없다. 따라서 불변하게 설계할 수 있다

     

    * 누락 *

    - 프레임워크 없이 순수한 자바 코드를 단위 테스트하는 경우

      @Autowired 가 프레임워크 안에서 동작할 때는 의존관계가 없으면 오류가 발생하지만, 순수한 자바 코드만으로 단위테스트 수행가능

     

    * final 키워드 *

    - 초기값아니면 생성자에서만 값을 넣어줄 수 있다! 나머지는 값을 바꿀 수 가 없다.

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

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

     

    - 참고 : 수정자 주입을 포함한 나머지 주입 방식은 모두 생성자 이후에 호출되므로, 필드에 final 키워드를 사용할 수 없다. 오직 생성자 주입만 final 키워드를 사용할 수 있다

     

    * 정리 *

    - 생성자 주입 방식을 선택하는 이유는 여러가지 있지만, 프레임워크에 의존하지 않고, 순수한 자바 언어의 특징을 잘 살리는 방법이기도 하다

    - 기본으로 생성자 주입을 사용하고, 필수 값이 아닌 경우에는 수정자 주입 방식을 옵션으로 부여하면 된다. 생성자 주입과 수정자 주입을 동시에 사용할 수 있다

    - 항상 생성자 주입을 선택해라! 그리고 가끔 옵션이 필요하면 수정자 주입을 선택해라. 필드 주입은 사용하지 않는게 좋다.

     


    롬복과 최신 트랜드

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

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

    - 이 최종결과 코드와 이전의 코드는 완전히 동일하다. 롬복이 자바의 애노테이션 프로세서라는 기능을 이용해서 컴파일 시점에 생성자 코드를 자동으로 생성해준다. 실제 class를 열어보면 다음 코드가 추가되어 있는  것을 확인할 수 있다.

     

    * 정리 *

    최근에는 생성자를 딱 1개를 두고, @Autowired를 생략하는 방법을 주로 사용한다. 여기에 Lombok라이브러리의 @RequiredArgsConstructor 함께 사용하면 기능은 다 제공하면서, 코드는 깔끔하게 사용할 수 있다

     


    조회 빈이 2개 이상 - 문제

    - @Autowired는 타입(Type)으로 조회한다

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

     

    @Autowired
    private DiscountPolicy discountPolicy

    - NoUniqueBeanDefinitionException 오류 발생

     

    - 이때 하위 타입으로 지정할 수 도 있지만, 하위 타입으로 지정하는 것은 DIP를 위배하고 유연성이 떨어진다. 그리고 이름만 다르고, 완전히 똑같은 타입의 스프링 빈이 2개 있을 때 해결이 안된다.

     


    @Autowired 필드명 , @Qualifier, @Primary

    조회 대상 빈이 2개 이상일때 해결방법

    - @Autowired 필드 명 매칭

    - @Qualifier → @Qualifier끼리 매칭 → 빈 이름 매칭

    - @Primary 사용

     

    1. @Autowired의 필드 명 매칭

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

     

    * 기존 코드 *

    @Autowired
    private DiscountPolicy discountPolicy

    * 필드 명을 빈 이름으로 변경 *

    @Autowired
    private DiscountPolicy rateDiscountPolicy

    - 필드 명이 rateDiscountPolicy 이므로 정상 주입

     

    * 필드 명 매칭은 먼저 타입 매칭을 시도하고 그 결과에 여러 빈이 있을 때 추가로 동작하는 기능이다. *

     

    * @Autowired 매칭 정리 *

    1. 타입 매칭

    2. 타입 매칭의 결과가 2개 이상일 때 필드 명, 파라미터 명으로 빈 이름 매칭

     

    2. @Qualifier 사용

    - @Qualifier는 추가 구분자를 붙여주는 방법이다. 주입시 추가적인 방법을 제공하는 것이지 빈 이름을 변경하는 것이 아니다.

    @Component
    @Qualifier("mainDiscountPolicy")
    public class RateDiscountPolicy implements DiscountPolicy {}
    @Component
    @Qualifier("fixDiscountPolicy")
    public class FixDiscountPolicy implements DiscountPolicy {}

    - 주입시에 @Qualifier를 붙여주고 등록한 이름을 적어준다

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

    - @Qualifier 로 주입할 때 @Qualifier("mainDiscountPolicy") 를 못찾으면 어떻게 될까?

    그러면 mainDiscountPolicy라는 이름의 스프링 빈을 추가로 찾는다. 하지만 경험상 @Qualifier는 @Qualifier를 찾는 용도로만 사용하는게 명확하고 좋다

     

    * Qualifier 정리

    1. @Qualifier 끼리 매칭

    2. 빈 이름 매칭

    3. 'NoSuchBeanDefinitionException' 예외 발생

     

    3. @Primary 사용

    - @Primary 는 우선순위를 정하는 방법이다. @Autowired 시에 여러 빈 매칭되면 @Primary가 우선권을 가진다.

     

     

    - 여기까지 보면 @Primary 와 @Qualifier 중에 어떤 것을 사용하면 좋을지 고민

       @Qualifier의 단점은 주입 받을 때 다음과 같은 모든 코드에 @Qualifier 를 붙여주어야 한다는 점이다.

     

    * @Primary, @Qualifier 활용 *

    - 코드에서 자주 사용하는 메인 데이터베이스의 커넥션을 획득하는 스프링 빈이 있고, 코드에서 특별한 기능으로 가끔 사용하는 서브 데이터베이스의 커넥션을 획득하는 스프링 빈이 있다고 생각해보자.

    → 메인 데이터베이스의 커넥션을 획득하는 스프링 빈은 @Primary 를 적용해서 조회하는 곳에서 @Qualifier 지정 없이 편리하게 조회하고,

    서브 데이터베이스 커넥션 빈을 획득할 떄는 @Qualifier 를 지정해서 명시적으로 획득하는 방식으로 사용하면 코드를 깔끔하게 유지할 수 있다.

    물론 이때 메인 데이터베이스의 스프링 빈을 등록할 때 @Qualifier를 지정해주는 것은 상관없다

     

    * 우선 순위 *

    - @Primary는 기본값처럼 동작하는 것이고, @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 {
    }

    - 애노테이션에는 상속이라는 개념이 없다. 이렇게 여러 애노테이션을 모아서 사용하는 기능을 스프링이 지원해주는 기능이다. @Qualifier뿐만 아니라 다른 애노테이션들도 함께 조합해서 사용할 수 있다.

    단적으로 @Autowired도 재정의 할 수 있다. 물론 스프링이 제공하는 기능을 뚜렷한 목적없이 무분별하게 재정의하는 것은 유지보수에 더 혼란만 가중할 수 있다.

     


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

    - 의도적으로 정말 해당 타입의 스프링 빈이 다 필요한 경우도 있다

    - 예를 들어서 할인 서비스를 제공하는데, 클라이언트가 할인의 종류(rate, fix)를 선택할 수 있다고 가정해보자.

      스프링을 사용하면 소위 말하는 전략 패턴을 매우 간단하게 구현할 수 있다.

     

    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);
    
                System.out.println("discountCode = " + discountCode);
                System.out.println("discountPolicy = " + discountPolicy);
    
                return discountPolicy.discount(member, price);
            }
        }
     @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(discountService).isInstanceOf(DiscountService.class);
            assertThat(discountPrice).isEqualTo(1000);
    
            int rateDiscountPrice = discountService.discount(member, 20000, "rateDiscountPolicy");
            assertThat(rateDiscountPrice).isEqualTo(2000);
        }

    * 로직 분석 *

    - DiscountService 는 Map으로 모든 'DiscountPolicy'를 주입받는다. 아때 fixDiscountPolicy, rateDiscountPolicy가 주입

    - discount() 메서드는 discountCode로 fixDiscountPolicy가 넘어오면 map에서 fixDiscountPolicy 스프링 빈을 찾아서 실행한다. 물론 "rateDiscountPolicy" 가 넘어오면 "rateDiscountPolicy" 스프링 빈을 찾아서 실행

     

    * 주입 분석 *

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

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

    - 만약 해당하는 타입의 스프링 빈이 없으면, 빈 컬렉션이나 Map을 주입한다.

     


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

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

    - 결정적으로 자동 빈 등록을 사용해도 OCP, DIP를 지킬 수 있다.

     

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

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

         ㆍ업무 로직 빈 : 웹을 지원하는 컨트롤러, 핵심 비즈니스 로직이 있는 서비스,

                                  데이터 계층의 로직을 처리하는 리포지토리 등이 모두 업무 로직이다.

                                  보통 비즈니스 요구사항을 개발할  때 추가되거나 변경된다.

         ㆍ기술 지원 빈 : 기술적인 문제나 공통 관심사(AOP)를 처리할 때 주로 사용된다. 데이터베이스 연결이나, 

                                 공통 로그 처리처럼 업무 로직을 지원하기 위한 하부 기술이나 공통 기술들이다.

     

    - 업무 로직은 숫자가 많고 문제 파악이 쉬워 자동 기능을 사용하는게 좋다

    - 기술 지원로직은 수가 적고, 애플리케이션 전반에 걸쳐 광범위하게 영향을 미치기에 가급적 수동 빈 등록 사용

     

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

     

     

    * 비즈니스 로직 중에서 다형성을 적극 활용할 때 *

    - 수동 빈으로 등록하거나 또는 자동으로 하면 *특정 패키지에 같이 묶어* 두는게 좋다! 핵심은 보고 이해가 되어야 한다!

    @Configuration
    public class DiscountPolicyConfig {
        @Bean
        public DiscountPolicy rateDiscountPolicy() {
            return new RateDiscountPolicy();
        }
        @Bean
        public DiscountPolicy fixDiscountPolicy() {
            return new FixDiscountPolicy();
        }
    }

    - 설정 정보만 봐도 한눈에 빈의 이름은 물론이고, 어떤 빈들이 주입될지 파악할 수 있다.

    그래도 빈 자동 등록을 사용하고 싶으면 파악하기 좋게 DiscountPolicy의 구현 빈들만 따로 모아서 특정 패키지 모아두자.

     

    - 참고 : 스프링과 스프링 부트가 자동으로 등록하는 수많은 빈들은 예외

    반면에 스프링 부트가 아니라 내가 직접 기술 지원 객체를 스프링 빈으로 등록한다면 수동으로 등록해서 명확하게 들어내는 것이 좋다.

     

    * 정리 *

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

    - 직접 등록하는 기술 지원 객체는 수동 등록

    - 다형성을 적극 활용하는 비즈니스 로직은 수동 등록을 고민해보자

     

     

    출처 :

    인프런 김영한님의 스프링 핵심 원리 - 기본편

    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/

    댓글

Designed by Tistory.