@Autowired / 생성자 주입 / lombok / @Qualifier / @Primary / @interface (항해일지 32일차)

김형준·2022년 6월 9일
0

TIL&WIL

목록 보기
32/45
post-thumbnail

1. 학습 일지


0) 심화주차 과제 Validation 최적화

  • 🔗 최적화 코드 GitHub
  • 과제 구현 코드 중에 불필요한 작업으로 성능을 떨어뜨리던 코드들 수정 ( 유연하게 생각하기 )
  • As-Is
    • 1) 음식점(메뉴판)에 같은 이름의 음식을 저장할 경우 -> 메뉴판에 저장된 음식들을 불러와서 반복문을 통해 같은 이름이 있는 지 검증
    • 2) 100원, 500원 단위 -> String으로 바꾼 뒤 String 연산으로 해결
  • To-Be
    • 1) 음식점(메뉴판)에 같은 이름의 음식을 저장할 경우 -> JQueyMethod를 활용하여 메뉴id, 음식명을 한번에 넘겨 조회 (Optional findByNameAndMenuId(String name, Long id);)
      • 불필요한 반복문 제거, SQL 쿼리 호출 횟수 감소
    • 2) 100원, 500원 단위 -> ( x % [100, 500] != 0) 으로 코드 간결화
      • 불필요한 연산, 메모리 할당 제거, 가독성 향상
  • Test 검증 완료✔

1) @Autowired 옵션 처리

  • @Autowired(required=false) : 자동 주입할 대상이 없으면 수정자 메서드 자체가 호출 안됨
  • org.springframework.lang.@Nullable : 자동 주입할 대상이 없으면 null이 입력된다.
  • Optional<> : 자동 주입할 대상이 없으면 Optional.empty 가 입력된다.
        @Autowired(required = false)
        public void setNoBean1(Member noBean1) {
	// 아예 호출이 안됨.
            System.out.println("noBean1 = " + noBean1);
        }

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

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

2) 생성자 주입을 선택해라!!

  • 불변성을 보장할 수 있다.

    • 대부분의 의존관계 주입은 한번 일어나면 애플리케이션 종료시점까지 의존관계를 변경할 일이 없다. (오히려 대부분의 의존관계는 애플리케이션 종료 전까지 변해선 안된다)
    • 생성자 주입은 스프링 컨테이너가 빈 등록 시 딱 한번 의존관계를 주입하고, 그 이후에는 바꿀 방법이 없다.
    • 따라서 의존관계의 불변성을 보장하는 장점을 지닌다.
  • 누락을 쉽게 감지할 수 있다. (feat. final키워드)

    • 생성자 주입의 경우 의존관계 주입이 되는 필드 선언부에 final을 붙임으로써 해당 필드의 초기화가 필수임을 명시할 수 있다.
      • 이렇게되면 필드에서 직접 초기화를 하지 않는 한, 생성자에서 해당 필드 값들을 필수적으로 넣어줘야 한다.
    • 따라서 필드 선언부에 final을 붙임으로써! 생성 단계에서 필수 값들을 빠트렸는 지 쉽게 감지할 수 있다 (바로 컴파일 에러 발생됨) 그리고 IDE에서 바로 어떤 값을 필수로 주입해야 하는지 알 수 있다.
      • 컴파일 오류가 세상에서 가장 빠르고, 좋은 오류다! ㅎㅎ (나머지 주입 방식에서는 final 키워드를 사용하지 못한다.)
  • 테스트가 용이해진다.

    • 생성자 주입 방식은 프레임워크에 의존하지 않고 순수한 자바 언어의 특징을 잘 살릴 수 있는 방법이다.
    • 만약 필드 주입이나 수정자 주입으로 의존관계를 주입한다면, DI 프레임워크의 도움 없이는 순수 자바 유닛 테스트가 힘들다.
      • 쉽게 말해, 필드나 수정자에 들어갈 빈을 DI프레임워크로 따로 생성해주지 않으면 NullPointerException이 발생한다.
    • 더 큰 문제는, 순수 자바 테스트에서 해당 객체를 생성할 때 정상적으로 빈이 등록된다는 점이다.
      • NullPointerException이 발생하는 시점은 스프링 빈으로 등록된 후 의존관계 주입에 필요한 빈들이 없을 때이다.
    • 반면 생성자 주입은 애초에 생성자 파라미터에 해당 값들이 없다면 바로 빨간줄이 뜨며 컴파일 에러가 발생하고,
    • 또한, 순수 자바 코드로 필요한 파라미터를 더미값으로 만들어서 넣어줄 수도 있다. (즉, DI 프레임워크의 도움이 필요없다.)
// class OrderServiceImplTest {
    @Test
    void createOrder(){
        // 순수 자바 테스트에서
        // 생성자 주입의 경우, 아래와 같이 생성자 파라미터를 임의로 만들어서 넣어줄 수 있다.
        MemberRepository memberRepository = new MemoryMemberRepository();
        memberRepository.save(new Member(1L, "name", Grade.VIP));
        DiscountPolicy discountPolicy = new RateDiscountPolicy();

        OrderServiceImpl orderService = new OrderServiceImpl(memberRepository, discountPolicy);
        Order order = orderService.createOrder(1L, "itemA", 10000);
        Assertions.assertThat(order.getDiscountPrice()).isEqualTo(1000);
    }
  • 결론
    • 기본으로 생성자 주입을 사용하고, 필수 값이 아닌 경우에는 수정자 주입 방식을 옵션으로 부여하면 된다.
      • 생성자 주입과 수정자 주입을 동시에 사용할 수 있다.
    • 필드 주입은 사용하지 말자

