[SpringBoot] [3] 7. 의존관계 자동 주입 (2)

윤경·2021년 8월 28일
0

Spring Boot

목록 보기
30/79
post-thumbnail

의존관계 자동 주입 (1)


5️⃣ 조회 빈이 2개 이상 - 문제

➡️ (참고) 코드를 수정하기 전에 이렇게 테스트를 전체적으로 함 싹 돌려주는 것이 좋음 !!

@Autowired는 타입(Type)으로 조회한다. (ex. @Autowired private DiscountPolicy discountPolicy)
그렇기 때문에 ac.getBean(DiscountPolicy.class) 이런식으로 동작한다.

타입으로 조회하게 되면 선택된 빈이 2개 이상일 때 문제가 발생한다.

DiscountPolicy의 하위 타입 FixDiscountPolicy, RateDiscountPolicy 둘 다 스프링 빈으로 선언 되어있다면 어떻게 될까?

✔️ FixDiscountPolicy.java

@Component
public class FixDiscountPolicy implements DiscountPolicy{

✔️ RateDiscountPolicy.java 는 이미 @Component 되어있다.

이렇게 둘 다 컴포넌트 애노테이션을 해놓으면 NoUniqueBeanDefinitionException가 발생한다.

이는 하나의 빈을 기대했는데 두개의 빈이 발견되었다고 알려주는 것이다.

이때, 하위 타입으로 지정할 수도 있지만 이것은 DIP를 위배하며 유연성이 떨어진다.
그리고 이름만 다르고 완전히 똑같은 타입의 스프링 빈이 두 개 있을 때도 해결이 안된다. 스프링 빈을 수동 등록해 문제를 해결할 수도 있지만 의존관계 자동 주입에서 해결하는 여러 방법이 있다.

다음 챕터에서 바로 알아보도록 하자.


6️⃣ @Autowired 필드 명, @Qualifier, @Primary

✔️ 해결 방안

  • @Autowired 필드 명 매칭
  • @Qualifier@Qualifier끼리 매칭 → 빈 이름 매칭
  • @Primary 사용

@Autowired

: 타입 매칭을 시도, 여러 빈이 있다면 필드 이름, 파라미터 이름으로 빈을 추가 매칭

EX) @Autowired private DiscountPolicy discountPolicy ➡️ @Autowired private DiscountPolicy rateDiscountPolicy(빈 이름)

📌 타입 매칭이 우선이고 (2개 이상의 빈이 있을 경우) 그 후 필드 명(또는 파라미터 명) 매칭이 수행된다.

@Qualifier

: 추가 구분자를 붙여주는 방법 (옵션을 하나 더 제공). 추가적인 방법을 제공하는 것 뿐이지 빈 이름을 변경하는 것은 아님

빈 등록시 @Qualifier 붙여주기
✔️ RateDiscountPolicy.java의 일부

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

✔️ FixDiscountPolicy.java의 일부

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

그리고 생성자 자동 주입 시 다음과 같이 @Qualifier 붙여주기
✔️ OrderServiceImpl.java 코드의 일부 (생성자 자동 주입)

✔️ OrderServiceImpl.java 코드의 일부 (수정자 자동 주입)

@Qualifier로 주입할 때 @Qualifier("mainDiscountPolicy")를 찾지 못한다면 mainDiscountPolicy라는 이름의 스프링 빈을 추가로 찾는다.

그렇다고 해서 다른 용도로 사용하지는 말자 !! ⭐️ @Qualifier@Qualifier 찾는 용도로만 사용하기

(컴포넌트 스캔 이외 직접 빈 등록시에도 @Qualifier를 동일하게 사용할 수 있음)

@Qualifier끼리 매칭 → 없다?
➡️ 빈 이름 찾아 매칭 → 없다?
➡️ 최종적으로 NoSuchBeanDefinitionException 예외 발생

@Primary

: 우선 순위를 정하는 방법. @Autowired시 여러 개의 빈이 배칭되면 @Primary우선권을 가진다.

예를 들어 rateDiscountPolicy가 우선권을 가지게 하려면 이렇게 RateDiscount 쪽에 @Primary를 붙여주면 된다.

그렇다면 @Qualifier@Primary 둘 중 어떤 것을 써야할까 ??

@Qualifier는 모든 코드에 @Qualifier를 붙여주어야 한다는 단점이 있다.
반면, @Primary를 사용하면 그런 번거로움이 없다.

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

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

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

📌 이때 @Qualifier@Primary우선 순위가 헷갈리지 않는가?

