본문 바로가기
Back-end/java spring

[Spring] 의존관계 자동 주입

by kkkdh 2023. 1. 5.
728x90

다양한 의존관계 주입 방법

의존관계 주입 방법 4가지

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

 

생성자 주입

  • 이름 그대로 생성자를 이용해 의존관계를 주입하는 방법으로, 앞선 개념 공부에서 사용한 코드가 생성자 주입 방식을 통한 의존관계 주입에 해당한다.
  • 특징
    • 생성자 호출 시점에 딱 1번만 호출되는 것이 보장된다.
    • 불변, 필수 의존관계에 사용한다.

 

MemberRepository, DiscountPolicy 두 개의 추상에 의존하는 OrderServiceImpl(주문 서비스 구현체)을 예시로 들어보자.

@Component
public class OrderServiceImpl implements OrderService{
    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;
    
    @Autowired // 이거 생략해도 된다. (생성자가 하나만 존재하기 때문이다.)
    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy){
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }
}

이렇게 의존 관계를 생성자를 통해 주입하는 방식을 의미한다.

 

중요! 생성자가 딱 1개만 있는 경우 @Autowired를 생략해도 의존관계가 자동으로 주입된다!! 물론 스프링 빈에만 해당

 

 

수정자 주입 (setter 주입)

  • setter라고 불리는 수정자 메서드를 통해 필드의 값을 변경해 의존관계를 주입하는 방법이다.
  • 특징
    • 선택, 변경 가능성이 있는 의존관계에 사용한다.
    • 자바 빈 프로퍼티 규약의 수정자 메서드 방식을 사용하는 방법이라고 한다.
@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 메서드를 이용해 의존관계를 주입하는 방식을 수정자 주입이라고 부른다. 수정자 주입의 경우 생성자를 이용한 의존관계 주입을 같이 해도 상관없고, 수정자 주입만을 구현해도 상관없다.

 

스프링 컨테이너에는 사실 두 가지의 라이프사이클이 존재한다.

1. 모든 스프링 빈 등록

2. 그 이후 연관관계를 자동으로 주입한다.

 

 

생성자 주입은 자바 객체를 만들기 위해 어쩔 수 없이 생성자가 호출되어야 하기 때문에, 스프링 빈을 등록하면서 의존관계가 주입되는 특징을 갖고 있다.

 

 

하지만, 수정자 주입은 2번째 단계에서 의존관계가 주입된다.

 

 

참고: Autowired의 기본 동작은 주입할 대상이 없는 경우 오류가 발생한다. 주입할 대상이 없어도 동작하게 하고 싶은 경우 @Autowired(required = false)로 지정하면 된다.
참고: 자바빈 프로퍼티, 자바는 과거부터 필드의 값을 직접 변경하지 않고, setXxx, getXxx라는 메서드를 통해 값을 읽거나 수정하도록 하는 규칙을 만들었는데, 그것이 자바빈 프로퍼티 규약이라고 한다.
class Data{
    private int age;
    
    public void setAge(int age){
        this.age = age;
    }
    
    public int getAge(){
        return age;
    }
}

위와 같은 예시가 자바빈 프로퍼티 규약을 지킨 예시중 하나라고 볼 수 있다.

 

 

필드 주입

이름 그대로 필드에 바로 의존관계를 주입하는 방법이다.

 

코드도 굉장히 심플하다

@Component
public class OrderServiceImpl implements OrderService{

    @Autowired
    private MemberRepository memberRepository;
    
    @Autowired
    private DiscountPolicy discountPolicy;
}

특징

  • 코드가 간결해서 많은 개발자들을 유혹하지만, 외부에서 변경이 불가능해 테스트하기 힘들다는 치명적 단점이 있다.
  • DI 프레임워크가 없다면, 아무것도 할 수 없다.
  • 그러니까 사용하지 말자!
    • 애플리케이션의 실제 코드와 관계없는 테스트 코드 (어차피 테스트할 때만 쓸 거니깐)
    • 스프링 설정을 목적으로 하는 @Configuration 같은 곳에서만 특별한 용도로 사용하자.
