웹 프로그래밍/[ Spring ]

[ Spring ] 09. DI (의존성 주입)

kim.svadoz 2021. 7. 7. 12:34
반응형

DI


의존관계 주입

스프링 컨테이너가 지원하는 핵심 개념중 하나로, 설정파일을 통해 객체간의 의존관계를 설정

이는, 스프링에서만 사용되는 용어가 아니라 객체지향 프로그래밍에서 통용되는 개념이다.

강한 결합

객체 내부에서 다른 객체를 생성하는 것은 강한 결합도를 가지는 구조이다.

A클래스 내부에서 B라는 객체를 직접 생성하고 있다면, B객체를 C객체로 바꾸고 싶은 경우, A클래스도 수정해야 하는 방식이고, 이를 강한결합이라 한다.

느슨한 결합

객체를 주입받는 다는 것은 외부에서 생성된 객체를 인터페이스를 통해 넘겨받는 것이다.

이렇게하면 결합도를 낮출수 있고, 런타임 시에 의존관계가 결정되기 때문에 유연한 구조를 가진다.

SOLID의 원칙 중 O에 해당하는 Open Closed Principle을 지키기 위해서 디자인 패턴 중 전략패턴을 사용하게 되는데, 생성자 주입을 사용하게 되면 전략패턴을 사용하는 것이다.

스프링 프레임워크에서는 필드주입이나, 수정자 주입 방법 보다, 생성자 주입을 더 권장하고 있다. 그 이유를 알아보자.

생성자 주입

Constructor Injection

Spring Framework 4.3 버전 부터는 의존성 주입으로부터 클래스를 완벽하게 분리할 수 있다.

단일 생성자인 경우에는 @Autowired 어노테이션을 붙이지 않아도 되지만 생성자가 2개 이상인 경우에는 생성자에 어노테이션을 붙여주어야 한다.

@Component
public class MyExample {
    // final로 선언할 수 있는 보너스
    private final HelloService helloService;

    // 단일 생성자인 경우 추가적인 어노테이션이 필요 없다.
    public MyExample(HelloService helloService) {
        this.helloService = helloService;
    }
}

필드 주입

Field Injection

사용법이 매우 간단하다. 사용하고자 하는 필드에 @Autowired 어노테이션을 붙여주면 자동으로 의존성이 주입된다.

편리하기 때문에 가장 많이 접할 수 있는 방법이다.

@Component
public class MyExample {
    @Autowired
    private HelloService helloService;
}

수정자 주입

Setter Injection

수정자(Setter)를 이용한 주입 방법도 있다.

꼭 setter 메서드일 필요는 없다. 메서드 이름이 수정자 네이밍 패턴(setXXX)이 아니어도 동일한 기능을 하면된다.

그래도 일관성과 명확환 코드를 만들기 위해 정확한 이름을 사용하는 것을 추천한다.

@Component
public class MyExample {
    private HelloService helloService;

    @Autowired
    public void setHelloService(HelloService helloService) {
        this.helloService = helloService;
    }
}

대부분, 코드에서 @Autowired 어노테이션을 필드에 붙여 사용하는 필드 주입 코드를 많이 봤을 것이다. 이는 사용하기 편리하기 때문일 것인데, 스프링팀에는 생성자 주입 방법을 권장하고 있다. 그 이유는 무엇일까?

왜 생성자 주입을 권장할까

그렇다면 왜 생성자 주입 방법을 더 권장하는 이유는 무엇일까? @Autowired 어노테이션만으로 간단하게 의존성을 주입할 수 있는데 말이다. 필드 주입이나 수정자 주입 방법과 다르게 생성자 주입 방법이 주는 장점에 대해서 살펴보자.

> 순환참조 방지

개발을 하다 보면 여러 컴포넌트 간에 의존성이 생긴다. 그중에서도 A가 B를 참조하고, B가 다시 A를 참조하는 순환 참조도 발생할 수 있는데 아래 코드를 통해 어떤 경우인지 살펴보자.

우선 두 개의 서비스 레이어 컴포넌트를 정의한다. 그리고 서로 참조하게 한다. 조금 더 극단적인 상황(?)을 만들기 위해서 순환 참조하는 구조에 더불어 서로의 메서드를 순환 호출하도록 한다.

그러니까 빈이 생성된 후에 비즈니스 로직으로 인하여 서로의 메서드를 순환 참조하는 형태이다. 실제로는 이러한 형태가 되어서는 안되며, 직접적으로 서로를 계속해서 호출하는 코드는 더더욱 안된다. “순환 참조가 되면 이럴 수도 있구나~”라고 생각하자.

@Service
public class MyPlayService {
    // 순환 참조
    @Autowired
    private MyLifeService myLifeService;

    public void sayMyPlay() {
        myLifeService.sayMyLife();
    }
}
@Service
public class MyLifeService {
    // 순환 참조
    @Autowired
    private MyPlayService myPlayService;

    public void sayMyLife() {
        myPlayService.sayMyPlay();
    }
}

위 코드는 애플리케이션이 아무런 오류나 경고 없이 구동되고, 실제 코드가 호출되기 전까지 문제를 발견할 수 없다.

그렇다면 생성자 주입을 사용한 경우에는 어떻게 될까?

@Service
public class MyPlayService {
    private final MyLifeService myLifeService;

    public MyPlayService(MyLifeService myLifeService) {
        this.myLifeService = myLifeService;
    }

    // 생략
}
@Service
public class MyLifeService {
    private final MyPlayService myPlayService;

    public MyLifeService(MyPlayService myPlayService) {
        this.myPlayService = myPlayService;
    }

    // 생략
}

실행 결과는 BeanCurrentlyInCreationException이 발생하며 애플리케이션이 구동조차 되지 않는다.
따라서, 발생할 수 있는 오류를 사전에 알 수 잇다.

> 테스트에 용이

> 좋은 품질의 코드

> 불변성

정리

정리해보면 아래와 같은 이유로 필드주입이나 수정자 주입보다 생정자 주입의 사용이 권장된다.

  • 순환 참조를 방지할 수 있다.
    • 순환 참조가 발생하는 경우 애플리케이션이 구동되지 않는다.
    • NPE 방지
  • 테스트 코드 작성이 편리하다.
    • 단순 POJO를 이용한 테스트 코드를 만들 수 있다.
  • 나쁜 냄새를 없앤다.
    • 더 품질 좋은 코드를 만들 수 있다.
  • immutable 하다.
    • 의존성 주입이 필요한 필드를 final로 선언가능하다.
    • 실행 중에 객체가 변하는 것을 막을 수 있다.
    • 오류를 사전에 방지할 수 있다.

참조 : https://madplay.github.io/post/why-constructor-injection-is-better-than-field-injection

반응형