[spring기본] 7. 의존관계 자동 주입

이건회·2022년 9월 5일
0

springbasic

목록 보기
9/14
  • 의존 관계 주입은 총 4가지 방법이 있다.

생성자 주입
수정자 주입(setter로 주입하는 것)
필드 주입
일반 메소드 주입

생성자 주입

  • 지금까지 해왔던 방법, 이름 그대로 생성자를 통해 의존 관계 주입
  • 특징 : 생성자 호출 시점에 딱 1번만 호출되는 것이 보장된다. 따라서 한번 세팅하면 다시 세팅되지 못하게 막히므로, 변하지 않는다는 장점이 있다. 따라서 불변, 필수 의존관계에 사용된다

  • 다음 코드에서 밖의 어느 누구도 memberRepository를 수정할 수 없다. 처음 호출되면 시점에 한번만 선언되므로 이에 대한 인스턴스를 변경할 수가 없다.
  • 불변이 장점이라는 것에 집중하자.
  • 또 필수 의존관계에 사용된다. 위 코드에 final로 선언된 것은, 무조건 값이 있어야 한다고 지정한 것이다.

  • 따라서 이렇게 생성자 주입 코드를 주석처리하면 오류가 남
  • 중요 : 생성자가 딱 1개만 있으면 @Autowired를 생략해도 자동 주입이 된다. 생략되었다고 이해하자

수정자 주입

@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;
        }
}
  • setter라는 수정자 메소드를 만들고 이 메소드를 통해 의존관계를 주입하는 방식이다.
  • "선택, 변경" 가능성이 있는 의존관계에 사용한다.
  • 자바빈 프로퍼티 : 자바에서는 과거부터 필드의 값을 직접 변경하지 않고, setXxx, getXxx 라는 메서드를 통해서 값을 읽거나 수정하는 규칙을 만들었는데, 그것이 자바빈 프로퍼티 규약이다. 더 자세한 내용이 궁금하면 자바빈 프로퍼티로 검색해보자.

필드 주입

  • 이름 그대로 필드에 그대로 주입한다.
@Component
    public class OrderServiceImpl implements OrderService {
        @Autowired
        private MemberRepository memberRepository;
        @Autowired
        private DiscountPolicy discountPolicy;
}
  • 코드가 매우 간결하다
  • 의존관계를 필드에 바로 집어넣었다.
  • 그러나, 외부에서 변경이 불가능해서 테스트하기 힘들다.
  • DI 프레임워크가 없으면 아무것도 할 수 없다. 웬만하면 사용하지 말자

일반 메소드 주입

@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)는 스프링 빈이어야 동작한다.

옵션 처리

  • 주입할 스프링 빈이 없어도 동작해야 할 때가 있다.
  • 그런데 @Autowired만 사용하면 required=true 가 기본값이므로, 자동 주입 대상이 없으면 오류가 발생한다.
  • 자동 주입 대상을 옵션으로 해결하려면
    @Autowired(required=false) : 자동 주입할 대상이 없으면 수정자 메서드 자체가 호출 안됨 org.springframework.lang.@Nullable : 자동 주입할 대상이 없으면 null이 입력된다. Optional<> : 자동 주입할 대상이 없으면 Optional.empty 가 입력된다.

  • 주입할 스프링 빈이 없는 Autowired 테스트를 해 보겠다.
  • Member는 자바 코드로 짰으므로 스프링 빈이 아니다.
  • 1번 자체가 호출이 되지 않는다. 자동 주입할 대상이 없으면, 수정자 메소드 자체가 호출되지 않는 것이다.
  • 2번은 null이 입력되었다. 호출이 되지만 자동주입 대상인 스프링 빈이 없어 null 값이 들어온다.
  • 3번은 Optional.empty다. 스프링 빈이 없으면 Optional.empty 값이 들어온다.