참고: 당연히 순수한 자바 코드에서는 동작하지 않는 방식이다. @Autowired를 스프링에서 제공하기 때문이다. @SpringBootTest처럼 스프링 컨테이너를 테스트에 통합한 경우에만 사용이 가능하다.
참고: 다음 코드와 같이 @Bean에서 파라미터에 의존관계는 자동 주입된다. 수동 등록 시 자동 등록된 빈의 의존관계가 필요할 때 문제를 해결할 수 있다.

 

필드 주입 같은 경우에는 한 번 의존관계를 주입한 이후에 다시 접근할 방법이 아예 사라진다. 예를 들어 테스트 상황에서 다른 의존관계를 주입해서 대체하고 싶은 경우가 발생했을 때, 변경할 길이 아예 사라짐

 

그래서 사용하지 않는 것을 권장한다고 한다.

하지만, 이런 식으로 테스트 코드에서 수동으로 의존관계를 주입해야 하는데, 귀찮은 상황에서는 필드 주입을 사용할 수도 있다.

 

 

일반 메서드 주입

일반 메서드를 통해 의존관계를 주입하는 방법

특징

  • 한 번에 여러 필드를 주입받을 수 있다.
  • 일반적으로 잘 사용하지 않는다.

이런식으로 아무 메서드나 이용해서 의존관계를 주입하는 방법을 의미한다.

사실상 수정자 주입이랑 똑같다. 얘를 사용하는 일은 거의 없다.

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

 


옵션 처리

주입할 스프링 빈이 없어도 동작하도록 구현해야 하는 경우가 있다. 예를 들어 스프링 빈을 주입하지 않는 경우에는 디폴트 방식을 정해놓고 그대로 돌아가게 하는 상황들이 존재

 

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

 

자동 주입 대상을 옵션으로 처리하는 방법은 다음과 같다.

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

Optional<>는 Java 8에서 나온 문법으로, Java를 공부하면 알 수 있다.

 

 

Optional이란?

스칼라나 하스켈 같은 소위 함수형 언어에서 사용하는 "존재하지 않는 값"을 표현하기 위한 것이 null이라면, "존재할지 안 할지 모르는 값"을 표현하기 위한 별개의 타입을 갖고 있습니다.

 

그리고 이 타입은 존재할지 안 할지 모르는 값을 제어할 수 있는 여러 가지 API를 제공하기 때문에, 해당 API를 통해 개발자들은 간접적으로 그 값에 접근하게 됩니다.

 

Optinal <>이란 이것에 영감을 받아 Java8에서 새로 도입된 클래스로 마찬가지로 "존재할 수도 있고, 존재하지 않을 수도 있는 객체" 즉, nullable 한 객체를 감싸고 있는 일종의 래퍼 클래스라고 합니다.

 

정리하자면, 직접 다루기에 까다롭고 위험한 null을 담을 수 있는 일종의 특수한 그릇이라고 할 수 있고, 이걸 이용해서 NPE(NULL Pointer Exception)를 유발할 수 있는 null을 직접 다루지 않아도 되는 이점을 얻을 수 있는 문법입니다. (명시적으로 nullable 함을 표현할 수도 있다.)

 

참고: https://www.daleseo.com/java8-optional-after/

 

 

다시 돌아와서 주입할 스프링 빈 없이도 동작하도록 처리하는 방법을 코드로 살펴봅시다.

 

위와 같은 테스트코드를 개념 확인에 사용한다.

위 코드는 세 개의 빈을 등록하고, 각 스프링 빈에 필드 주입을 하는 예시임을 확인할 수 있고, 주입되려 하는 Member는 스프링 빈이 아니다. (스프링 컨테이너에 등록되지 않은 일반 객체입니다.)

 

테스트 코드 실행 결과는 다음과 같다.

