본문 바로가기

Spring

스프링 핵심 원리 - 기본편 : 의존관계 자동 주입(2/2)

스프링 핵심 원리 - 기본편 : 의존관계 자동 주입(2/2)

 

- 롬복과 최신 트랜드

Lombok 라이브러리를 사용하면 생성자, getter setter 와같은 수정자를 직접 작성하지 않고 어노테이션을 통해 간편하게 추가할 수 있다. 때문에 의존관계 주입을 통해 할당된 객체의 불변성은 유지하면서, 필드주입처럼 편리하게 개발할 수 있다.  아래 이미지처럼 @RequiredArgsConsturctor 어노테이션을 사용하고 생성자를 작성하지 않아도된다!!

@RequiredArgsConstructor는 컴파일 단계에서 final로 선언된 필드를 매개변수로 받는 생성자 코드를 생성하여 자동으로 추가해준다. 때문에 컴파일이 완료된 .class 파일에는 아래 이미지처럼 생성자가 추가되고 @RequiredArgsConstructor 는 사라진 것을 알 수 있다.

.class 파일의 @RequiredArgsConstructor 에 의해 생성된 생성자

이밖에도 @Getter, @Setter, @ToString 등 다양하게 제공되는 어노테이션으로 코드를 줄여서 가독성을 높힐 수 있다. 특히 게터와 세터는 직접 코드로 작성하면 필드가 많을경우 줄을 많이 잡아먹어 클래스가 어떤 기능이 있는지 보기 힘들정도로 가독성이 떨어지는데 이 것을 단 두줄의 어노테이션으로 생략할 수 있는게 정말 강력한 기능인 것 같다.

 

 

- 조회 빈이 2개 이상 -문제

컴포넌트 스캔을 통한 자동 의존관계주입에서는 주입 당하는 클라이언트 필드의 타입을 통해 스프링 컨테이너의 빈을 조회하고 주입한다. 때문의 같은 타입의 빈이 여러개 등록되어 있을 경우 어떤 빈을 주입해야할지 모르기 때문에 NoUniqueBeanDefinitionException오류를 발생시킨다. 

 

 

- @Autowired 필드 명 @Qualifier, @Primary

이를 해결하기 위해 필드를 하위 타입으로 지정하는 것은 구현객체에 의존하게 되는 것이므로 DIP에 위배된다. 스프링 빈을 수동으로 등록하지 않으면서 이를 해결하기 위해 아래와 같은 방법들이 있다.

@Autowired 필드명 매칭

만약 주입 가능한 동일한 타입의 빈이 여러개 있을 경우 필드의 이름 또는 파라미터 이름으로 빈 이름을 추가 매칭한다. 아래 이미지처럼 빈의 이름과 같은 이름이 있을 경우 해당 빈을 주입한다.

파라미터의 이름을 등록된 빈의 이름과 같게 작성한다(rateDiscountPolicy)

파라미터나 필드명의 경우 개발자가 빈의 이름을 오타낼 수 있는 가능성이 있으므로 좋은 방법은 아닌거 같다. (내 생각)

 

@Qualifier 사용

@Qualifier 는 추가 빈의 이름 이외에 구분자를 붙여주는 방법이다. 어떤 웹서비스나 게임에서 개인 아이디가 있으면서 본인 만의 고유한 닉네임을 가지는 경우가 있는 것처럼, @Qualifier 어노테이션을 통해 의존관계를 주입할 빈 객체를 특정할 수 있다. 먼저 아래와 같이 클래스 위에 @Qualifier 어노테이션과 이를 구분할 등록할 이름을 적어준다.

RateDiscountPolicy 클래스에 @Qualifier("mainDiscountPolicy") 추가

그리고 의존관계를 주입받을 클라이언트 클래스에서 아래와 같이 @Qualifier을 기입한다.

DiscountPolicy 앞에 @Qualifier 기입

이렇게 하면 주입할 빈을 찾을 때 Qualifier가 가진 이름을 매칭하여 빈을 주입한다. 뭔가 @Autowired의 변수 이름으로 매칭하는 것보다 '빈 중복이 발생할 수 있고 그럴경우 해당 빈을 주입한다' 라는 의미를 한눈에 알려주는 것에서 조금 나은 것 같지만 그래도 이름을 개발자가 직접 기입하는 과정에서 오타가 있을 수 있고 이는 런타임에 개발자가 발견하기 힘든 오류를 만들 수 있을 것 같다.

 

@Primary 사용

