프로그래밍/Spring

[Spring boot] 7. 의존관계 자동 주입

daykim 2023. 6. 5. 21:45
김영한 스프링 핵심 원리 - 기본편 정리
 

스프링 핵심 원리 - 기본편 - 인프런 | 강의

스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., - 강의 소개 | 인프런

www.inflearn.com

 

목차

  • 다양한 의존관계 주입 방법
  • 옵션 처리
  • 생성자 주입을 선택해라!
  • 롬복과 최신 트렌드
  • 조회 빈이 2개 이상 - 문제
  • @Autowired 필드 명, @Qualifier, @Primary
  • 애노테이션 직접 만들기
  • 조회한 빈이 모두 필요할 때, List, Map
  • 자동, 수동의 올바른 실무 운영 기준

 

다양한 의존관계 주입 방법


의존관계 주입 방법

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

 

생성자 주입

생성자를 통해 의존 관계를 주입받는 방법이다.

  • 지금까지 진행했던 방법이 생성자 주입이다.
  • 특징
    • 생성자 호출 시점에서 딱 1번만 호출하는 것이 보장된다.
    • 불변, 필수 의존관계에 사용 (주로)
    • 불변 -> 생성자 한 번 호출. 강제로 수정하지 않는 이상 불변이다.
    • 필수 -> private final 변수는 필수적으로 무조건 값이 있어야 한다고 지정한 것이다.

  • @Component가 있다. 컴포넌트 스캔 -> OrderServiceImpl이 스프링 빈에 등록이 될 때 생성자를 호출한다.
  • 생성자에 @Autowired 있네 -> 스프링 컨테이너에서 스프링 빈 꺼내서 주입(memberRepository, discountPolicy)해준다.
  • 생성자가 1개만 있다면 @Autowired 를 생략해도 된다.

 

수정자 주입 (setter 주입)

setter라 불리는 필드의 값을 변경하는 수정자 메서드를 통해 의존관계를 주입하는 방법이다.

  • 특징
    • 선택, 변경 가능성이 있는 의존관계에 사용
    • 선택 -> 생성자에선 필수값이라 했지만, 수정자 주입에선 주입하려는 것이 스프링 빈에 등록이 되지 않았을 수 있다. 그럴때도 사용 가능해서 선택적으로 의존관계 주입이 가능하다고 한다.
    • @Autowired의 기본 동작은 주입할 대상이 없으면 오류가 발생한다.
      그러나 @Autowired(required = false) 로 설정하면 주입 대상이 없어도 동작하게 할 수 있다.
    • 자바빈 프로퍼티 규약의 수정자 메서드 방식을 사용하는 방법이다.
  • 생성자 주입은 클래스를 빈에 등록하며 생성자의 의존성 주입이 일어난다. 그 후에 수정자 주입

 

자바빈 프로퍼티 규약 예시

class Hello {
	private int wldwld;
    
    public void setWldwld(int wldwld) {
    	this.wldwld = wldwld;
    }
    
    public int getWldwld() {
    	return wldwld;
    }
}
  • 자바빈 프로퍼티 : 자바에선 과거부터 필드 값을 직접 변경하지 않고, setXXX getXXX라는 메서드를 통해 값을 읽거나 수정하는 규칙을 만들었다. 그것이 자바빈 프로퍼티

 

필드 주입

필드에 바로 주입하는 방법

  • 특징
    • 코드가 간결하다.
    • 그러나 외부에서 변경이 불가능해 테스트하기 힘들다는 치명적인 단점이 있다.
    • DI 프레임워크가 없으면 아무것도 할 수 없다.
    • 사용하지 말자! 일부 경우에만 특별 용도로 사용
  • 순수한 자바 테스트 코드에는 당연히 @Autowired가 동작하지 않는다.
  • @SpringBootTest 처럼 스프링 컨테이너를 테스트에 통합한 경우에만 가능

 

일반 메서드 주입

일반 메서드를 통해서 주입받을 수 있다.

  • 일반 메서드에 @Autowired 입력해서 주입 받는다.
  •  특징
    • 한 번에 여러 필드를 주입 받을 수 있다.
    • 일반적으로 잘 사용하지 않는다.

 