첫 번째 스프링 빈 등록 메서드는 실행조차 안되었다.

위와 같이 첫 번째 옵션 처리를 통해 자동 주입 대상이 없는 경우에는 수정자 메서드 자체가 실행이 되지 않음을 확인할 수 있었고, @Nullable을 사용하는 경우 의존관계 주입 대신 null이, 세 번째의 경우 Optional.empty가 입력되는 결과를 확인할 수 있었다.

 

 


생성자 주입을 선택하라!

이 챕터에서는 의존관계 주입 방법 중 생성자 주입을 선택하라고 단언적으로 주장하고 있다.

 

왜 여러 가지 의존관계 주입 방법 중에서 생성자 주입이 좋은지에 대한 이유를 정리해 보자.

 

불변

  • 대부분의 의존관계 주입은 한 번 일어나면, 애플리케이션 종료 시점까지 의존관계를 변경할 일이 없다. 오히려 대부분의 의존관계는 애플리케이션 종료 전까지 변해서는 안된다. (불변해야 한다.)
    • 예를 들어 한 번 공연이 시작하면, 중간에 배역이 바뀌면 안 되는 경우를 들 수 있을 것 같다.
  • 수정자 주입을 사용하면, setXxx method를 public으로 열어두어야 한다. -> 다른 사람의 수정 가능성이 열린다.
  • 누군가 실수로 변경할 수도 있고, 변경하면 안 되는 method를 열어두는 것 자체가 좋은 설계 방식이 아님
  • 생성자 주입을 사용하는 경우, 객체를 생성할 때 딱 한 번만 의존관계가 주입되므로, 불변하게 설계할 수 있다.

 

누락

프레임워크 없이 순수한 자바 코드를 단위 테스트 하는 경우에 수정자 주입을 사용하는 경우를 살펴보자.

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;
    }
    
    //...
}

수정자 주입을 이용하는 경우 스프링 컨테이너 없이 순수한 자바 코드만을 이용해 테스트하면, 의존관계의 주입이 없더라도 컴파일 시점에서 문제를 알아차릴 수 없다.

 

물론, 위 코드가 스프링 프레임워크 안에서 동작할 때에는 문제 될 부분이 없음

 

다음과 같은 테스트를 수행하면, 실행은 되는 결과를 확인할 수 있다.

생성자 주입을 사용하면, 컴파일 에러가 발생해 사전에 오류를 방지할 수 있다.

하지만, 막상 실행이 되더라도 NPE(Null Point Exception)이 발생하고, 이는 올바른 의존관계 주입이 이루어지지 않았기 때문이다.

 

그러나 생성자 주입을 활용하면, 컴파일 에러가 발생해 코드상에서 사전에 문제 되는 상황을 확인할 수 있게 된다.

이렇게 코드를 고치면 이제 문제가 발생하지 않는다.

final 키워드 사용 가능

이에 더불어 생성자 주입을 사용하면, 필드에 final 키워드를 사용할 수 있다. 그래서 생성자에 혹시라도 값이 설정되지 않는 경우 오류를 컴파일 시점에서 막아준다.

(이는 final 키워드가 붙은 변수의 경우 반드시 초기에 값이 갱신되거나, 생성자를 통해 갱신되어야 하기 때문이다.)

 

final 키워드는 cpp에서 const와 같은 의미를 지니는 것 같다.

 

기억하자!: 컴파일 에러가 가장 빠르고 좋은 에러이다

정리

  1. 생성자 주입 방식을 선택하는 이유에는 여러가지가 있지만, 생성자 주입이 프레임워크에 의존하지 않고 순수한 자바 언어의 특징을 잘 살린다는 점이 가장 중요
  2. 기본으로 생성자 주입을 사용하고, 필수 값이 아닌 경우 수정자 주입 방식을 옵션으로 부여하면 된다. (생성자 주입과 수정자 주입을 동시에 사용할 수 있다.)
  3. 항상 생성자 주입을 선택하자!! 그리고 가끔 옵션이 필요하면, 수정자 주입을 선택하자. 필드 주입은 그냥 쓰지 말자!

 

 