@Primary는 우선순위를 정하는 방법이다. @Primary를 클래스에 지정해줌으로써, 이 클래스의 객체와 같은 타입의 빈 있을 때, 의존관계 주입시 @Primary가 지정된 객체가 주입의 우선권을 가진다.

같은 타입의 부모를 가진 두 구현객체

위의 이미지처럼 컴포넌트 스캔을 통해 빈으로 등록될 같은 타입의 클래스가 두개 있고 어느 한쪽이 @Primary 로 지정되어 있다면, 의존관계 주입시 @Primary로 지정되어 있는 쪽이 우선적으로 주입된다.

 

따로 이름을 지정해주는 것이아니라, 우선적으로 주입하고자 하는 클래스에 @Primary를 기입하는 것 뿐이여서 실수도 적고 훨씬 강력해 보인다. 하지만 좀더 상세한 조건에 따라 의존관계를 주입하는데 필요한 유연성은 떨어진다.

 

만약 @Primary와 @Qualifier가 충돌할 경우 @Qualifier가 더 높은 우선 순위를 가진다. 

 

때문에 더 주로 사용하는 스프링 빈에 @Primary를 지정해 사용하고, 특수하게 다른 빈이 필요한 경우에 @Qualifier을 사용해 지정해 주는 방식으로 사용하면 대부분의 경우에 코드를 깔끔하게 할 수 있다.

 

 

- 애노테이션 직접 만들기

위에서 사용한 @Qualifier("mainDiscountPolicy") 과 같이 문자열을 기입하는 방식은 오타가 있어도 컴파일시 타입체크가 안되기 때문에 어노테이션을 직접 만들어서 이를 해결할 수 있다. 

package hello.core.annotation;
import org.springframework.beans.factory.annotation.Qualifier;
import java.lang.annotation.*;

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

이렇게 생성한 빈을 @Qualifier("mainDiscountPolicy")를 사용했던 곳에 대체하여 사용해주기만 하면 된다.

(상)스프링 빈으로 등록될 구현 클래스에 @MainDiscountPolicy 지정/(하)의존관계가 주입되는 생성자에 @MainDiscountPolicy 지정

이 방식을 사용하므로써 오타로 인해 예상지 못한 런타임 오류가 발생하는 것을 막을 수 있다.

 

하지만 어노테이션을 뚜렷한 목표없이 무분별하게 재정의해서 사용하는 것은 개발과 유지보수의 혼란을 가중할 수 있기 때문에 조심해서 신중히 사용해야 한다.

 

 

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

꼭 한가지가 아닌 같은 타입의 모든 빈이 필요할 수도 있다. 이렇게 Map 또는 List 를 통해 등록된 모든 빈을 주입받을 수 있다. 그리고 이를 활용해 전략패턴을 구현하는 테스트 예제를 진행한다.

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

static class DiscountService {
    private final Map<String, DiscountPolicy> policyMap;
    private final List<DiscountPolicy> policies;

    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(discountPolicy.getClass());
        return discountPolicy.discount(member,price);
    }
}

@Test 메소드 findAllBean에서 스프링 컨테이너를 생성한다. AutoAppConfig를 통해 DiscountPolicy 타입을 가지 두개의 빈이 컨테이너에 등록된다. 이 두개의 빈은 DiscountService의 생성자를 통해 policyMap, policies 에 주입된다. 

DiscountPolicy 타입 빈들이 주입된 policyMap 과 policies

이렇게 주입된 빈들은 discount() 메소드에서 매개변수에 따라 필요한 객체를 사용하여 결과를 반환하는 전략 패턴에 활용된다. 이처럼 스프링을 사용하면 전략 패턴을 매우 간단하게 구현가능하다.

 

 

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

수동 빈 설정과 컴포넌트스캔, 자동 주입 사이에서 점점 자동 주입을 선호하고있는 추세이다. 하지만 특수한 경우에는 수동으로 빈을 등록하는 것이 어떤 빈을 등록했는지 한 눈에 알 수 있어 상황에 따라 조합해서 쓴다.

 

결국 이러한 추세의 목적은 개발자를 더 편하게 하는 방향으로 이뤄지므로 상황에 맞춰 더 편리한 쪽으로 사용하면 된다. 기본적으로는 자동 기능을 사용하고 다형성을 적극적으로 활용하거나 특정 기술을 지원하는 객체에 대해서는 수동으로 등록을 해서 어떤 역할을 같는지 명확하게 하는 것이 좋은 방법인 것 같다.

 

 


[참고 강의 출처]

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