Notice
Recent Posts
Recent Comments
Link
관리 메뉴

look-forest

의존 관계 자동 주입 본문

Spring/Spring 핵심 원리

의존 관계 자동 주입

studyHub 2023. 4. 29. 16:58

기본적으로 의존관계 자동 주입은 스프링 컨테이너가 관리하는 빈이어야 동작한다

이번 글에서는 다양한 의존관계 주입 방법과, 그중 어떤 방법을 어떻게 쓰면 좋은지에 대해 중점적으로 알아보겠다.


 

다양한 의존관계 주입 방법

의존관계 주입 방법은 4가지가 있다

  • 생성자 주입
  • setter 주입 (수정자 주입)
  • 필드 주입
  • 일반 메서드 주입

1. 생성자 주입

생성자를 통해 필드 초기화 → final 선언 가능

  • 필수 의존관계에 사용
    - 생성자가 인자(의존관계)를 전달받아야만 객체가 생성되므로
  • 불변 의존관계에 사용
    - 생성자는 생성 시점에 딱 1번만 호출되는 것이 보장
    - 생성자를 쓰면 final을 쓸 수 있다
  • 생성자가 딱 1개만 있으면 @Autowired를 생략해도 된다
    (여러 개면 어떤 생성자에 autowired 해줘야 할지 명확하지 않으니 지정해줘야 함)
final 선언된 레퍼런스 타입 변수는 반드시 선언과 함께 초기화 되어야 한다.
따라서 field 주입이나 setter 주입 시에는 의존관계 주입을 받을 필드에 final 선언할 없다.

2. setter 주입 (수정자 주입)

자바빈 프로퍼티 규약의 수정자 메서드 방식을 사용하는 방법

  • 변경 가능성이 있는 의존관계에 사용
    - 애초에 setter는 중간에 값을 setting 할 용도
  • 선택적 의존관계 주입에 사용
    - 선택적: 해당 bean이 등록되어 있으면 연결, 없으면 연결 x
    - @Autowired(required=false)로 지정

3. 필드 주입

간결하지만..

  • DI 컨테이너가 없으면 값을 주입할 방법이 없어, 순수 자바만으로 테스트하기 힘들다는 치명적인 단점
    (다만 스프링을 띄우는 테스트 코드에선 간결한 맛에 써도 된다)

4. 일반 메서드 주입

setter 주입과 비슷. 생성자, setter 선에서 끝낼 수 있으니 쓸 일 없다

  • 한 번에 여러 필드를 주입받을 수 있다

 


 

의존관계 자동 주입 시점

스프링 컨테이너의 라이프 사이클은 크게빈 등록의존관계 주입 으로 나뉜다

  • 다만 생성자 주입의 경우엔 빈을 생성할 때 생성자를 호출하므로, 빈 등록 단계에 의존관계가 주입된다
    (그래서 생성자(①), setter(②) 둘 다 있으면 autowired가 2번 작동한다)
  • 스프링 컨테이너의 라이프 사이클에서 동작하는 것이므로,
    new()로 객체를 생성할 땐 autowired가 동작하지 않는다
  • 수동 등록(@Bean) 시에도, 파라미터의 의존관계는 자동 주입된다
    어찌 됐건 빈으로 등록되므로, 연결 과정을 거치기 때문

 


 

옵션 처리: 선택적 빈 주입

주입할 스프링 빈이 없어도 동작해야 때가 있다. ( 없이 defualt 동작한다던지..)

그런데 @Autowired required 옵션의 기본값이 true 되어 있어 자동 주입 대상이 없으면 오류가 발생한다.

자동 주입 대상을 옵션으로 처리하는 방법

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

@Nullable, Optional은 스프링 전반에 걸쳐 지원되므로, 생성자 자동 주입에서 특정 필드에만 적용할 수 있다

# 코드로 확인

더보기
Member는 스프링 빈이 아니라 주입해 줄 수 없는 상황

[결과] (setBean1 실행되지 않음)

bean2 = null
bean3 = Optional.empty

※ @Autowired(required=false)가 생성자에 붙을 경우?

더보기

'@Autowired(required=false)가 붙으면, 자동 주입할 대상이 없을 땐 setter 메서드 자체가 호출되지 않는다'라고 하였다. 그럼 setter 주입이 아닌 생성자 주입에 붙을 경우엔 어떻게 될까?

생성자에 @Autowired(required=false)를 붙인 후 테스트를 해봤더니 에러가 터졌다.

 

Member는 빈으로 등록되지 않았다
UnsatisfiedDependencyException : No qualifying bean of type 'hello.core.member.Member' available

 

생성자 주입의 경우에는 @Autowired(required=false)가 적용되지 않는다.

애당초 생성자는 '필수' 의존관계에 사용기도 하고, 생성자가 호출되지 않으면 안 되지 않은가..

 

