본문 바로가기
Back-end/Spring

[Spring] 의존관계 자동 주입

by 잔디🌿 2025. 2. 27.

    다양한 의존관계 주입 방법

    생성자 주입

    생성자를 통해서 의존관계를 주입받는다. 

    • 생성자 호출시점에 딱 1번만 호출되는 것이 보장된다.
    • 불변, 필수 의존관계에 사용된다. (private final)
    @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;
         }
    }

    생성자가 딱 하나일 때에는 @Autowired를 생략할 수 있다.(만약 다른 생성자가 있다면 생략하면 안됨!!)

     

    수정자 주입(setter 주입)

    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)로 지정하면 된다.

    참고로, 자바빈 프로퍼티와 자바에서는 필드 값을 직접 변경하도록 하지 않고, getter, setter을 사용한다. 

     

    필드주입

    필드에 바로 주입하는 방법이다.

    • 외부에서 변경이 불가능해서 테스트하기 힘들다는 치명적인 단점이 있다.(목 객체를 넣지 못함)
    • DI 프레임워크가 없으면 아무것도 할 수 없다. (순수한 자바코드로 실행 불가)

    -> 권장되지 않는 방법이다

     

    실행해도 되는 경우

    • 애플리케이션의 실제 코드와 관계 없는 테스트코드'
    • 스프링 설정을 목적으로 하는 @Configuration 같은 곳에서만 사용

    일반 메서드 주입

    일반 메서드를 통해서 주입받을 수 있다.

    • 한번에 여러 필드를 주입받을 수 있다.
    • 일반적으로 잘 사용하지는 않는다. 
    @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 코드를 적용해도 아무 기능도 동작하지 않는다.

     

    옵션처리

    주입할 스프링빈이 없어도 동작해야 할 때가 있다 이 때 그냥 @Autowired를 사용하면 옵션의 기본값이 true이기 때문에 오류가 발생한다.

     

    자동주입대상을 옵션으로(빈이 없어도 오류가 나지 않게)처리하는 방법은 다음과 같다.

     

    • @Autowired(required=false) : 자동주입할 빈이 없으면 수정자 메서드 자체가 호출되지 않는다
    • @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);
     }

    해당 부분에 대한 예제이다

     

    생성자 주입을 선택해라

    최근에는 생성자 주입이 권장된다. 

     

    불변

    • 대부분의 의존관계 주입은 끝날때까지 변하면 안된다.
    • 수정자 주입을 사용하면 setter을 public로 열어두어야한다. -> 누군가 실수로 변경할 수 있다
    • 생성자 주입은 객체를 생성할 때 딱 1번만 호출되므로, 불변하게 설계가 가능하다

    누락

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

    만약 위와 같은 클래스가 있고, 이를 사용한 테스트를 하고자 할 때

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

    이런식으로 순수 자바 코드로 테스트하면 실행은 되지만, null point exception이 발생한다.

    하지만, 만약 생성자 주입방식으로 위 클래스를 설계했다면, 컴파일 오류가 발생하여 누락을 보다 쉽게 잡을 수 있다.

     

    final 키워드

    생성자 주입을 사용하면 final 키워드를 사용할 수 있다. final을 설정해주면 불변성을 보장할 수 있다.

    또한 final 키워드를 사용하면 생성자에서 실수로 특정 필드를 주입하는 것을 누락했을 때 컴파일 시점에 오류를 발생시킨다.

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

     

    롬복과 최신 트랜드

    개발을 하다보면 거의 다 불변이라 final 키워드를 사용하게 된다. 이럴 때 생성자를 하나하나 만드는 것은 귀찮다

    롬복을 사용하면 필드를 정의하는 것만으로도 모든 것을 해결할 수 있다.

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

    이렇게 @RequiredArgsConstructor을 사용하면 final이 들어간 필드에 대해서 자동으로 의존성 주입을 해준다.

     

    조회한 빈이 2개 이상일 때 

    @Autowired는 type로 조회한다. 

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

    따라서 위와 같은 빈 두개가 있는 상태에서 

    @Autowired
     private DiscountPolicy discountPolicy

    위와 같은 코드를 실행하면 NoUniqueBeanDefinitionException 에러가 발생한다.

     

    @Autowired 필드명, @Qualifier, @Primary

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

    • Autowired 필드명 매칭
    • @Qualifier끼리 매칭
    • @Primary 사용

    @Autowired 필드명 매칭

     @Autowired
     private DiscountPolicy discountPolicy

    를 

     @Autowired
     private DiscountPolicy rateDiscountPolicy

    로 바꾸면 문제 없이 주입된다.

     

    @Qualifier 사용

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

    주입할 때에도 위와 같이 어노테이션을 붙여준다.

     

    @Bean
     @Qualifier("mainDiscountPolicy")
     public DiscountPolicy discountPolicy() {
       return new ...
     }

    이렇게 빈을 직접 등록할 때에도 사용할 수 있다.

     

    @Primary사용

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

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

    이런식으로 빈을 생성하고, DiscountPolicy를 주입하면 RateDiscountPolicy가 우선 주입된다.

     

    @Primary보다 @Qualifier가 우선순위가 높다.

    스프링은 기본적으로 자동보다 수동이, 넓은 범위의 선택권 보다는 좁은 범위의 선택권이 우선순위가 높다.

     

    어노테이션 직접 만들기

    @Qulifier("mainDiscountPolicy") 이렇게 문자를 적으면 컴파일 시 타입 체크가 안된다.

    -> 정의할때랑 주입할 때 문자가 달라도 오류가 발생하지 않는다.

    이럴 때를 대비해서 어노테이션을 새로 만들 수 있다.

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

    위와 같이 인터페이스를 만들고 위에는 Qulifier에 붙일 수 있는 곳에 모두 붙일 수 있게 하기 위해서 @Qulifier에 있는 어노테이션들 모두 가져와서 붙여넣기 한다.

    @Autowired
    public OrderServiceImpl(MemberRepository memberRepository,
    @MainDiscountPolicy DiscountPolicy discountPolicy) {
    	 this.memberRepository = memberRepository;
         this.discountPolicy = discountPolicy;
     }
    //수정자 자동 주입
    @Autowired
    public DiscountPolicy setDiscountPolicy(@MainDiscountPolicy DiscountPolicy discountPolicy) {
         this.discountPolicy = discountPolicy;
     }

    어노테이션을 만들면 이렇게 간단하게 적용할 수 있다.

    이런 방식으로 어노테이션을 커스텀하고 만들 수 있지만, 스프링에 있는 기본 어노테이션만 사용해도 충분하고, 잘못건들이면 유지보수에 혼란을 줄 수 있다.

     

    여기서 궁금한 점이 생겼다. 

    그냥 @Qualifier있으니까 굳이 @Qualifier안에 있는 코드까지 다 넣을 필요가 있나?

    찾아보니까 스프링은 어노테이션의 어노테이션까지는 참조하지만

    어노테이션의 어노테이션의 어노테이션까지는 참조하지 못한다고 한다.

     

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

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

    스프링을 이용하면 이를 아주 편리하게 구현할 수 있다.

     

    우선

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

    위와 같이 같은 타입으로 구현된 두가지 클래스가 있다.

     

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

    이런식으로 의존관계주입을 받는다(생성자 하나라 @Autowired 안해도 괜차늠)

    Map는 해당 클래스 명(키)와 객체가 나오고, List는 객체들이 나온다.

     

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

    이때 위와 같이 메서드를 생성하면 다른 DiscountService를 주입한 클래스에서 

    discountService.discount(member,1000,"fixDiscountPolicy");

    이런식으로 호출하면 쉽게 할인정책을 설정할 수 있다.

     

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

    편리한 자동 기능을 기본으로 사용하는 것이 좋다.

    최근 스프링부트는 컴포넌트 스캔을 기본으로 사용한다.

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

     

    수동빈은 언제 사용?

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

     

    업무로직 : 웹을 지원하는 컨트롤러, 핵심 비즈니스 로직이 있는 서비스, 데이터 계층의 로직을 처리하는 리포 지토리등이 모두 업무 로직이다. 보통 비즈니스 요구사항을 개발할 때 추가되거나 변경된다.

     

    기술지원로직기술적인 문제나 공통 관심사(AOP) 처리할 주로 사용된다데이터베이스 연결이나공통 로그 처리 처럼 업무 로직을 지원하기 위한 하부 기술이나 공통 기술들이다.

     

    주로 기술지원로직에 수동빈을 사용한다. 왜냐하면 적용이 잘 되고있는지 아닌지 파악하기 어렵기 때문이다.

     

    또한 다형성을 적극 활용할 때 좋다

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

    앞의 예시처럼 다형성을 활용하여 다양한 유형의 같은 타입의 빈을 관리할 때 딱 보고 이해가 될 수 있도록 할 수 있다.

     

    출처

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

    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

    'Back-end > Spring' 카테고리의 다른 글

    [Spring] 빈 스코프  (1) 2025.03.11
    [Spring] 빈 생명주기 콜백  (1) 2025.02.27
    [Spring] 컴포넌트 스캔  (2) 2024.11.29
    [Spring] 싱글톤 컨테이너  (3) 2024.11.14
    [spring] 스프링 컨테이너와 스프링 빈  (1) 2024.10.04