생성자 주입 선택

  • 과거에는 수정자 주입을 썼지만, 지금은 대부분이 생성자 주입을 쓴다. 그 이유는 뭘까
    불변

  • 첫 번째는 불변이다. 대부분의 의존관계 주입은 한번 일어나면 애플리케이션 종료시까지 의존관계 변경할 일이 없다. 즉 불변해야 한다.

  • 수정자 주입을 사용하면, setxxx 메소드를 public으로 열어 두어야 한다.

  • 고로 누군가 실수로 변경할 수도 있고, 변경하면 안되는 메소드를 열어두는 것은 좋은 설계 방법이 아니다.

  • 생성자 주입은 객체를 생성할 때 딱 1번만 호출되므로 이후 호출될 일이 없다. 따라서 불변하다.
    누락

  • 두 번째는 누락이다.

  • 만약 orderService를 생성자 주입이 아닌 수정자 주입으로 만들고 테스트하면

  • 다음과 같이 NPE가 발생한다.

  • 이유는 createOrder를 하려면 저장할 memberRepository, discountPolicy의 값이 필요한데 그 값이 들어오지 않았다. 조금 더 이해를 쉽게 하자면

  • 만약 다시 이렇게 생성자 주입으로 변경하면

  • 이렇게 new OrderServiceImpl(); 에 빨간줄이 뜬다.

  • 생성자 주입은 무조건 memberRepository, discountPolicy가 들어와야만 구현체가 생성되도록 했기 때문이다. 그런데 수정자 주입을 사용하면 이 파라미터가 없어도 OrderServiceImpl 객체가 생성되므로 memberRepository, discountPolicy를 필요로 하는 createOrder가 작동할 수가 없다.

  • 따라서 다음과 같이 구현체 값을 파라미터로 넣어주면 테스트가 성공한다.

  • 중요한 것은 오직 생성자 주입을 선택해야지 final 키워드를 선택할 수 있다는 것이다. final로 정해지면 값이 절대 불변하기 때문에, 생성자에서만 값을 세팅하고 절대 바꿀 수 없다는 장점이 있다. 또 개발자가 실수로 discountpolicy나 memberrepository 값을 넣는 코드를 누락해도 컴파일 시점에 오류가 나서 막아줄 수 있다. (컴파일 오류가 가장 최고)

  • 즉, 항상 생성자 주입을 선택하라. 가끔 옵션이 필요할 때만 수정자 주입 사용

롬복과 최신 트렌드

private final MemberRepository memberRepository;
  • 개발을 하면 대부분이 불변이므로 필드에 다음과 같이 final 키워드를 사용한다
  • 그런데 생성자도 만들고 this로 주입 값을 대입하는 코드도 만들고 개 귀찮다. 어떻게 좀 더 편리하게 할 수 있을까.
//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 라이브러리 추가 끝
    testImplementation('org.springframework.boot:spring-boot-starter-test') {
        exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
} }
  • 롬복이라는 것을 사용하자. 먼저 위의존관계 세팅을 build.gradle에 넣어줘야 한다.

  • 또 세팅의 preference plugin에서 Lombok을 깔아야 한다. 나는 미리 깔아놨다.

  • plugins의 Annotation precessors에서 enable annotation processing를 꼭 켜줘야 동작한다.

  • lombok을 사용하면 @Getter,@Setter 어노테이션을 추가하기만 하면 자동으로 getter,setter를 쓸 수 있다.

  • 또 생성자 관련 어노테이션도 지원한다.

  • 이제 OrderServiceImpl을 롬복으로 수정해보자

  • @RequiredArgsConstructor를 사용하면, 아래 생성자 주입 코드와 완벽히 똑같은 코드를 자동으로 만들어준다. @RequiredArgsConstructor가 final 이 붙은 필드를 모아 생성자를 만들어주는 것이다. 그래서 같은 코드가 중복되므로 빨간줄이 뜬다.

  • 따라서 생성자 주입 코드를 따로 짤 필요가 없다. 이 코드와 이전 코드는 완전히 동일하다. 실제 클래스를 열어보면 알 수 있다.

  • 최근에는 생성자를 딱 한개 두고 @Autowired를 사용하는데, @RequiredArgsConstructor가 이 기능을 전부 제공한다.

조회 빈이 2개 이상이면 문제발생

  • @Autowired는 타입으로 조회한다. 어플리케이션을 조회해 예를 들어 DiscountPolicy 에 해당하는 타입을 찾기 때문에 ac.getBean(DiscountPolicy.class)와 유사하게 동작한다.
  • 고로 이전에 배웠듯 조회된 타입이 두개 이상이다? 문제가 당연히 발생한다. DiscountPolicy를 조회했는데 FixDiscount랑 RateDiscount랑 두개 조회되면 뭘 쓸지 모르니까;;


  • 그럼 한번 테스트해보자, FixDiscountPolicy RateDiscountPolicy 둘 다 빈으로 등록해보자. @둘다 @Component를 붙여본다.

  • expected single matching bean but found 2: fixDiscountPolicy,rateDiscountPolicy 이라는 오류가 친절하게 뜬다
  • 그런데 의존 관계 자동 주입에서 이를 해결하는 방법이 있다.