3) 롬복과 최신 트렌드 (@RequiredArgsConstructor)

  • @RequiredArgsConstructor
    • 필드에 final이 붙은 변수들을 파라미터에 가지는 생성자를 만들어준다. (ctrl + f12 누르면 해당 클래스의 메서드 볼 수 있음)
    • 따라서 생성자를 따로 만들어 줄 필요가 없다.
    • 이는 롬복이 자바의 애노테이션 프로세서라는 기능을 이용해서 컴파일 시점에 생성자 코드를 자동으로 생성해주는 것이다.

4) (문제 상황) 조회한 빈이 2개 이상일 경우

  • @Autowired는 기본적으로 Type으로 조회한다.
  • 이 경우 조회하는 빈이 2개 이상일 경우 -> NoUniqueBeanDefinitionException 발생!
    • 이 때 하위 타입으로 지정할 수도 있지만, 이는 DIP를 위배하고 유연성이 떨어진다.

5) (해결 방안) @Autowired 필드 명, @Qualifier, @Primary

  • @Autowired 매칭 정리
    • @Autowired는 타입 매칭을 시도하고, 이때 여러 빈이 있으면 필드이름, 파라미터 이름으로 빈 이름을 추가 매칭한다.
    • 아래 코드의 경우 DiscountPolicy의 파라미터 변수 명은 rateDiscountPolicy이므로, rateDiscountPolicy를 추가 매칭하여 가져온다.
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy rateDiscountPolicy) {
    this.memberRepository = memberRepository;
    this.discountPolicy = discountPolicy;
}
  • @Quilifier
    • 추가 구분자를 붙여주는 방법이다. 주입 시 추가적인 방법을 제공하는 것이지 빈 이름을 변경하는 것은 아니다.
    • @Component 혹은 @Bean이 붙은 클래스에서 @Qualifier("구분자") 를 붙여주고,
    • @Autowired가 붙은 필드 혹은 생성자 파라미터에 @Qualifier("구분자") 를 붙여준다.
    • 이렇게 붙여주면, @Qualifier 끼리 매칭하고, 없을 경우 해당 이름의 빈을 찾는다. 이것도 없다면 NoUniqueBeanDefinitionException 발생
@Component
@Qualifier("mainDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy{

// ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

    @Autowired  // @RequiredArgsConstructor로 대체 가능
    public OrderServiceImpl(MemberRepository memberRepository, @Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }
  • @Primary

    • 우선순위를 정하는 방법이다.
    • 여러 빈이 조회가 되었을 경우, @Primary가 붙은 빈이 우선권을 지닌다.
    • 즉, 여러 후보군 중 하나에 @Primary를 붙여서 우선권을 줄 수 있다.
    • 현업에서 많이 쓰이는 방식이다.
  • 메인 DB와 보조 DB를 사용할 경우, 메인 DB쪽에 @Pimary를 걸어주고, 보조 DB에는 @Qualifier를 걸어주는 방식 등을 사용한다.

  • 항상 자세하게 접근하는 것이 우선권을 가져간다. 즉, @Qualifier가 우선권을 가진다.


6) 애노테이션 직접 만들기

  • @Qualifier에 문자를 적으면 컴파일 시 타입 체크가 안된다.
  • 따라서 @Qualifier("문자열")이 붙은 어노테이션을 직접 생성해준다.
    • 먼저 어노테이션 (@interface)를 하나 생성하여 @Qualifier에 붙은 어노테이션 전부를 복사 붙여넣기 해준다.
    • 추가적으로 @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 {
}

// 필요한 곳에 붙여주기 (해당 예제에선 DiscountPolicy의 구현체)
@MainDiscountPolicy // 직접 만들어준 어노테이션
@Component
public class RateDiscountPolicy implements DiscountPolicy{

// 필요한 곳에 붙여주기 (해당 예제에선 DiscountPolicy이 의존성 주입이 일어나는 곳, 생성자 파라미터)
@Autowired  // @RequiredArgsConstructor로 대체 가능
public OrderServiceImpl(MemberRepository memberRepository, @MainDiscountPolicy DiscountPolicy discountPolicy) {
    this.memberRepository = memberRepository;
    this.discountPolicy = discountPolicy;
}

2. 코멘트

  • 오늘은 주특기 심화주차가 마무리 된 날이다.
  • 마지막으로 과제를 검토하며 리팩토링했다. 일단 구현에 집중하다 보니 가독성도 떨어지고 성능도 저하시키는 코드들이 꽤 있었고, 이를 수정했다.
  • 이어서 핵심 강의를 수강하며 굵직굵직한 스프링 개념들을 정리했다.
  • 하루 하루 배워가는게 정말 많은 것 같다. 꾸준히 정리하고 저장하며 배웠던 순간의 기억을 보관해둬야겠다.. 나중에 다시 찾아와 읽어보며 더욱 견고해질 것이다!
  • 내일부터 드디어 프론트 분들과 협업을 시작한다. 어떻게 보면 진정한 협업의 첫걸음이다보니 긴장도 되지만 설레기도 한다.
  • 내일부터 새로운 팀원분들과 또 화이팅!
profile
BackEnd Developer

0개의 댓글