Lombok과 최신 트렌드

 

build.gradle에 lombok 설정을 위한 추가 정보를 작성
의존관계도 작성해준다.

작성 이후에 build.gradle의 우측 상단에서 갱신을 해주고

lombok 설치 완료
Annotatino Processors 에서 annotation processing을 적용해야 한다.

이렇게 세팅하면, 이제 intelliJ에서 lombok을 사용할 수 있게 된다.

 

lombok을 사용하면, annotation processing으로 귀찮은 코드를 대신 짜준다. 실무에서 정말 많이 사용한다고 함.

이렇게 단순히 annotation을 작성하는 것 만으로 일일이 작성한 메서드들을 알아서 만들어준다.

위의 코드 예시에서는 @Getter, @Setter, @ToString annotation을 이용해 getter, setter method와 심지어 toString method까지 annotation processing에 의해 자동으로 구현되는 결과를 확인할 수 있다.

예시 코드의 실행 결과

롬복에서 제공하는 것 중에 @RequiredArgsConsturctor라는 어노테이션이 있는데, 이건 우리가 이전에 생성자 주입을 위해 구현한 코드를 그대로 만들어준다.

 

자세하게는 final keyword가 붙어있는 필드가 required 필드이므로, required 필드의 값을 갱신하는 생성자를 자동으로 만들어주는 어노테이션이라고 할 수 있다.

 

그 결과 코드를 아래와 같이 줄여준다.

생성자를 개발자가 만들어줄 필요가 없어졌다

정리

최근에는 생성자를 딱 1개만 두고, @Autowired를 사용하는 방법을 주로 사용하는데, 여기에 Lombok 라이브러리의 @RequiredArgsConstructor까지 함께 사용하면, 기능은 전부 제공하면서, 코드는 필드 주입보다 더 깔끔하게 사용할 수 있게 된다.

 

 


조회한 빈이 2개 이상이라면 어떻게 해야 할까?? - 문제제기

이는 @Autowired가 타입(Type)으로 조회하기 때문에, 의존관계 주입을 위한 스프링 빈의 조회 과정에서 여러 개의 빈이 조회되는 문제가 발생할 수 있다.

 

스프링 빈의 조회에서 학습했듯 타입으로 조회하는 경우 여러 개의 빈이 조회되기(해당 타입을 상속한 객체가 여러 개인 경우) 때문에 발생하는 문제이다.

 

예를 들어 DiscountPolicy 타입을 상속한 FixDiscountPolicy, RateDiscountPolicy 두 개 전부를 스프링 빈으로 선언해 보자.

(기존에는 RateDiscountPolicy만 스프링 빈으로 선언했다.)

당연하게도 테스트가 일부 실패한다.

이는 하나의 스프링 빈에 대한 조회를 기대했으나, 여러 개가 조회되었기 때문에 발생한 오류이다.

(NoUniqueBeanDefinition 오류가 발생)

 

이때, 하위 타입으로 지정할 수도 있지만(의존관계 주입을 위한 타입), 하위 타입으로 지정하는 행위는 DIP를 위배(추상이 아닌 구체에 의존)하고, 유연성이 떨어진다.

 

그리고 이름만, 다르고 완전히 똑같은 타입의 스프링 빈이 2개 있는 경우 또다시 문제가 발생한다.

 

스프링 빈을 수동으로 등록해 문제를 해결할 수도 있지만, 의존 관계 자동 주입에서 해결하는 여러 가지 방법이 있는데, 하나씩 정리해보도록 하자.

 


@Autowired 필드 명, @Qualifier, @Primary

여러 개의 빈이 연결될 때, 해결하는 방법을 하나씩 정리해 보자.

 

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

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

 

@Autowired 필드 명 매칭

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

 

이게 무슨 말인지 알아보기 위해 기존 코드의 일부를 가져와보자.