대신 아래와 같이 적용하면 된다.

생성자의 파라미터에 @Autowired(required=false) 붙이기

null이 주입된다

그냥 @Nullable, Optional 등을 사용하자.

 


 

생성자 주입을 쓰자.

  1. 불변하게 설계할 수 있다
    대부분의 의존관계는 변경할 일이 없다!
    그런데 수정자 주입을 사용하면 setter를 public으로 열어둬야 하기 때문에, 실수로 변경할 여지가 있다.
    변경해선 안 되는 것을 메서드로 열어두는 것은 좋은 설계 방법이 아니다!
    생성자 주입은 객체를 생성할 때 딱 한 번만 호출되므로 불변하게 설계할 수 있다.
  2. 누락 시 컴파일 오류가 나게끔 설계할 수 있다
    컨테이너 없이 순수 자바만으로 단위 테스트를 할 경우, 의존관계를 직접 넣어줘야 할 때가 있다.
    setter를 사용할 경우, 어떤 의존관계가 필요한지 눈에 보이지 않아 누락될 가능성이 있다.
    하지만 생성자를 사용하면 주입 누락 시 컴파일 오류가 바로 보인다!
  3. final을 쓸 수 있다
    생성자에서 혹시라도 값이 설정되지 않는 오류를 컴파일 시점에 막아준다!
컴파일 오류는 세상에서 가장 빠르고, 좋은 오류다!
final 키워드는 초기 값을 직접 세팅하거나 생성자를 이용해야만 한다.
수정자 주입을 포함한 나머지 주입 방식은 모두 생성자 이후에 호출되므로, 필드에 final 키워드를 사용할 수 없다.

 


[TIP] 최신 트렌드 - 롬복 활용하기

더보기

개발을 해보면, 대부분 불변이기 때문에 생성자 하나에 final 키워드를 사용하게 된다.

그런데 생성자도 만들어야 하고 주입 받은 값을 대입하는 것도 귀찮다!

 

2개인데도 양이 적지 않다. 귀찮..

이를 편리하게 자동 생성해주는 것이 lombok 라이브러리이다.

롬복은 자바의 애노테이션 프로세서라는 기능을 이용해서 컴파일 시점에 코드를 자동 생성해준다

 

코드가 눈에 보이진 않지만 컴파일 시점에 생성된다!

 밖에 @Getter, @Setter, @ToString 등으로 해당 코드를 자동 생성해준다.

[참고] @SpringBootTest에서 @Autowired   있는 이유

더보기

테스트를 위한 컨테이너를 만든 적도 없고, 테스트 클래스를 빈으로 등록하지도 않았는데

도대체 어떻게 컴포넌트 스캔과 의존관계 자동 주입이 이뤄지는 걸까?

test 디렉토리에 있는데 어떻게..?

 

자, 일단 @SpringBootTest는 스프링을 띄워서 테스트할 용도라는 것을 상기하자.

 

1. 컨테이너를 만든 적이 없는데?

@SpringBootTest 스프링 부트가 제공하는 스프링 컨테이너를 사용해서 실행한다.

@SpringBootApplication 안에는 @ComponentScan이 존재하며,
package 위치가 프로젝트 최상단이기 때문에 전체 애플리케이션 코드가 컴포넌트 스캔의 대상 된다.

더불어 테스트에서 실행했기 때문에 test 패키지의 하위 패키지 컴포넌트 스캔의 대상!

 

2. 테스트 클래스를 빈으로 등록한 적 없는데?

@SpringBootTest가 붙은 테스트 클래스 빈으로 등록되지 않았지만 예외적으로 @Autowired를 허용해준다.
이것은 JUnit 스프링이 예외적으로 테스트를 편리하게 하도록 허용하는 기능이다.

 

 

 

 


 

 

 

 

 

이어서,

조회된 빈이 여러 개일 경우 처리 방법 

자동/수동 의존 관계 주입 선택 기준에 대해 알아보자.

[문제] 자동 주입 시 조회된 빈이 여러 개인 경우 1 - 충돌 발생

@Autowried는 기본적으로 Type으로 조회하기 때문에, 선택된 빈이 2 이상일  오류 발생한다.

NoUniqueBeanDefinitionException: expected single matching bean but found 2: fixDiscountPolicy, rateDiscountPolicy

 

그렇다고 하위 타입으로 지정하면,

DIP를 위배하고 유연성이 떨어진다.

또한 이름만 다르고 하위 타입마저 같은 빈이 여러 개 있으면 해결이 안 된다.

 

스프링 빈을 수동 등록해서 문제를 해결해도 되지만,

의존 관계 자동 주입에서 해결하는 여러 방법도 있다.

[해결] 명확히 특정하는 3가지 방법

Q. 조회 대상 빈이 여러 개일 경우, 특정하는 방법 3가지는?

