프로그래밍 공부/Spring

[Spring] DI(Dependency Injection, 의존성 주입)이란?

의존이란?

의존 주입이라는 단어를 알아보기 전,  의존이라는 단어의 의미부터 알아보자.

여기서 말하는 의존은 객체 간의 의존을 의미한다. 간단하게 아래의 예시를 보자. 

public class LottoService{
    private LottoTicket winLottoTicket = new WinLottoTicket();

    public int checkNumber(LottoTicket userTicket){
        return winLottoTicket.checkSameNumber(userTicket);
    }
}

위의 코드에서 집중해서 보아야 할 점은, LottoService라는 클래스가 LottoTicket이라는 클래스의 메서드를 사용한다는 점이다. 

 

즉, 간단하게 말해서 객체에서 '의존'한다는 의미는 한 클래스가 다른 클래스의 메서드를 실행하는 것을 의미한다.

A객체가 B 객체에게 의존한다
= A 객체가 B 객체를 사용한다
= A→B

이러한 상황에서 변경에 의한 영향을 생각해보게 되면, B의 변화는 결국 A에게 영향을 주게 된다. 

즉, 의존한다라는 것이 생각보다 많은 책임이 따르고, 변경에 영향을 받게 된다는 것을 알게 된다.

 

의존을 어떻게 처리하는 것이 좋을까?

이렇게 의존에 대해서 정리하고 나면, 한 가지 의문이 들게 된다.

 

그렇다면, 이러한 의존을 어떻게 처리하는 것이 효과적인 방법일까? 

 

사실 위의 예시에도 나와있듯이 가장 쉬운 방법은 객체를 직접 생성해 필드에 할당하는 방법이다. 

하지만, 이는 유지보수 관점에서 문제가 생기게 된다. 이것도 예시로 살펴보자.

public class LottoService{
//    private LottoTicket winLottoTicket = new WinLottoTicket();
    private LottoTicket winLottoTicket = new CachedWinLottoTicket();

    public int checkNumber(LottoTicket userTicket){
        return winLottoTicket.checkSameNumber(userTicket);
    }
}

기존에 만들어뒀던 winLottoTicket이 아닌, CachedWinLottoTicket (캐시는 간단하게 말하자면 자주 사용하는 정보를 모아둔 임시 장소 같은 느낌이다.)으로 바꿨다고 생각해보자.

 

이렇게 수정하였을 시, 지금은 수정해야 할 클래스가 하나이지만, 가령 이와 같은 의존 주입을 해야 되는 클래스가 10개, 100개라면 모두를 찾아서 수정하는 것은 번거로운 일이 될 것이다.

 

그렇기 때문에, 스프링에서는 이렇게 의존하게 되는 객체 간의 관계, 즉 의존성을 맺어주기 위해 의존성 주입 (Dependency Injection)을 사용하게 된다. 참고로 Spring 4부터는 생성자 주입을 강력히 권장하고 있다고 한다.

 

 

 

생성자 주입, DI (Dependency Injection) 란?

Spring에서는 DI 컨테이너를 통해 서로 강하게 결합되어 있는 두 클래스를 분리하고, 두 객체 간의 관계를 결정해 줌으로써 결합도를 낮추고 유연성을 확보하고자 하였다.

 

즉, 의존성 주입으로 외부에서 의존을 주입받게 되면서, 의존을 내부에서 정의하지 않게 되면서 객체간의 의존성은 낮추고, 재사용성은 높이면서 자연스럽게 변화에 유연한 코드를 만들게 되는 것이다. 

 

이러한 방식은 실질적으로는 내부에서 의존을 넣는 것임에도 불구하고 외부에서 의존을 주입받는 방법처럼 보인다는 의미에서 Inversion of Control, 즉 IOC라고 불리는 개념으로 이어진다.

 

DI의 장점

  • 두 객체 간의 관계라는 관심사의 분리
  • 두 객체 간의 결합도를 낮춤
  • 객체의 유연성을 높임
  • 테스트 작성을 용이하게 함
  • 코드의 재사용성 증가



DI (Dependency Injection)의 방법

DI 알아보자고 들어왔는데 참 서론이 길었다.

 

Spring에서 의존성을 주입하는 방법은 크게 생성자, 수정자(Setter), 필드 주입의 3가지가 존재한다. 사실 말 그대로 그 앞에 있는 방식을 사용한 주입이라는 것만 이해하면 각 방식의 특징은 이해할 수 있다. 

공식문서에 있는 예시를 통해서 각각을 알아보자.

 

생성자 방식 (Constructor 주입)

public class SimpleMovieLister {

