ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Spring Boot] 생성자 주입이란? + @Autowired 옵션 처리
    Back-End/Spring Boot 2022. 10. 4. 00:20

     

    의존관계 주입은 크게 생성자, 수정자(setter), 드, 일반 메서드 주입과 같이 4가지가 있는데 생성자 주입이 가장 이상적인 방법이다. 생성자 주입을 사용하자.!

     

    📑 생성자 주입

     

    생성자 주입이란 이름 그대로 생성자를 통해 의존 관계를 주입 받는 방법이다. 생성자 호출 시점에 딱 1번만 호출되는 것이 보장되며 불변, 필수 의존 관계에 사용한다.  생성자가 1개만 있다면 @Autowired를 생략해도 자동 주입이 된다. 물론 스프링 빈에만 해당한다. 

     

    @Component
    public class OrderServiceImpl implements OrderService{
    
        private final MemberRepository memberRepository; //final이 붙어 있음 값을 세팅해줘야한다.!
        private final DiscountPolicy discountPolicy; //인터페이스에만 의존함. BUT) NullPointException 발생
    
        //@Autowired : 스프링 컨테이너에서 스프링 빈을 꺼내 등록해준다.
        //불변 데이터, 그 누구도 값을 변경할 수 없다.
        //생성자가 딱 1개만 있으면 @Autowired를 생략해도 자동 주입 된다.
        @Autowired
        public OrderServiceImpl(MemberRepository memberRepository, @MainDiscountPolicy DiscountPolicy discountPolicy) {
            this.memberRepository = memberRepository;
            this.discountPolicy = discountPolicy;
        }

     

     

    과거에는 수정자 주입과 필드 주입을 많이 사용했지만, 최근 스프링을 포함한 DI 프레임워크 대부분이 생성자 주입을 권장한다. 대부분의 의존관계 주입은 한 번 일어나면 애플리케이션 종료시점까지 의존관계를 변경할 일이 없다. 오히려 대부분의 의존간계는 애플리케이션 종료 전까지 변하면 안된다. ( 불변 ! ) 

     

     

    📌 생성자 주입을 사용하면 final 키워드를 사용할 수 있다.

    혹시라도 생성자에서 값이 설정되지 않는 오류를 컴파일 시점에서 막아준다. 수정자 주입을 포함한 나머지 주입 방식은 모두 생성자 이후에 호출 되므로, 필드에 final 키워드를 사용할 수 없다. 오직 생성자 주입 방식만 final 키워드를 사용할 수 있는 것! 

     

     

     

    📑 @Autowired의 옵션 처리

     

    @Autowired는 의존관계를 자동으로 주입해준다. 즉) 생성자에 @Autowired 를 지정하면, 스프링 컨테이너가 자동으로 해당 스프링 빈을 찾아서 주입한다. 타입이 같은 빈을 찾아서 등록한다. 

     

    종종, 주입할 스프링 빈이 없어도 동작해야 할 때가 있다. 그런데 @Autowried만 사용하면 required의 기본값이 true로 되어 있어 자동 주입 대상이 없으면 오류가 발생한다. 

     

    expected at least 1 bean which qualifies as autowire candidate

     

    자동 주입 대상을 옵션으로 처리하는 방법은 다음과 같다.

    1. @Autowired(required=false) : 자동 주입 대상이 없으면 수정자 메서드 자체가 호출 안됨.
    2. org.springframework.lang.@Nullable : 자동 주입할 대상이 없으면 null이 입력된다.
    3. Optional < >:자동 주입할 대상이 없으면 Optional.empty가 입력된다.

     

    package hello.core.autowired;
    
    import hello.core.member.Member;
    import hello.core.member.MemberService;
    import hello.core.member.MemberServiceImpl;
    import org.assertj.core.api.Assertions;
    import org.junit.jupiter.api.Test;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.annotation.AnnotationConfigApplicationContext;
    import org.springframework.lang.Nullable;
    
    import java.util.Optional;
    
    //옵셥처리
    public class AutowiredTest {
        
        @Test
        void AutowiredOption(){
            ApplicationContext ac = new AnnotationConfigApplicationContext(TestBean.class);
    
    
        }
        
        static class TestBean{
    
            private Member bean;
    
            @Autowired(required = false) // UnsatisfiedDependencyException
            public void setNoBean1(Member noBean1){
                System.out.println("noBean1 = " + noBean1);
            }
    
            @Autowired
            public void setNoBean2(@Nullable Member noBean2){
                System.out.println("noBean2 = " + noBean2);
            }
            
            @Autowired
            public void setNoBean3(Optional<Member> noBean3){
                System.out.println("noBean3 = " + noBean3);
            }
        }
        
    }

     

     

     

    setNotBean1()은 아예 메서드 호출 자체가 안된다. !! 

     

     

     

    📑 롬복 라이브러리를 이용한 @RequiredArgsConstructor 

     

    롬북 라이브러라기 제공하는 @RequiredArgsConstructor 기능을 사용하면 final이 붙은 필드를 모아서 생성자를 자동으로 만들어 준다. 최근에는 생성자를 딱 1개 두고, @Autowired를 생략하는 방법을 선호하기에. Lombok 라이브러리의 기능을 함께 사용하자.

     

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

     

     

    build.gradle에 다음과 같이 Lombok 설정을 추가해주면 사용 가능하다. 물론 롬북 라이브러리가 설치되어 있어야 되는데 설치 방법은 아래 포스팅 참고! 

     

    //lombok 설정 추가 시작
    configurations {
     compileOnly {
     extendsFrom annotationProcessor
     }
    }
    //lombok 설정 추가 끝
    
    dependencies {
     implementation 'org.springframework.boot:spring-boot-starter'
     //lombok 라이브러리 추가 시작
     compileOnly 'org.projectlombok:lombok'
     annotationProcessor 'org.projectlombok:lombok'
     testCompileOnly 'org.projectlombok:lombok'
     testAnnotationProcessor 'org.projectlombok:lombok'
     //lombok 라이브러리 추가 끝
    
     }
    }

     

    https://wonisdaily.tistory.com/2

     

    [Lombok] 이클립스(Eclipse)에 롬북 라이브러리 설치하기

    롬북을 사용하면? 이클립스와 스프링 플러그인 만으로 스프링 개발은 가능하지만, Lombok을 이용하면 Java 개발 시 자주 사용하는 getter/setter, toString(), 생성자 등을 자동으로 생성해주므로 약간의

    wonisdaily.tistory.com

     

     

    그 다음 settings -> annotation processors 검색 -> enable annotation proessing 체크 꼭 필수이다.!! 

     

     

     

     

     

     

    📑 조회 시 빈이 2개 이상

     

    @Autowired는 타입(Type)으로 조회한다. 만약 private DiscountPolicy discountPolicy가 있다면 DiscountPolicy 타입으로 조회하기 때문에 마치, 다음 코드와 유사하게 동작한다. ac.getBean(DiscountPolicy.class) 

     

    DiscountPolicy의 하위 타입으로 fix, rate 2가지가 있다고 가정했을 때 여기서 문제가 발생한다. 

     

    org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name
    'orderServiceImpl' defined in file [C:\Users\thdnj\spring-study\core\core\out\production\classes\hello\core\order\OrderServiceImpl.class]: Unsatisfied dependency expressed through constructor parameter 1; nested exception is org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'hello.core.discount.DiscountPolicy' available: expected single matching bean but found 2: fixDiscountPolicy,rateDiscountPolicy

     

    메시지를 살펴보면 하나의 빈을 기대했는데 2개가 발견되었다고 알려준다. 이때 하위 타입을 직접 지정할 수 있지만, 하위 타입으로 지정하는 것은 DIP를 위배하고 유연성이 떨어진다. 

     

    이럴 때 해결할 수 있는 방법이 3가지 있다. 

     

    1. @Autowired 필드 명 매칭
    2. @Qualifier 빈 이름 매칭
    3. @Primary 사용

     

     

    📌 1. @Autowired 필드 명 매칭

     

    @Autowired는 타입 매칭을 시도하고, 이때 여러 빈이 있으면 필드 이름, 파라미터 이름으로 빈 이름을 추가 매칭한다. 예를들어 @Autowired private DiscountPolicy discountPolicy 이렇게 선언된 기존 코드의 필드명을 빈 이름으로 변경한다.

     

    @Autowired
    private DiscountPolicy rateDiscountPolicy

     

    이렇게 되면 필드 명으로 정상 주입 된다. ! 

     

     

    📌 2. @Qualifier 빈 이름 매칭

     

    @Qualifier는 추가 구분자를 붙여주는 방법이다. 주입시 추가적인 방법을 제공하는 것이지 빈 이름을 변경하는 것은 아니다. 아래와 같이 @Qualifiler("")에 이름을 적어준다. 

     

     

     

    생성자 자동 주입할 땐 @Qualifier로 지정해둔 이름으로 빈을 찾아 반환한다. 

     

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

     

     

     

    📌 3. @Primary 사용

     

    @primary는 우선 순위를 정하는 방법이다. @Autowired 시에 여러 번 매칭 되면 @Primary가 우선권을 가진다. 만약 rateDiscountPolicy가 우선권을 가지게 하려면 다음과 같다.

     

    @Component
    @Primary
    public class  RateDiscountPolicy implements DiscountPolicy { } 

     

    이렇게 설정만 해두고 맨 처음 코드처럼 동작시켜 보면 문제 없이 작동하는 걸 확인할 수 있다. 그럼 @Primary와 @Qualifier 중 어떤 걸 사용하면 좋은거야? 라는 의문이 생길 수 있다. 

     

     

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

     

    메인 데이터베이스의 커넥션을 획득하는 스프링 빈은 @Primary 를 적용해서 조회하는 곳에서 @Qualifier 지정 없이 편리하게 조회하고, 서브 데이터베이스 커넥션 빈을 획득할 때는 @Qualifier 를 지정해서 명시적으로 획득 하는 방식으로 사용하면 코드를 깔끔하게 유지할 수 있다

     

    @Primary는 기본값 처럼 동작하는 것이고, @Qualifier는 매우 상세하게 동작하기 때문에 @Qualifier가 우선권이 높다. 

     

    반응형

    댓글

Designed by Tistory.