참고
당연하지만 의존관계 자동 주입은 스프링 컨테이너가 관리하는 스프링 빈이어야 동작한다.
스프링 빈이 아닌 Member 같은 클래스에서 @Autowired 코드를 적용해도 아무 기능도 동작하지 않는다.

 

 

옵션 처리


주입할 스프링 빈이 없어도 동작해야 할 때가 있다.
그런데 @Autowired만 사용하면, required 옵션의 기본값이 true로 되어 있어 자동 주입 대상이 없으면 오류 발생한다.

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

  • @Autowired(required=false) : 자동 주입할 대상이 없으면, 수정자 메서드 자체가 호출 안 됨
  • org.springframework.lang.@Nullable : 자동 주입할 대상이 없으면 null이 입력된다.
  • Optional<> : 자동 주입할 대상이 없으면 Optional.empty 가 입력된다.

  • Member는 스프링 빈에서 관리하는 것이 아니다.
  • noBean1은 메서드 자체가 호출이 안 된것이다.

 

 

생성자 주입을 선택해라!


과거에는 수정자 주입과 필드 주입을 많이 사용했지만, 최근에는 스프링을 포함한 DI 프레임워크 대부분이 생성자 주입을 권장한다.

불변

  • 대부분 의존관계 주입은 한 번 일어나면, 애플리케이션 종료 시점까지 의존관계를 변경할 일이 없다.
    오히려 대부분의 의존관계는 애플리케이션 종료 전까지 안된다. (불변해야 한다.)
  • 수정자 주입을 사용하면, setXXX 메서드를 public으로 열어두어야 한다.
  • 누군가 실수로 변경할 수 도 있고, 변경하면 안 되는 메서드를 열어두는 것은 좋은 설계 방법이 아니다.
  • 생성자 주입은 객체를 생성할 때 딱 1번만 호출되므로, 이후에 호출되는 일이 없다.
  • 따라서 불변하게 설계할 수 있다.

 누락

  • 프레임워크 없이 순수한 자바 코드를 단위 테스트 하는 경우에 다음과 같이 수정자 의존 관계인 경우,
  • @Autowired가 프레임워크 안에서 동작할 때는 의존관계가 없으면 오류가 발생하지만, 지금은 프레임워크 없이 순수한 자바 코드로만 단위 테스트를 수행하고 있다.
  • 이렇게 테스트하면 실행은 되지만, 막상 실행 결과는 NPE(Null Point Exception)이 발생하는데, memberRepository, discountPolicy 모두 의존관계 주입이 누락되었기 때문이다.

  • 생성자 주입을 사용하면, 다음처럼 주입 데이터를 누락했을 때 컴파일 오류가 발생한다.
    그리고 IDE에서 바로 어떤 값을 필수로 주입해야 하는지 알 수 있다.

 

final 키워드

  • 생성자 주입을 사용하며 필드에 final 키워드를 사용할 수 있다.
  • 그래서 생성자에서 혹시라도 값이 설정되지 않는 오류를 컴파일 시점에 막아준다.
    e.g. 코드 누락

  • java: variable discountPolicy might not have been initialized

 

참고
수정자 주입을 포함한 나머지 주입 방식은, 모두 생성자 이후에 호출되기 때문에 필드에 final 키워드를 사용할 수 없다.
오직 생성자 주입에서만 final 키워드를 사용할 수 있다.

 

정리

  • 생성자 주입 방식을 선택하는 이유는, 프레임 워크에 의존하지 않고 순수한 자바 언어의 특징을 잘 살리는 방법이기도 하다.
  • 기본으로 생성자 주입을 사용하고, 필수 값이 아닌 경우에는 수정자 주입 방식을 옵션으로 부여하면 된다.
  • 즉, 생성자 주입과 수정자 주입을 동시에 사용할 수 있다.
  • 항상 생성자 주입을 선택해라. 그리고 가끔 옵션이 필요하면 수정자 주입을 선택해라.
  • 필드 주입은 사용하지 않는게 좋다.

 

 

롬복과 최신 트렌드