    // simpleMovieLister가 movieFinder를 의존하는 형태.
    private final MovieFinder movieFinder;

    // 생성자에서 주입 진행됨!
    public SimpleMovieLister(MovieFinder movieFinder) {
        this.movieFinder = movieFinder;
    }
}

생성자 기반의 DI를 진행할 경우, 생성자의 호출 시점에 1회만 호출된다는 것이 보장된다. 그렇기에 주입받은 객체가 변하지 않거나, 객체 주입을 강제할 수 있다.

 

특히 Spring 프레임워크 상에서는 공식문서에도 나와있듯, 생성자 주입을 적극적으로 지원하고 있다. 사실 시간이 별로 없는 사람들은 이 부분만 읽어도 된다

 

수정자 방식 (Setter 주입)

public class SimpleMovieLister {

    // simpleMovieLister가 movieFinder를 의존하는 형태.
    private final MovieFinder movieFinder;

    // 수정자에서 주입 진행됨!
    public void setMovieFinder(MovieFinder movieFinder) {
        this.movieFinder = movieFinder;
    }
}

수정자 주입의 경우 선택적인 의존성을 주입할 경우에 유용하다. 다만, 필수적인 의존이 충족되지 않았을 경우 에러가 발생할 수 있다는 점은 주의해야 한다. 

 

 

필드 주입 (Field 주입)

이 부분은 사실 공식 문서에 예시가 없는 부분이기도 해서, 그냥 비슷하게 예시를 만들어 설명해보려고 한다.

public class SimpleMovieLister {

    // simpleMovieLister가 movieFinder를 의존하는 형태.
    @Autowired
    private final MovieFinder movieFinder;
}

필드 주입은 필드에 바로 의존관계를 주입하는 방법인데, 실제로 Intellij에서 사용하려고 하면 Field Injection is not recommended라는 경고문구가 뜬다.

코드는 위의 예시들보다 훨씬 간결해진 것 같은데, 왜 필드 주입을 쓰지 말라고 할까?

 

이는 테스트 코드의 중요성과 연결된다. 필드 주입이 이루어질 경우, 외부에서 이를 접근할 수 없게 되기 때문이다. 즉, 할 수 있는 방법이 없다. 그렇기에 테스트 코드나 설정을 위해 불가피한 경우에만 사용하도록 권장된다고 한다.

 

 

 

생성자 주입을 사용해야 하는 이유

최근에는 Spring을 포함한 DI 프레임워크의 대부분이 생성자 주입을 권장하고 있는데, 이 이유는 다음의 네 가지로 정리할 수 있다고 한다.

  1. 객체의 불변성 확보
  2. 테스트 코드의 작성
  3. final 키워드 작성 및 Lombok과의 결합
  4. 순환 의존 방지

 

객체의 불변성

의존관계의 변경은 사실 그렇게 많지 않다. 그렇기에 생성자 주입을 통해서 변경의 가능성은 배제한 채, 불변성을 보장할 수 있게 된다.

 

테스트 코드 작성

생성자 주입의 경우에는 컴파일 시점에 객체를 주입받아 테스트 코드를 작성할 수 있으며, 주입하는 객체가 누락된 경우 컴파일 시점에 발견할 수 있다. 더하여, Mock 객체 등을 만들어서 테스트 용으로 넣게 되면 테스트 시의 편리함 역시 가질 수 있게 된다. 

 

final 키워드 작성 및 Lombok과의 결합

생성자 주입을 사용하면 필드 객체에 final 키워드를 붙일 수 있게 되는데, 이는 컴파일 시점에 의존성이 누락되더라도 다시 한번 더 확인해볼 수 있도록 돕는다. 

또한 Constructor 생성 시, Lombok을 사용하여 @RequiredArgsConstructor를 구성하게 된다면 조금 더 편한 코드를 짤 수 있게 도와준다. 

 

순환 의존 방지

생성자 주입에서는 순환 의존성을 가질 경우 BeanCurrentlyInCreationException이 발생해서 문제 상황을 알 수 있게 해 준다. 

 

 

생성자 주입이라는 방식은 결국 의존을 주입하는 과정에서 보다 객체지향적이고 유연하게 사용할 수 있도록 도와주는 방식이라는 점에서 효과적이고, 또 추천되는 방식인 것 같다.

 

 

참고 

초보 웹 개발자를 위한 스프링 5 프로그래밍 입문- Chapter3 스프링 DI

https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-dependencies

https://tecoble.techcourse.co.kr/post/2020-07-18-di-constuctor-injection/

https://mangkyu.tistory.com/125#recentEntries