@Autowired 필드 명, @Quilfier, @Primary

  • 조회할 빈이 두 개 이상이면 다음과 같은 방법이 있다

@Autowired 필드 명을 매칭한다
@Qualifier -> @Qualifier끼리 매칭한다 -> 빈 이름 매칭
@Primary 사용

@Autowired 필드 명을 매칭한다

  • @Autowired는 타입으로 매칭을 시도한다고 알고 있다. 그런데 이때 빈 조회가 여러개 되면 필드 이름(파라미터 이름)으로 빈 이름을 추가로 매칭한다.

예를 들어

@Autowired
  private DiscountPolicy discountPolicy
  • 이렇게 조회하면 당연히 Fis, Rate 두 빈이 조회되지만
@Autowired
  private DiscountPolicy rateDiscountPolicy
  • 이렇게 필드 이름을 rateDiscountPolicy로 한다? 그러면 두 개 중에 필드와 빈 이름이 같은 빈을 가져와 주입한다. 한 번 해보자

  • 이렇게 생성자 파라미터 값을 rateDiscountPolicy로 바꾸고 테스트를 돌리면
  • 아까 터졌던 basicScan 테스트가 통과된다.

@Autowired 정리
1. 타입 매칭
2. 타입 매칭의 결과가 2개 이상일 때 필드 명, 파라미터 명으로 빈 이름 매칭

@Qualifier -> @Qualifier끼리 매칭한다

  • @Qualifier는 추가 구분자를 붙여준다. 주입 시 추가적인 구분 옵션을 제공하는 것이지 빈 이름을 변경하는 것은 아니다.

  • 이런 식으로 Rate에 @Qualifier를 붙이고 ("mainDiscountPolicy")를 적어주고
  • Fix에는 @Qualifier를 붙이고("fixDiscountPolicy")로 적으면, 괄호 내 이름으로 구분한다.

  • 그리고 orderServiceImpl의 파라미터 옆에 @Qualifier("mainDiscountPolicy")를 붙여주면, 이와 같은 Qualifier가 있는 빈을 찾아 주입해준다.
@Bean
  @Qualifier("mainDiscountPolicy")
  public DiscountPolicy discountPolicy() {
    return new ...
  }
  • 다음 코드처럼 컴포넌트 스캔 말고 직접 빈을 등록할 때도 @Qualifier를 동일하게 사용할 수 있다.

@Qualifier 정리
1. @Qualifier끼리 매칭
2. 그래도 안되면 빈 이름 매칭
3. 그래도 안되면 NoSuchBeanDefinitionException 예외 발생

@Primary 사용

  • 이 방법을 많이 쓴다.
  • 우선순위를 정하는 방식이다.
  • @Autowired 시 여러 개 매칭되면 @Primary가 우선권을 가진다.

  • 이런 식으로 RateDiscountPolicy에 @Primary가 붙으면, 이 빈이 최상위 빈이 되어 주입된다. 매우 쉽다.
  • 따라서 @Primary를 쓰면 @Qualifier를 붙일 필요가 없다.
  • 그런데 @Primary와 @Qualifier 중 뭐가 더 우선일까?
  • @Primary 는 기본값 처럼 동작하는 것이고, @Qualifier 는 매우 상세하게 동작한다. 이런 경우 어떤 것이 우선권을 가져갈까? 스프링은 자동보다는 수동이, 넒은 범위의 선택권 보다는 좁은 범위의 선택권이 우선 순위가 높다. 즉 자세할 수록 우선이다. 따라서 여기서도 @Qualifier 가 우선권이 높다.