@Primary는 기본값처럼 동작하고 @Qualifier는 매우 상세하게 동작한다.
스프링은 항상 상세한 것을 우선으로 한다. 따라서 @Qualifier가 우선순위가 높다.


7️⃣ 애노테이션 직접 만들기

@Qualifier("mainDiscountPolicy")는 컴파일시 타입 체크가 안된다. mainnnDiscountPolicy가 되더라도 에러를 뱉지 않아 나중에 곤란해질 수 있다.

애노테이션을 직접 만들어 곤란한 상황을 미리 예방하자.
✔️ hello.core/annotation 패키지 생성 후 MainDiscountPolicy.java annotation 생성

package hello.core.annotation;

import org.springframework.beans.factory.annotation.Qualifier;

import javax.swing.text.Element;
import java.lang.annotation.*;

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

이렇게 애노테이션을 만들어놓으면

✔️ RateDiscountPolicy.java 에서 이렇게 쓸 수 있다.

@Component
@MainDiscountPolicy
public class RateDiscountPolicy implements DiscountPolicy {

✔️ OrderServiceImpl.java

애노테이션은 상속이라는 개념이 없다.

이렇게 여러 애노테이션을 모아 사용하는 기능은 스프링이 지원한다.
@Qualifier뿐 아니라 다른 애노테이션들도 함께 조합해 사용할 수 있다.
단적으로 @Autowired도 재정의 할 수 있다.

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

📌 (mac 기준) command + o: 찾기


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

스프링 빈이 모두 필요한 경우가 있다.

실무에서 자주 쓸법한 예제를 통해 알아보자.

✔️ test/autowired AllBeanTest.java

package hello.core.autowired;

import hello.core.AutoAppConfig;
import hello.core.discount.DiscountPolicy;
import hello.core.member.Grade;
import hello.core.member.Member;
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 java.util.List;
import java.util.Map;

import static org.assertj.core.api.Assertions.*;

public class AllBeanTest {

    @Test
    void findAllBean() {
        // 이렇게하면 AutoAppConfig, DiscountService 둘 다 등록
        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);
    }

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

        // 자동 의존관계 주입이 될 때 fix, rate 둘 다 등록 됨
//        @Autowired  // 생성자 하나라 사실 생략 가능
        // 17,8줄 코드를 입력하고 option+enter로 생성자 만들기
        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);
        }
    }
}

📌 AnnotationConfigApplicationContext ( 컴포넌트 스캔 수행 )
같은 컴포넌트 스캔 기능을 하는 scan 메소드가 있음

‼️ 참고
하 내가 미쳐. 자꾸 빈 충돌 에러가 나는 것이다. 그래서 (RateDiscountPolicy.java에서) (@MainDiscountPolicy) 어노테이션을 주석 처리로 만들고 @Primary로 해결했다. 시간낭비는 항상 열 받아

아무튼,

이 예제는 할인 서비스를 고객이 rate/fix 중 선택할 수 있다고 가정한 것이다.

로직 분석

  • DisCountService는 Map으로 모든 DiscountPolicy(fixDiscountPolicy, rateDiscountPolicy)를 주입 받는다.
  • discount() 메소드는 (위의 코드에서는) discountCode로 fixDiscountPolicy가 넘어오며 map에서 fixDiscountPolicy 스프링 빈을 찾아 실행한다.
    (물론 rateDiscountPolicy가 넘어오면 rateDiscountPolicy 실행)

주입 분석

  • Map<String, DiscountPolicy>: map의 키에 스프링 빈의 이름을 넣어줌. 그 값으론 DiscountPolicy 타입으로 조회한 모든 스프링 빈이 담김.
  • List<DiscountPolicy>: DiscountPolicy 타입으로 조회한 모든 스프링 빈이 담김.
  • 만약 해당 타입 스프링 빈이 없다면, 빈 컬렉션이나 Map을 주입.

📌 스프링 컨테이너 생성하며 스프링 빈 등록하려면 ?

스프링 컨테이너는 생성자에 클래스 정보를 받는다.
여기에 클래스 정보를 넘기면 해당 클래스가 스프링 빈으로 자동 등록되는 것.

new AnnotationConfigApplicationContext(AutoConfig.class, DiscountService.class);
  1. new AnnotationConfigApplicationContext()를 통해 스프링 컨테이너 생성.
  2. AutoAppConfig.class, DiscountService.class를 파라미터로 넘겨 해당 클래스를 자동으로 스프링 빈으로 등록.

스프링 컨테이너를 생성하며 해당 컨테이너에 동시에 AutoAppconfig, DiscountServce를 스프링 빈으로 자동 등록


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