막상 개발하면, 대부분이 다 불변이고 그래서 다음과 같이 필드에 final 키워드를 사용하게 된다.
그런데 생성자도 만들어야 하고, 주입 받은 값을 대입하는 코드도 만들어야 한다.
필드 주입처럼 좀 편리하게 사용하는 방법을 위해 기본 코드를 최적화 해보자.

Lombok 세팅

  1. 롬복 build.gradle 추가 (설정, dependencies)
  2. File -> settings -> plugin -> lombok 설치
  3. File -> settings -> 검색창에 annotation processors에서 Enable annotation proccessing 체크

 

Lombok 적용

  • 롬복 라이브러리인 @RequiredArgsConstructor 기능을 사용하면 final이 붙은 필드를 모아 생성자를 자동으로 만들어준다. (Window : Ctrl + F12 누르면 메소드 확인할 수 있다.)
  • 최종 결과 정말 간결한데 이전의 코드와 완전히 동일하다.
  • 롬복이 자바의 애노테이션 프로세서라는 기능을 이용해, 컴파일 시점에 생성자 코드를 자동으로 생성해준다.

 

정리

  • 최근엔 생성자를 딱 1개 두고, @Autowired를 생략하는 방법을 주로 사용한다.
  • 여기에  Lombok 라이브러리의 @RequiredArgsConstructor 함계 사용하면 기능은 다 제공하면서, 코드는 깔끔하게 사용할 수 있다.

 

 

조회 빈이 2개 이상 - 문제


@Autowired는 Type으로 조회한다.

  • 타입으로 조회하기 때문에, 마치 다음 코드와 유사하게 동작한다. (실제로는 더 많은 기능을 제공한다.)
  • ac.getBean(DiscountPolicy.class)

스프링 빈 조회하기에서 학습했듯이 타입으로 조회하면 선택된 빈이 2개 이상일 때 문제가 발생한다.

  • DiscountPolicy의 하위 타입인 FixDiscountPolicy, RateDiscountPolicy 둘 다 스프링 빈으로 선언해보자.
  • 그리고 의존관계 자동 주입을 실행하면 NoUniqueBeanDefinitionException 오류가 발생한다.

  • 오류메시지가 친절하게도 하나의 빈을 기대했는데 fixDiscountPolicy, rateDiscountPolicy 2개가 발견되었다고 알려준다.

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

 

 

@Autowired 필드 명, @Qualifier, @Primary


조회 대상 빈이 2개 이상일 때 해결 방법

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

 

@Autowired 필드 명 매칭

1. @Autowired는 타입 매칭을 시도하고,
2. 이 때 여러 빈이 있으면 필드 이름, 파라미터 이름으로 빈 이름을 추가 매칭한다.

기존코드

@Autowired
private DiscountPolicy discountPolicy

필드 명을 빈 이름으로 변경

@Autowired
private DiscountPolicy rateDiscountPolicy;

필드 명 매칭은 먼저 타입 매칭을 시도하고, 그 결과에 여러 빈이 있을 때 추가로 동작하는 기능이다.

 

@Qualifier 사용

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

  • 주입 시 추가적인 방법을 제공하는 것이지 빈 이름을 변경하는 것은 아니다.
  • 빈 등록 시 @Qualifier를 붙여준다.
  • 주입 시에 @Qualifier를 붙여주고 등록한 이름을 적어준다.

 

  • @Qualifier로 주입할 때, @Qualifier("mainDiscountPolicy")를 못 찾으면,
  • mainDiscountPolicy라는 이름의 스프링 빈을 추가로 찾는다.
  • 하지만, 경험상 @Qualifier는 @Qualifier를 찾는 용도로만 사용하는 것이 좋다고 한다.

 

빈을 직접 등록할 때도@Qualifier를 동일하게 사용할 수 있다.

@Bean
@Qualifier("mainDiscountPolicy")
public DiscountPolicy discountPolicy() {
	....
}

 

정리
1. @Qualifier끼리 매칭
2. 빈 이름 매칭
3. NoSuchBeanDefinitionException 예외 발생

 

@Primary 사용

@Primary는 우선 순위를 정하는 방법이다.
@Autowired 시에 여러 빈이 매칭되면, @Primary가 우선권을 가진다.

  • @Qualifier의 단점은, 주입 받을 때 모든 코드에 @Qualifier를 붙여야 한다는 것이다.
  • 반면, @Primary를 사용하면 모든 코드에 붙일 필요 없다.

 

