Spring DI Pattern
Spring DI Pattern에 대하여
Dependency Injection에 대해서는 Spring 개발자라면 한번쯤 들어봤을법 하다.
DI는 Spring IoC Container가 관리하는 빈들을 필요한 시점에 의존성 주입 시 사용하는 기술의 이름이다.
DI를 하는 가장 큰 목적은 객체간의 결합도를 낮추기 위함이다.
Spring IoC Container가 @Bean으로 등록된 객체들을 싱글톤으로 관리하고 있다가 런타임시에 의존성을 주입함으로써 객체간 유연한 구조를 만드는 것이 가능하다.
SOLID 원칙의 O에 해당하는 Open Closed Principle을 지키기 위해 디자인 패턴 중 하나인 Strategy Pattern을 사용한다.
전략 패턴은 DI의 3가지 방법 중 생성자 주입을 사용할 시 사용하게 되는데, 인터페이스 객체를 만들고 생성자의 매개변수로 구체적인 구현체를 외부에서 주입하는 방식을 사용한다.
Constructor based Injection (Good!)
public class Person{
PersianCat cat = new PersianCat();
}
위와 같이 직접 객체를 생성할 수 있지만 이 경우 객체간의 결합도가 높아지게 된다.
이러한 객체간의 결합도를 낮추기 위해 우리는 Dependency Injection이라는 기술을 사용하는 것이다.
이러한 DI의 방법에는 3가지 방법이 있는데 그 중 첫번째 방법인 생성자 주입 방식을 보도록 하자
아래의 코드를 보자
public class Person{
ICat cat = null;
public Person(ICat cat){
this.cat = cat;
}
}
Person person = new Person(new PersianCat());
외부에서 의존성 주입을 하고 있고, 생성자를 통해 외부로부터 받아온 의존성을 주입하고 있다.
결론부터 이야기하면 위의 생성자를 통한 주입 방법이 가장 추천하는 방법이다.
Spring 또한 DI는 생성자 주입 방법을 사용할 것을 권장하고 있다.
그렇다면 왜 이 방법을 가장 추천하는 걸까?
기존 Spring 개발자라면 생성자 주입 방식을 가장 권장한다는 말이 조금 낯설게 들릴지도 모르겠다.
필자 또한 Field Injection을 주로 사용해 왔기 때문에 더더욱 궁금했다.
왜 Field Injection 보다 코드량도 많아서 작성하기도 귀찮은 생성자 주입 방법을 권장하는 걸까?
그 이유는 아래의 Setter Injection과 Field Injection이 가진 문제점을 보며 이해해보도록 하자.
Setter Injection (Ugly)
수정자 주입 방법은 아래와 같이 사용한다.
public class Person{
ICat cat;
public setCat(ICat cat){
this.cat = cat;
}
public void touch(){
cat.meow();
}
}
Person person = new Person();
person.setCat(new PersianCat());
person.setCat(new RussianBlueCat());
수정자 주입 방법은 어떤 구현체이든 ICat을 구현하기만 하면 된다는 신박한 방법을 사용한다.
수정자 주입으로 의존관계 주입은 런타임시에 할 수 있기 때문에 낮은 결합도를 갖는다.
그러나 Person 객체가 생성되고 난 이후 ICat의 구현체를 주입하지 않더라도 touch() 메소드를 호출할 수 있게 되고 이는 NullPointException이 발생하는 문제를 초래하게 된다.
주입이 필요한 객체가 주입이 되지 않더라도 생성할 수 있는 문제가 있다.
때문에 이 방법은 좋은 방법이 아니다.
Field Injection (Bad)
@Service
public class CatServiceImpl implements CatService{
@Autowired
private DogService dog;
@Override
public void meow(){
dog.bark();
}
}
그렇다면 Field Injection은 어떨까?
스프링을 써봤다면 Field Injection을 사용해서 주로 DI를 했을 것이다.
앞서 언급한 방법 중 의존성 주입을 하기에 가장 편한 방법이다.
필자 또한 별 의구심없이 Field Injection을 주로 사용했었다.
실제 이전 회사의 서비스 중인 코드도 Field Injection을 사용하고 있었다.
Field Injection은 위에서 언급한 방법 중 가장 나쁜 DI Pattern이다.
왜일까? 이렇게 편리하고 간단하게 작성할 수 있는데도 Field Injection을 사용하면 안된다니…
Field Injection는 치명적인 두가지 단점이 있다.
- 테스트가 어렵다.
- 순환 참조가 발생하기 쉽다.
1번의 경우 아주 간단한 이유인데 단위테스트 시 의존 관계를 가지는 객체를 생성해서 주입할 수 있는 방법이 없다.
Mock 객체를 만들어야 하는데 DI 컨테이너에 의존하는 바람에 독립적으로 인스턴스화가 불가능한 문제가 발생한다.
DI 컨테이너에서 관리되는 클래스는 특정 컨테이너에 의존하지 않고 POJO여야 한다.
DI 컨테이너를 사용하지 않고도 인스턴스화 할 수 있어야 하며, 단위 테스트도 가능해야하고, 다른 DI 프레임 워크로 전환할 수 있어야한다.
그러나 지금 Field Injection의 경우는 어떤가?
DI 컨테이너와 밀접하게 결합되면서 인스턴스화를 할 수 있는 방법이 없다.
즉, 테스트가 어려워졌다
2번의 경우는 조금 더 복잡한 문제이다.
Field Injection을 사용하면 순환참조가 발생하기 쉬운데 순환 참조가 무엇인지는 아래의 코드를 봐보도록 하자.
@Service
public class CatServiceImpl implements CatService{
@Autowired
private DogService dog;
@Override
public void meow(){
dog.bark();
}
}
@Service
public class DogServiceImpl implements DogService{
@Autowired
private CatService cat;
@Override
public void bark(){
cat.meow();
}
}
위의 두 코드를 실행하면 어떤일이 벌어질까?
CatServiceImpl은 DogService의 bark()를 호출하고,
DogServiceImpl은 CatService의 meow()를 호출할 것이다.
서로서로 계속 호출 하다가 결국 StackOverflow를 발생시키고 죽는다.
이러한 순환 참조의 문제는 실제 코드가 호출되기 전까지 컴파일 단계에서 이를 알아차릴 방법이 없다.
Field Injection의 치명적인 문제는 필드 주입이나 수정자 주입의 경우 객체 생성 시점에서 순환참조가 일어나는지 확인할 수 있는 방법이 없다는 점이다.
때문에 Field Injection은 가장 지양해야하는 DI Pattern임에도 불구하고 사용하기가 간단하다는 이유로 많이 사용하고 있다.
다시 보니 선녀 같다!
DI Pattern에 대해서 깊게 고민해보지 못했는데 이번 계기로 왜 생성자 패턴을 써야하는지 이해하게 되었다.
생성자 주입 방식의 장점
테스트 코드 작성 용이
외부에서 인스턴스 주입이 가능하기 때문에 Mock 객체를 만들 수 있다.
또한 DI 컨테이너와 독립성을 갖게 된다.
순환 참조 감지 가능
순환 의존성을 가질 경우 BeanCurrentlyCreationExcepiton
를 발생시켜 미리 알 수 있다.
불변 객체
생성자 주입 방식에서 필드는 final로 선언 가능하다.
하지만 필드 주입 방식에서는 final로 선언할 수 없기 때문에 객체가 변경 가능한 상태가 된다.
NPE 방지
생성자 주입 방식은 의존관계 설정이 되지 않으면 객체생성이 불가능하다.
즉, NPE를 방지할 수 있다.
결론
생성자 주입을 쓰자
Reference
https://dzone.com/articles/spring-di-patterns-the-good-the-bad-and-the-ugly
https://yaboong.github.io/spring/2019/08/29/why-field-injection-is-bad/
https://stackoverflow.com/questions/4176520/what-is-the-difference-between-strategy-pattern-and-dependency-injection
https://n1tjrgns.tistory.com/230