스프링

스프링 기초 - 의존관계 주입

ho코딩 2024. 2. 15. 22:40

 

의존관계를 주입하는 방법에는 다양한 방법이 있습니다. 

1. 생성자 주입

2. 수정자 주입(setter)

3. 필드 주입

4. 일반 메서드 주입 

 

생성자 주입

 

말 그대로 생성자를 주입하는 방법입니다.

 

  • 특징
    • 생성자 호출시점에 딱 1번만 호출되는 것이 보장된다
    • 불변, 필수 의존관계에 사용된다

생성자 주입

 

 

수정자 주입(setter)

 

Setter라고 불리는 필드의 값을 변경하는 수정자 메서드를 통해서 의존관계를 주입.

  • 특징
    • 선택,변경 가능성이 있는 의존관계에 사용
    • 자바빈 프로퍼티 규약의 수정자 메서드 방식을 사용하는 방법

수정자 주입

 

생성자 주입과 다르게 따로따로 설정해주어야함. 

 

+@Autowired의 기본 동작은 "주입할 대상이 없으면 오류 발생"이므로, 주입할 대상이 없어도 동작하게 하려면 

@Autowired(required=false)로 지정하면 된다.

 

 

자바 빈 프로퍼티 예시

 

 

 

필드 주입

 

-필드에 바로 주입하는 방법

  • 특징
    • 코드가 간결하지만, 외부에서 변경이 불가능해 테스트가 힘들다는 치명적인 단점
    • DI 프레임워크가 없으면 아무것도 할 수 없다.
    • 사용x 를 권장한다. 

필드 주입

 

-이처럼 필드에 바로 때려박아버리는 것,,

-@SpringBootTest처럼 컨테이너를 테스트에 통합한 경우에만 테스트가 가능하다. 

 

 

 

일반 메서드 주입

 

-일반 메서드를 통해서 주입하는 방법

  • 특징
    • 한번에 여러 필드를 주입 받을 수 있다.
    • 일반적으로 잘 사용 x 

 

 

 

옵션 처리

 

 

주입할 스프링 빈이 없어도 동작해야 할 때가 있다.

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

 

이때 자동 주입 대상을 옵션으로 처리하는 방법은 3가지이다.

 

1.@Autowired(required=false) : 이전에 설명했던 방법입니다. 기본값인 true를 false로 바꿔줍니다.

2.@Nullable : 자동 주입할 대상이 없으면 null 값이 입력됩니다. 

3.Optional<> : 여기서는 Member의 값이 존재할 수도 있고 없을 수도 있습니다. 없는 경우 Optioanl.empty()를 반환합니다.

 

 

결론

 

결론적으로는, 생성자 주입을 선택하는 것이 바람직합니다. 

 

이유

 

1.불변 

-대부분 의존관계는 한번 일어나면 애플리케이션 종료까지 바뀔 일이 없다. 오히려 변하면 안된다.

-수정자 주입을 사용하면, set메서드를 public으로 열어놔야한다 (변경 가능성)

-> 누군가가 실수로 변경할 수 도 있고 변경하면 안되는 메서드를 열어둘 수 도 있다. 

-생성자 주입은 객체를 딱 1번만 생성시에 호출되므로 이후에 호출되는 일이 없다. ->불변하게 설계 가능. 

 

2.누락 

->setter 방식의 주입을 수행하는 경우. 

setter방식
setter 테스트

 

테스트를 돌릴 때, 실행은 되지만 NullPointException이 발생한다.

그 이유는 memberRepository., discountPolicy 모두 의존관계 주입이 누락되었기 때문이다. 

 

 

3.final 키워드 

생성자 주입을 사용하면 필드에 final 키워드를 사용할 수 있다. 

그래서 생성자에서 혹시라도 값이 생성되지 않는 오류를 컴파일 시점에 막아준다. 

 

 

여기서 보면 discountPolicy 필드를 누락했는데, 이 상태로 실행시키면 컴파일 시점에 

`java: variable discountPolicy might not have been initialized`  이렇게 친절하게 오류 메시지를 띄어준다. 

 

 

이외에도 프레임워크에 너무 의존하지 않고, 순수한 자바 언어의 특징을 잘 살리는 방법이기도 하다.

 

+ 기본으로 생성자 주입을 사용하고, 필수 값이 아닌 경우에는 수정자 주입 방식을 옵션으로 부여하는게 현명하다. 

생성자 주입과 수정자 주입을 동시에 사용할 수 있다. 

 

- 아무튼 어지간하면 항상 생성자 주입을 선택해라 ! 수정자 주입은 옵션. 

 

 

최적화

 