애노테이션 직접 만들기

  • @Qualifier("mainDiscountPolicy") 이렇게 문자를 적으면 컴파일시 타입 체크가 안된다. 문자는 컴파일 타이밍에 타입 체크가 안 되기 때문이다. 다음과 같은 애노테이션을 만들어서 문제를 해결할 수 있다.

  • @Qulifier의 어노테이션 설정값을 전부 복사해

  • @MainDiscountPolicy 어노테이션을 만들어 넣어준다.

  • 그러고 RateDiscountPolicy에도 이 어노테이션 추가 후

  • OrderServiceImpl에도 넣어주면 정상적으로 RateDiscountPolicy가 주입된다.

  • 어노테이션은 상속 개념이 없고, 이를 모으는 것을 스프링이 지원하는 것이므로, 스프링에서 이처럼 어노테이션을 모아 사용할 수 있다.

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

  • 의도적으로 해당 타입의 스프링 빈이 다 필요한 경우도 있다.
  • 예를 들어 구매자가 할인 타입을 선택할 수 있는 경우다.

  • 테스트를 만들어 보자.
  • Map으로 DiscountPolicy를 모두 받았다.
  • List로도 DiscountPolicy를 모두 주입받아본다

  • 그런데 테스트하니 아무 값도 안 뜬다.

  • AutoAppConfig로 자동 의존관계 주입이 되어야 컴포넌트 스캔으로 Fix, Rate가 빈 등록이 된다.

  • 이제 비즈니스 로직을 만들어 보자, VIP고, 가격이 만원이면 Fix일 때 얼마가 할인인지 보는 것이다.
  • 고정할인이면 천원이므로 천원이 나와야 한다.

  • 이제 할인 코드를 넣어줘야 한다. discount 함수에서 Map으로 빈을 모두 받은 policyMap에 discountCode로 넘겨준 할인정책(= 빈 이름)을 넣어주면 할인정책이 선택될 것이다.

로직 분석

  • DiscountService는 Map으로 모든 DiscountPolicy 를 주입받는다. 이때 fixDiscountPolicy , rateDiscountPolicy 가 주입된다.
  • discount () 메서드는 discountCode로 "fixDiscountPolicy"가 넘어오면 map에서 fixDiscountPolicy 스프링 빈을 찾아서 실행한다. 물론 “rateDiscountPolicy”가 넘어오면 rateDiscountPolicy 스프링 빈을 찾아서 실행한다.
    주입 분석
Map<String, DiscountPolicy>
  • map의 키에 스프링 빈의 이름을 넣어주고, 그 값으로 DiscountPolicy 타입으로 조회한 모든 스프링 빈을 담아준다.
List<DiscountPolicy>
  • DiscountPolicy 타입으로 조회한 모든 스프링 빈을 담아준다. 만약 해당하는 타입의 스프링 빈이 없으면, 빈 컬렉션이나 Map을 주입한다.

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

  • 편리한 자동 기능을 기본으로 사용하자. 대부분 자동을 사용하는 추세. 자동 빈 등록을 해도 OCP, DIP를 다 지킬 수 있다.
  • 수동 등록은 언제 사용할까?
  • 애플리케이션은 크게 업무 로직과 기술 지원 로직으로 나눌 수 있다.

업무 로직 빈: 웹을 지원하는 컨트롤러, 핵심 비즈니스 로직이 있는 서비스, 데이터 계층의 로직을 처리하는 리포지토리등이 모두 업무 로직이다. 보통 비즈니스 요구사항을 개발할 때 추가되거나 변경된다.
-> 업무 요구사항이 들어오면 고쳐야 되는 로직들
기술 지원 빈: 기술적인 문제나 공통 관심사(AOP)를 처리할 때 주로 사용된다. 데이터베이스 연결이나, 공통 로그 처리 처럼 업무 로직을 지원하기 위한 하부 기술이나 공통 기술들이다.

  • 업무 로직은 숫자도 많고 유사한 패턴이 많아 준비된 자동 기능을 적극 사용하는 것이 좋다.

  • 기술 지원 로직은 업무 로직에 비해 그 수가 적고 애플리케이션 전체에 걸쳐 광범위하게 영향을 주므로 문제가 발생했을 때 어디가 문제인지 잘 모른다.

  • 그래서 기술 지원 로직은 수동 빈 등록을 통해 설정 정보에 명확하게 들어내는 것이 유지보수에 좋음

  • 또 비즈니스 로직 중에서 다형성을 적극 활용할 때, 예를 들어 DiscountPolicy에 무엇이 들어오는 지 확인하고 싶을 때, 어떤 빈들이 들어올 수 있는지 Appconfig 만들 때 처럼 수동으로 만들어주는게 한눈에 파악하기 좋다. 협업할때 남의 코드를 볼 때 어떤 빈들이 주입되는지 볼 수 있기 때문이다.

  • 이런 경우 만약 자동으로 하면 특정 패키지에 주입 빈들을 같이 묶어 놓는게 좋다

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

profile
하마드

0개의 댓글