//이랬던 기존의 코드를
@Autowired
private DiscountPolicy discountPolicy

//이렇게 변경해보자.
@Autowired
private DiscountPolicy rateDiscountPolicy

바로 위와 같이 DiscountPolicy라는 타입으로 빈을 탐색하는 경우 2개 이상의 빈을 조회하게 되고, 기존의 코드는 이로 인해 충돌이 발생했다.

 

그래서 필드명 매칭은 두 번째로 작성한 코드와 같이 rateDiscountPolicy로 필드의 이름을 변경해서 필드 명을 기반으로 빈을 선택할 수 있다는 것을 의미한다.

 

이렇게 필드명 매칭을 통해 rateDiscountPolicy를 기준으로 빈을 하나만 조회할 수 있게 된다. 

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

이렇게 파라미터의 이름을 변경해도 된다.

이건 필드 인젝션을 하더라도 적용되는 내용이다. (필드의 이름을 내가 의존관계로 주입하고 싶은 객체의 이름과 일치하도록 넣어주면 알아서 찾아준다.)

 

@Autowired 매칭 정리

  1. 타입 매칭
  2. 타입 매칭의 결과가 2개 이상일 때, 필드 명 또는 파라미터 명으로 빈 이름을 매칭한다.

 

 

@Qualifier 사용

@Qualifier는 추가 구분자를 붙여주는 방법이다. 주입 시 추가적인 방법을 제공하는 것이지, 빈의 이름을 변경하는 것은 아님에 주의해야 한다.

fixDiscountPolicy라는 추가 구분자를 부여
mainDiscountPolicy라는 추가 구분자를 부여

@Qualifier를 이용해 각각의 스프링 빈에 추가 구분자를 부여하고, 다음 코드와 같이 Qualifier를 이용해 부여한 추가 구분자를 이용해 빈을 조회할 수 있다.

mainDiscountPolicy라는 추가 구분자를 갖는 빈을 선택하도록 지정한다.
위의 예시와 같이 @Qualifier를 사용할 수 있다.

그런데, @Qualifier를 사용할 때, 알맞은 추가 구분자를 못 찾는다면 어떻게 될까??

(여기서는 @Qualifier("mainDiscountPolicy") 일 것이다.)

 

이런 경우에는 mainDiscountPolicy라는 이름의 스프링 빈을 추가로 찾는다고 한다. 하지만, 김영한 님의 경험상 @Qualifier는 @Qualifier를 찾는 용도로만 사용하는 것이 좋다고 한다! (애매한 것은 하지 말자)

 

다음과 같이 Bean을 직접 등록하는 경우에도 @Qualifier를 사용해 추가 구분자를 부여할 수 있다.

@Qualifier 정리

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

 

 

@Primary 사용

Primary를 편해서 많이 사용하는 방법이라고 한다. 하지만, 한계가 있다고 함

 

@Primary는 우선순위를 정하는 방법이다. @Autowired 시에 여러 빈이 매칭되면, @Primary 어노테이션을 붙인 빈이 우선권을 갖는다.

우선권을 부여하고 싶은 빈에 어노테이션을 작성해 주면 된다.

이렇게 우선으로 사용하기를 원하는 빈에 붙여주면, 간단하게 여러 개의 빈 중에서 선택하도록 구현할 수 있다.

 

여기까지 보면, @Qualifier와 @Primary 중 어떤 것을 사용할지 고민되기 마련이다. 왜냐하면 @Qualifier의 경우 주입받을 때 다음과 같이 모든 코드에 @Qualifier를 붙여주어야 하는 단점이 있기 때문

반면, @Primary는 붙일 필요가 없음

 

 

@Primary, @Qualifier의 활용

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

 

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

 

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

 

우선순위

@Primary@Qualifier의 우선순위를 비교하자면, @Primary는 기본값처럼 동작하는 것이고, @Qualifier는 매우 상세하게 동작하는 방식인데, 이러한 경우 어떤 것이 우선권을 가져갈까??

 