아무튼 이제 생성자 주입을 통한 의존관계 주입 방법이 현명하다는 것을 알았다.

 

하지만 생성자를 만드는 것 너무 귀찮다. 

필드가 1~2개면 만들겠는데, 만약 수십개라면..? 생각만해도 귀찮다. 

거의 대부분 불변이고, 필드에 final 키워드도 쓰는데 생성자 만드는 코드를 편하게 만드는 방법이 없을까? 

 

롬복을 사용하면 가능하다. 

 

먼저 코드를 살펴보자. 

 

참고로, 생성자가 딱 1개만 있으면 @Autowired를 생략할 수 있다. - 스프링 기능

롬복 적용 전

 

이 예시에서 보면, OrderServiceImpl의 생성자가 1개이다. 그 위에 @Autowired가 있어야하는데 생략되어있다. 

 

그리고 이제 롬복까지 적용해주면? 

롬복 적용 후

 

코드가 매우 간결해졌다. 

 

롬복(lombok)의 @RequiredArgsConstructor는 final이 붙은 필드를 모아서 생성자를 자동으로 만들어준다!! 

 

즉, '롬복 적용 전' 이미지의 코드와 '롬복 적용 후' 이미지의 코드가 완전히 일치하다는 뜻이다.  

 

plugins {
id 'org.springframework.boot' version '2.3.2.RELEASE'
id 'io.spring.dependency-management' version '1.0.9.RELEASE'
id 'java'
}
group = 'hello'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

//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'
       }
}

 

이 lombok 라이브러리 추가 시작~끝 부분을 build.gradle에 추가하면 된다. 

 

 

조회 빈이 2개 이상

 

 

@Autowired는 타입(Type)으로 조회하여 주입한다. 

때문에, ac.getBean()과 유사하게 동작한다. 

 

앞에서는 한 타입에 1개의 빈을 넣었지만 2개 이상을 넣는다면? 

 

DiscountPolicy의 두 가지 방법인 Fix와 Rate를 모두 DiscountPolicy 타입에 넣었다. 

 

그랬더니

 

NoUniqueBeanDefinitionException오류가 떴다. 

그니까 1개의 빈을 기대했는데, 2개나 있다더라 이말이다. 

 

조회 빈이 2개 이상 -해결방안

 

1.@Autowired 필드 명 매칭 

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

 

 

기존 

@Autowired

private DiscountPolicy discountPolicy에서 위와 같이 변경해주었다. 

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

 

 

 

 

2.Qualifier 사용 

 -@Qualifier는 추가 구분자를 붙여주는 방법이다. 주입시 추가적인 방법을 제공하는 것이지 빈 이름을 변경하는건 x

RateDiscountPolicy를 mainDiscountPolicy로
FixDiscountPolicy는 fixDiscountPolicy로
생성자에 적용

 

그리고 마지막으로 생성자에 적용할 때는, 파라미터 부분에 < @Qualiier("구분자") 파라미터 > 로 해주면 된다.  

-단점: 주입 받을 때 모든 코드에 (@Qualifier("구분자"))를 넣어줘야한다. 

 

 

3.@Primary 사용

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

 

이처럼, 2개 이상의 빈이 있을 경우, RateDiscountPolicy 처럼 위에 @Primary가 붙은게 우선이 된다. 

 

 

 

조회한 빈이 모두 필요한 경우, List/Map

 

할인 서비스를 제공하는데, 클라이언트가 할인의 종류(rate,fix)를 선택할 수 있다고 가정해보자.

이와 같이 흘러간다. 

 

1.

테스트 코드이므로, ApplicationContext를 통해서 컨테이너를 초기화,

설정 정보로(AutoAppConfig와 DiscountService)를 넘긴다. 

 

2.

ac.getBean()으로 검증할 DiscountService 빈을 가져오고 

테스트용으로 사용할 Member 객체를 생성한다. 

 

3. 

discount()메서드를 통해서 discountPrice를 계산하고 값을 담는다. 

discount()메서드는 파라미터로 넘어온 값과 맞는 빈을 찾고 반환한다.

 

만약 discount의 discountCode로 fixDiscountPolicy가 넘어오면 fixDiscountPolicy 빈을 찾아 반환한다. 

 

 

-정리 

1.Map<String, DiscountPolicy>` :

->map의 키에 스프링 빈의 이름을 넣어주고, 그 값으로 `DiscountPolicy` 타입으로 조회한 모든 스프링 빈을 담아준다.

 

2.List<DiscountPolicy>` : 

`DiscountPolicy` 타입으로 조회한 모든 스프링 빈을 담아준다.
만약 해당하는 타입의 스프링 빈이 없으면, 빈 컬렉션이나 Map을 주입한다.
**