더보기

1. @Autowired의 필드명 매칭

2. @Qualifier: @Qualifier끼리 매칭 → 빈 이름 매칭

3. @Primary

 

1. @Autowired의 매칭 방법

@Autowired는 ①타입으로 매칭(하위 타입까지 전부)을 시도하고,

이때 여러 빈이 있다면 필드 이름, 파라미터 이름으로 빈을 매칭 한다.

빈 타입이 DiscountPolicy인 것이 여러 개라면, 그 중 빈 이름이 rateDiscountPolicy인 것으로 매칭한다.

2. @Qualifier

추가 구분자를 붙여주는 방법이다.

1. 빈 등록시, @Qualifier를 붙여준다
2. 주입 시, @Qualifier 구분자를 활용한다

단점: 주입 받을  다음과 같이 모든 코드에 @Qualifier 붙여주어야 한다..

해당하는 @Qualifier("XXX")가 없으면 빈 이름이 XXX인 것을 찾는다.
하지만 @Qualifier는 빈 이름이 아니라 @Qualifier를 찾는 용도로만 사용하는 게 명확하고 좋다.

3. @Primary

우선순위를 정하는 방법이다.

@Autowired 시에 여러 빈이 매칭 되면 @Primary가 우선권을 가진다.

장점: 생성자, 수정자엔 변경이 없어 깔끔하다!

 

@Primary vs @Qualifier

@Primary는 기본 값처럼 동작하는 것이고, @Qualifier는 매우 상세하게 동작한다.

 

Q. 무엇을 사용하겠는가?

더보기

@Primary가 변경의 지점을 하나로 모을 수 있으므로 주로 사용하자.

주로 사용하는 빈에 @Primary 붙이고

특수하게, 가끔 사용하는 빈은 @Qualifier 지정해서 명시적으로 획득하면 된다.

Q. 무엇이 우선 순위가 높은가?

더보기

스프링은 자동보다는 수동, 넒은 범위의 선택권 보다는 좁은 범위 선택권 우선 순위가 높다 (명시적/구체적)

따라서 여기서도 @Qualifier 우선권이 높다.

 

[TIP] @Qualifier 사용  애노테이션 직접 만들기

더보기

Q. @Qualifier("subDiscountPolicy") 문제점?

= 오타 가능성.

문자는 컴파일 타임에 오류 체크가 안된다.

▷ 애노테이션을 만들어서 쓰자

# 애노테이션 생성하는 방법

1. 애노테이션 인터페이스 생성

@Qualifier("mainDiscountPolicy")
public @interface MainDiscountPolicy {
}

2. 사용처에 애노테이션 붙이기

적용할 빈에 붙이고
주입받을 곳에 붙인다
애노테이션에는 상속이라는 개념이 없다.
이렇게 여러 애노테이션을 모아서 사용하는 기능은 스프링이 지원해주는 기능이다.

[문제] 조회된 빈이 여러 개일 경우 2 - 해당 타입의 빈이 모두 필요할 때

가령 할인 서비스를 제공하는데, 클라이언트가 할인의 종류(rate, fix) 선택할  있는 경우를 생각해보자.

클라이언트의 선택에 따라 구체적인 하위 타입의 bean을 꺼내 쓰려면 어떻게 해야 할까?

스프링을 사용하면 Strategy Pattern을 매우 간단히 구현할 수 있다.

void applyDiscount(){
	...
	// 마지막 인자인 DiscountCode에 따라 할인률을 달리 적용하고 싶은 상황
 	// 할인 정책 관련 클래스는 FixDiscountPolicy, RateDiscountPolicy가 있다
 	int fixDiscountPrice = discountService.discountPrice(member, 10000, "fixDiscountPolicy");
 	int rateDiscountPrice = discountService.discountPrice(member, 20000, "rateDiscountPolicy");
    ...
}

// discountPrice를 어떻게 구현해야할까?
public int discountPrice(Member member, int price, String DiscountCode) {
	// 1. DiscountCode에 따라 할인 정책 인스턴스 선택
	// 2. return 선택된 할인 정책 인스턴스.discount(member, price)
}

이때, 빈은 초반에 스프링 컨테이너의 DI로부터 얻을 수 있다.

▷그럼 해당 타입의 빈을 전부 미리 받아놔야 한다..!

[해결] @Autowired가 List/Map에 해당 타입을 모두 넣어준다!

List나 Map에 @Autowired가 붙어있으면 해당 컬렉션을 만들고 넣어준다!

(만약 해당 타입의 빈이 없으면,  컬렉션이나 Map 주입)

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

    @Autowired
    public DiscountService(Map<String, DiscountPolicy> policyMap, List<DiscountPolicy> policies) {
        this.policyMap = policyMap;
        this.policies = policies;
    }
}

 

[활용] 동적으로 빈을 선택해야 할 때, 다형성 코드 구현에 용이