스프링은 항상 자동보다 수동이, 넓은 범위보다는 좁은 범위의 선택권이 우선순위가 높다. 따라서 여기서도 @Qualifier의 우선순위가 더 높다고 한다.

 


어노테이션(Annotation) 직접 만들기

앞서 Qualifier를 사용해 추가 구분자를 부여하는 방식이 있었는데, 이렇게 하는 경우 추가 구분자가 문자열에 해당하기 때문에, 컴파일 시 타입 체크가 안 되는 문제가 발생한다.

(@Qualifier("mainDiscountPolicy") 이렇게 됐을 때, "mainDiscountPolicy" 문자열이 추가 구분자가 되기 때문)

 

이럴 때, 다음과 같이 어노테이션을 만들어서 문제를 해결하는 방법도 있다고 한다!

새로 만든 어노테이션이 @Qualifier를 포함하고 있는 구조이다.

이렇게 만든 어노테이션을 추가 구분자를 부여하고 싶은 스프링 빈에 작성해 주면 된다.

어노테이션을 작성해 Qualifier 부여한 것과 같은 효과를 낼 수 있다.

이렇게 설정하고 기존에 @Qualifier를 의존관계를 주입하는 곳에 작성했던 것과 같은 위치에 어노테이션을 작성해 주면 된다.

이렇게 작성해 주면 된다.

이전에도 정리했던 바와 같이 어노테이션에는 상속이라는 개념이 원래 없다. 이건 스프링이 지원해주는 기능임

 

참고로 @Qualifier 뿐만 아니라 다른 어노테이션들도 함께 조합해서 사용할 수 있다. 단적으로 @Autowired 같은 어노테이션들도 재정의가 충분히 가능

 

물론, 스프링이 제공하는 기능을 뚜렷한 목적 없이 무분별하게 재정의 하는 것은 유지보수에 혼란만 가중할 수 있으므로 주의하자!

 


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

의도적으로 정말 해당 타입의 스프링 빈이 전부 필요한 경우가 있을수도 있다. (하나의 타입으로 여러 개의 빈이 조회되는 상황을 말하는 것이다.)

 

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

 

이번에는 테스트 코드를 통해 새로운 DiscountPolicy라는 스프링 빈을 만을어 개념을 이해해 보자.

위와 같이 새로운 DiscountService가 여러 개의 할인 정책에 의존하는 경우

위 코드에서 봐야 할 것은 의존관계 주입의 대상이 기존처럼 단일 객체가 아니라, Map과 List 자료구조로 변경되었다는 점이다. 

이렇게 Map 또는 List로 의존관계 주입의 대상을 설정하는 경우 타입으로 조회한 스프링 빈이 한 개가 아니어도 조회된 모든 빈에 대해서 의존관계 주입이 이루어진다.

이렇게 여러 개의 스프링 빈이 의존관계로 주입된다.
앞서 작성된 코드를 검증하는 테스트코드

코드를 보면 확인할 수 있듯이 이렇게 코드를 구성하면, 클라이언트의 입장에서 할인 전략을 선택하는 프로그램을 쉽게 구현할 수 있다.

 

 

로직 분석

  • DiscountService는 Map으로 모든 DiscountPolicy를 주입받는다. 이때 fixDiscountPolicy, rateDiscountPolicy가 주입된다.
  • discount() method는 discountCode로 "fixDiscountPolicy"가 넘어오면, Map 자료구조에서 fixDiscountPolicy 스프링 빈을 찾아 실행한다. 이는 rateDiscountPolicy에도 똑같이 적용됨

주입 분석

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

참고 - 스프링 컨테이너를 생성하면서 스프링 빈을 등록하는 과정

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

new AnnotationConfigApplicationContext(AutoAppConfig.class, DiscountService.class);

이러면, 두 개의 클래스 정보가 넘어가서 스프링 컨테이너가 생성되며, 스프링 빈으로 등록된다.

 