@Primary, @Qualifier 활용

ex)

  • 다음과 같이 가정해보자.
  • 코드에서 자주 사용하는 메인 데이터베이스의 커넥션을 획득하는 스프링 빈이 있고,
  • 코드에서 특별한 가끔 사용하는 서브 데이터베이스의 커넥션을 획득하는 스프링 빈이 있다.
  • 메인 데이터베이스의 커넥션을 획득하는 빈은 @Primary를 적용해 조회하는 곳에서 @Qualifier 지정 없이 편리하게 조회하고,
  • 서브 데이터베이스 커넥션 빈을 획득할 때는 @Qualifier를 지정해 명시적으로 획득하는 방식으로 사용하면 코드를 깔끔하게 유지할 수 있다.
  • 물론 이 때, 메인 데이터베이스의 스프링 빈을 등록할 때, @Qualifier를 지정해주는 것은 상관없다.

 

우선 순위

  • @Primary는 기본값처럼 동작하는 것이고
  • @Qualifier는 매우 상세하게 동작한다.
  • 이런 경우 어떤 것이 우선권을 가져갈까?
  • 스프링은 자동보단 수동이, 넓은 범위의 선택권 보다는 좁은 범위의 선택권이 우선 순위가 높다.
  • 따라서, @Qualifier의 우선권이 높다.

 

 

애노테이션 직접 만들기


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

  • Ctrl + N -> @Qualifier 검색해서 긁어오기
  • 애노테이션에는 상속이라는 개념이 없다.
  • 이렇게 여러 애노테이션을 모아서 사용하는 기능은 스프링이 지원해주는 기능이다.
  • @Qualifier 뿐 아니라 다른 애노테이션들도 함께 조합해 사용할 수 있다.
  • 단적으로, @Autowired도 재정의할 수 있다.
  • 물론 스프링이 제공하는 기능을 뚜렷한 목적 없이 무분별하게 재정의하는 것은 유지보수에 더 혼란만 가중할 수 있다.

 

 

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


의도적으로 정말 해당 타입의 스프링 빈이 다 필요한 경우도 있다.

  • 예를 들어, 할인 서비스를 제공하는데, 클라이언트가 할인 종류(rate, fix)를 선택할 수 있다고 가정해보자.
  • 스프링을 사용하면 소위 말하는 전략 패턴을 매우 간단하게 구현할 수 있다.

로직 분석

  •  DiscountService는 Map으로 모든 DiscountPolicy를 주입 받는다.
  • 이 때, fixDiscountPolicy와 rateDiscountPolicy가 주입된다.
  • discount() 메서드는 discountCode로 "fixDiscountPolicy"가 넘어오면 map에서 fixDiscountPolicy 스프링 빈을 찾아 실행한다.
  • 물론 "rateDiscountPolicy"가 넘어오면, rateDiscountPolicy 스프링 빈을 찾아 실행한다.

 

주입 분석

  • Map<String, DiscountPolicy> : map의 키에 스프링 빈의 이름을 넣어주고, 그 값으로 DiscountPolicy 타입으로 조회한 모든 스프링 빈을 담아준다.
  • List<DiscountPolicy> : DiscountPolicy 타입으로 조회한 모든 스프링 빈을 담아준다.
  • 만약 해당하는 타입의 스프링 빈이 없으면, 빈 컬렛견이나 Map을 주입한다.

 

참고 - 스프링 컨테이너를 생성하면서, 스프링 빈 등록하기

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

  • 이 코드는 2가지로 나누어 이해할 수 있다.
  • new AnnotationConfigApplicationContext()를 통해 스프링 컨테이너를 생성한다.
  • AutoAppConfig.class, DiscountService.class를 파라미터로 넘기며 해당 클래스를 자동으로 스프링 빈으로 등록한다.

정리하면, 스프링 컨테이너를 생성하며, 해당 컨테이너에 동시에 AutoAppConfig, DiscountService를 스프링 빈으로 자동 등록한다.

 

 

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


pdf 보쟈