스프링으로부터 주입받은 Map으로 Strategy 패턴을 매우 간단하게 구현할  있다.

더보기

2가지 할인 정책 중 선택할 수 있는 서비스

int discountAmount1 = discountService.discount(member, 30000, "fixDiscountPolicy");
int discountAmount2 = discountService.discount(member, 30000, "rateDiscountPolicy");

Assertions.assertThat(discountAmount1).isEqualTo(1000);
Assertions.assertThat(discountAmount2).isEqualTo(3000);

 

Strategy 패턴을 적용해 구현

@RequiredArgsConstructor
class DiscountService {
    private final Map<String, DiscountPolicy> policyMap;

    public int discount(Member member, int price, String discountCode) {
        DiscountPolicy discountPolicy = policyMap.get(discountCode);
        return discountPolicy.discount(member, price);
    }
}

 

 

 


 

 

컴포넌트 스캔과 의존관계 주입, 자동 vs 수동 선택 기준

지금까지 빈 등록과 의존 관계 주입 방법에 대해 알아보았다.

그렇다면 어떤 경우에 컴포넌트 스캔과 자동 주입을 사용하고,

어떤 경우에 설정 정보를 통해서 수동으로 빈을 등록하고, 수동으로 주입해야 할까?

 

Quiz

  1. OO 기본으로 사용
  2. 기술 지원 객체는 OO 등록
  3. 다형성을 적극 활용하는 비즈니스 로직의 경우엔 OO 등록 고려

 * hint

   자동 방식의 장점: 편하고, 스프링 지원 추세.

   수동 방식의 장점: 한 곳에 모아 놓으므로, 명확하게 한눈에 보인다 .

 

Answer

더보기
  1. 자동
  2. 수동
  3. 수동

 

1. 편리한 자동 빈 등록을 기본적으로 사용하자.

더보기

점점 자동을 선호하는 추세.

  1. 스프링 @Component 뿐만 아니라 @Controller, @Service, @Repository처럼 계층에 맞추어 로직을 자동으로 스캔할  있도록 지원
  2. 스프링 부트 컴포넌트 스캔을 기본으로 사용하고, 스프링 부트의 다양한 빈들도 내부적으로 자동 등록
  3. 편리하다
    설정 정보를 기반으로 애플리케이션을 구성하는 부분과 실제 동작하는 부분을 명확하게 나누는 것이 이상적이지만,
    개발자 입장에서 @Component 넣어주면 되는 일을 @Configuration 설정 정보에 가서 @Bean 적고, new로 객체를 생성하고, 주입할 대상을 일일이 적어주는 정은 상당히 번거롭다..
  4. 관리할 빈이 많아서 설정 정보가 커지면 설정 정보를 관리하는  자체가 부담
  5. 결정적으로 자동  등록을 사용해도 OCP, DIP 지킬  있다
    (@Primary 
    붙이고 떼는 수고 정도는 해야겠지만..)

2. 수동 빈 등록을 사용하면 좋은 경우

① 직접 등록하는 기술 지원 객체

더보기

애플리케이션 로직은 크게 2가지로 나눌 수 있다.

 

1) 업무 로직 빈

   Controller, Service, Repository 계층 등 비즈니스 요구 사항 관련.

   - 갯수도 많고,
   - 유사한 패턴이므로 보통 문제 발생시 어디서 문제가 발생했는지 파악이 쉽다.

    자동 기능 적극 사용하는 것이 좋다.

 

2) 기술 지원 빈

   기술적인 문제나 공통 관심사(AOP)를 처리.
   데이터베이스 연결이나, 공통 로그 처리 처럼 업무 로직을 지원하기 위한 하부 기술이나 공통 기술.

   - 갯수가 적고,

   - 보통 애플리케이션 전반에 광범위한 영향을 미쳐 적용이 잘 되고 있는지 조차 파악하기 어려운 경우가 많다.
     ▶ 가급적 수동 빈 등록으로 명확하게 드러내는 것이 좋다.

   

    ※ DataSource 등 스프링이 자동 등록하는 기술 지원 객체들은 예외. 스프링의 의도대로 사용하자.

 

② 비즈니스 로직 중 다형성이 적극 활용되는 경우

더보기

어떤 빈들이 주입되고 이름은 무엇일지 사용처 코드만 보고 파악이 어렵다.

따라서 한눈에 파악 되도록 설정 클래스에 모아두자!

@Configuration
public class DiscountPolicyConfig {
    
    @Bean
    public DiscountPolicy rateDiscountPolicy() {
	    return new RateDiscountPolicy();
    }
    
    @Bean
    public DiscountPolicy fixDiscountPolicy() {
    	return new FixDiscountPolicy();
    }
}

 

 

 


 

 

참고 자료
스프링 핵심 원리(김영한 님)