위 코드를 두 가지로 나누어 이해 가능

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

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

 


자동, 수동(의존관계 주입)의 올바른 실무 운영 기준

편리한 자동 기능을 기본으로 사용하자!

그렇다면, 어떤 경우에 컴포넌트 스캔과 자동 주입을 이용하고, 또 어떤 경우에는 수동으로 빈을 등록하고, 의존관계 또한 수동으로 주입해야 할까?

 

결론부터 이야기하면, 스프링이 나오고 시간이 지날수록 자동 등록을 선호하는 추세라고..

스프링은 @Component 뿐만 아니라 @Controller, @Service, @Repository처럼 계층에 맞춰 일반적인 애플리케이션 로직을 자동으로 스캔할 수 있도록 지원

 

거기에 더해 최근 스프링 부트는 컴포넌트 스캔을 기본으로 지원하도록 설정되어 있고, 스프링 부트의 다양한 스프링 빈들도 조건이 맞으면 자동으로 등록되도록 설계되어 있다.

 

설정 정보를 기반으로 애플리케이션을 구성하는 부분과 실제 동작하는 부분을 명확하게 나누는 것이 이상적이지만, 개발자의 입장에서 수동 등록 과정은 귀찮기 마련이다. (자동으로 충분히 커버되기 때문인 것 같다.)

 

또 관리할 빈이 많아져서 설정 정보가 커지면, 수동으로 설정 정보를 관리하는 것 또한 부담이 된다. 결정적으로는 자동으로 빈 등록을 해도 OCP, DIP 원칙을 지킬 수 있다.

 

 

그러면 대체 수동 빈 등록은 언제 사용하면 좋은 걸까??

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

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

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

 

기술 지원 로직은 업무 로직과 비교해서 수가 매우 적고, 보통 애플리케이션 전반에 걸쳐 광범위한 영향을 미친다. 그리고 기술 지원 로직은 문제 부분이 어딘지 파악하기 매우 어렵고, 심지어 제대로 동작하고 있는지 조차 파악하기 어려운 경우가 많다고 한다.

 

그래서 이런 기술 지원 로직들은 가급적 수동 빈 등록을 사용해 명확하게 드러나도록 관리하는 것이 좋다고 한다.

 

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

 

 

비즈니스 로직 중에서 다형성을 적극 활용할 때

의존관계 자동 주입 - 조회한 빈이 모두 필요할 때, List, Map을 다시 보자.

DiscountService가 의존관계 자동 주입으로 Map에 주입을 받는 상황을 생각해 보자.

 

여기에 어떤 빈들이 주입될지, 각 빈들의 이름은 무엇일지 코드만 보고 한 번에 쉽게 파악할 수 있을까?

내가 개발했으니 크게 관계가 없지만, 만약 이 코드를 다른 개발자가 개발해서 나에게 준 것이라면 어떨까?

 

자동 등록을 사용하고 있기 때문에 파악하려면 여러 코드를 찾아봐야 한다.

 

이런 경우 수동 빈으로 등록하거나 또는 자동으로 하면 특정 패키지에 같이 묶어두는 게 좋다!

핵심은 딱 보고 이해가 되어야 한다!

 

이 부분을 별도의 설정 정보로 만들고 수동으로 등록하면 다음과 같을 것이다.

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

이렇게 하면, 해당 설정 정보만 봐도 한눈에 빈의 이름은 물론이고, 어떤 빈들이 주입될지 파악할 수 있다.

 

그럼에도 이와 같은 상황에서 빈 자동 등록을 사용하고 싶으면 파악하기 좋게 DiscountPolicy의 구현 빈들만 따로 모아서 특정 패키지에 모아두자.

 

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

 

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

 

정리

  1. 편리한 자동 기능을 기본으로 사용하자!
  2. 직접 등록하는 기술 지원 객체는 수동 등록!
  3. 다형성을 적극 활용하는 비즈니스 로직은 수동 등록을 고민해 보자!

 

728x90

댓글