일단, 결론부터 말하자면 자동을 기본으로 사용하자.

😺: 어떤 경우에 컴포넌트 스캔과 자동 주입을 사용하고, 어떤 경우에 설정 정보를 통해 수동으로 빈을 등록하고 의존관계를 수동으로 주입해야 하지 ??

현재 추세는 자동 을 선호한다.
스프링은 @Component, @Controller, @Service, @Repository처럼 계층에 맞춰 일반적인 애플리케이션 로직을 자동으로 스캔하도록 지원한다.
그리고 최근 스프링 부트는 컴포넌트 스캔을 기본으로 사용하며 스프링 부트의 다양한 스프링 빈들도 조건이 맞으면 자동으로 등록하도록 설계되었다.

설정 정보를 기반으로 애플리케이션을 구성하는 부분실제 동작하는 부분을 명확하게 나누는 것이 이상적이다.
하지만 개발자 입장에서 스프링 빈을 하나 등록할 때 @Component만 넣어주면 끝나는 일을
👩🏻‍💻 @Configuration 설정 정보에 가서 👩🏻‍💻@Bean을 적고, 👩🏻‍💻 객체를 생성하고, 또 👩🏻‍💻주입할 대상을 일일이 적어주는 것은 매우 번거롭다.

또한, 관리할 빈이 많아지면 설정 정보가 커지고 이렇게 되면 설정 정보를 관리하는 것 자체가 부담된다.
그리고 결정적으로 ‼️ 자동 빈 등록을 사용해도 OCP, DIP를 지킬 수 있다.

😺: 그럼 수동 빈 등록은 안 써도 되겠네 ??

아 니 다. 그렇다고는 또 할 수 없다.

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

업무 로직 빈

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

업무 로직은 숫자도 매우 많고, 한 번에 개발해야 하며 컨트롤러, 서비스, 레퍼지토리처럼 어느정도 유사한 패턴이 있다.
이런 경우 자동 기능을 적극 사용하는 것이 좋다. 보통 문제가 발생해도 어디서 문제가 발생했는지 명확하게 파악하기 쉽다.

기술 지원 빈

  • 기술 지원 빈: 기술적인 문제나 공통 관심사 (AOP) 를 처리할 때 주로 사용된다.
    DB 연결, 공통 로그 처리처럼 업무 로직을 지원하기 위한 하부 기술이나 공통 기술들이다.

기술 지원 로직은 업무 로직과 비교해 그 수가 매우 적고, 보통 애플리케이션 전반에 걸쳐 빈 하나가 광범위하게 영향을 미친다.
그리고 업무 로직은 문제가 발생했을 때 어디서 문제가 발생했는지 명확하게 잘 드러나지만, 기술 지원 로직은 적용이 잘 되고 있는지 아닌지 조차 파악하기 어려운 경우가 많다.
그래서 이런 기술 지원 로직들은 가급적 수동 빈 등록을 사용해 명확하게 드러내는 것이 좋다.

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

위에서 배운 조회한 빈이 모두 필요할 때 우린 어떻게 했었는가 ? ➡️ List, Map
DiscountService가 의존관계 자동 주입으로 Map<String, DiscountPolicy>에 주입을 받는 상황이었다. 여기 어떤 빈들이 주입되고 이름이 무엇일지 코드만 보고 한 눈에 파악할 수 없었다.

늘 말하듯 개발은 나 혼자하는 것이 아니다. 다른 사람과 협업할 경우 관계를 파악하기 어렵기 때문에 수동 빈으로 등록하거나 자동으로 특정 패키지에 같이 묶어두는 것이 좋다. ➡️ 한 눈에 파악 가능 !!

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

이런 부분들은 스프링 자체를 잘 이해하고 스프링의 의도대로 잘 사용하는 것이 중요하다.
스프링 부트의 경우 DataSource같은 데이터베이스 연결에 사용하는 기술 지원 로직까지 내부에서 자동으로 등록하는데, 이런 부분은 메뉴얼을 잘 참고해 스프링 부트의 의도대로 편리하게 사용하면 된다.

반면, 스프링 부트가 아닌 내가 직접 기술 지원 객체를 스프링 빈으로 등록한다면 수동으로 등록해 명확하게 드러내는 것이 좋다.

😺: 자동 기능을 기본적으로 사용하되 직접 등록하는 기술 지원 객체는 수동 등록을 사용하면 되는구나 !!
다형성을 적극 활용하는 비즈니스 로직은 수동 등록을 고민해봐야겠다 ..


profile
개발 바보 이사 중

0개의 댓글