오늘은 DI(Dependency Injection)에 대해 알아보는 시간을 가지겠습니다. 의존성 주입이라는 단어는 많이 들어봤지만 정확한 개념은 잘 몰라 기본적인 정의부터 시작해 Spring framework 에서는 어떻게 사용되어지는지 알아보겠습니.
DI(의존성 주입)란, 디자인 패턴 중 하나로 객체가 의존하고 있는 다른 객체들에게 받는 패턴을 의미합니다.
dependency injection is a design pattern in which an object receives other objects that it depends on.
이렇게만 보면 정확히 어떤 개념인지 이해가 되지 않습니다. 그렇다면 이 DI라는 패턴이 도대체 왜 만들어졌고 Spring framework에선 어떠한 방식으로 사용되어지고 있는지 확인해 보겠습니다.
지금부터는 의존성 주입이라는 개념이 없었던 시절이라고 생각하고 그 당시의 개발자로 빙의해 이 글을 읽어봤으면 좋을 것 같습니다.
(개발 중…)
Q: 야. Store에서 Pencil말고 Eraser도 팔고 싶은데 어떻게 해야 팔 수 있을까..?
A: 클래스 추가하고 세팅해서 넣던지 해~
Q: ?? 귀찮잖아.. 그걸 어떻게 일일히 넣어… 나중에 10개 팔거면 다 선언하고 넣게..? 그거 어떻게 감당하려고
A: 뭐 어쩌라고 ㅋㅋㅋ 시간없으니까 빨리 개발이나 해
….. (중간 생략)
Q: 아! 그러면 Store를 생성할 때 Pencil을 넣어주면 어떨까? Pencil과 Eraser는 Product라는 인터페이스로 묶고 실행시에 Store에서 값을 주입받도록 만드는거야 어때?
A: 무슨 소리야… 어떻게 넣어줄건데? 그냥 해 시간없어…
Q: 그 방법을 만들어 나가야지 ㅎㅎ 잠시만~ 예제 짜올게
다시 현재로 돌아오겠습니다. 빙의를 푸시기 바랍니다.
DI 패턴은 다음과 같은 문제를 해결하기 위해 만들어졌습니다.
의존성 주입이 없었을 당시, 두 개의 객체가 의존 관계에 있을 경우 강한 결합이 발생해 독립적인 생성에 어려움을 겪었습니다. 일일히 생성해줘야 하며, 만약 하나의 기능을 추가할 경우, 결합이 발생된 여러 객체들의 관계를 고려해 최대한 영향이 가지 않는 방향으로 코드를 리팩토링하고 구현을 해야하기 때문입니다.
이러한 문제를 해결하기 위해 어떻게 할지 고민하였고, 정적으로는 해결이 어려운 상황이기에 동적인 상황에서 방법을 모색하였습니다. 의존성을 필요 상황에서 주입해준다면 지금 발생하는 문제들에 대해 해결할 수 있지 않을까?라는 생각에서부터 시작되었고 DI라는 디자인 패턴이 나오게 되었습니다.
Spring 프레임워크는 IOC(Inversion Of Control), 제어의 역전으로 프레임워크에서 개발자 대신 제어를 진행하는데 이를 이용해 DI 기능을 추가하였습니다. 스프링 프레임워크에 Bean으로 객체를 생성하면, 이후 해당 객체가 의존성으로 필요한 객체들을 프레임워크에서 자동으로 주입해주는 것입니다.
이러한 기능을 통해 개발자들은 보다 더 느슨한 결합을 가진 코드를 구현할 수 있고 추가 기능에 대해 유연하게 대응할 수 있게 되었습니다.(왜 그런지는 맨 아래 예제에서 알려드리겠습니다.)
의존성 주입의 종류에는 다음과 같은 종류가 있습니다.
//변수 선언부에 @Autowired Annotation을 붙인다.
@Component
public class SampleController {
@Autowired
private SampleService sampleService;
}
@Component
public class SampleController {
private SampleService sampleService;
@Autowired
public void setSampleService(SampleService sampleService) {
this.sampleService = sampleService;
}
}
@Component
public class SampleService {
private SampleDAO sampleDAO;
@Autowired
public SampleService(SampleDAO sampleDAO) {
this.sampleDAO = sampleDAO;
}
}
이 글에서는 의존성 주입에는 다음과 같은 종류가 있다는 것만 알고 있으면 될 것 같습니다. 하지만 의존성 주입 또한 중요하기 때문에 다음 장에서 설명하겠습니다. 키워드만 기억해주세용~
앞서 본 시나리오 및 문제가 무슨 이야기인지 이해가 안되고 DI가 뭔지 모르겠으면 다음 설명을 순서대로 읽어보시면 될 것 같습니다. (필자의 필력이 딸려서 그런거니 예시를 보면 이해가 될 것입니다.)
public class Store {
private Pencil pencil;
}
문방구 아주머니는 연필 말고도, 지우개, 펜, 샤프까지 팔려고 한다. 어떻게 구현할 것인가요?
구현시 두 가지 방법이 있을 것입니다.
@Setter
public class Store {
private Pencil pencil;
private Eraser eraser;
private Pen pen;
private Sharp sharp;
}
다음과 같이 코드를 작성할 경우 다음과 같은 문제가 발생합니다.
//Store 객체를 사용하려 할 때
public static void main(){
Store store = new Store(new Pencil(),null, null, null);
//or
Store store = new Store();
store.setPencil(new Pencil());
}
의존하는 객체 수가 증가하고, 객체간 강한 결합이 발생할 경우, 지금과 같은 상황은 쉽겠지만 Store 객체를 의존하는 Market 객체라던지, Pencil 내 새로운 객체가 생김과 같이 구성이 복잡해질 경우, 각각의 객체를 이용함에 있어 점차 어려워질 것이고 이는 곧 에러로 연결될 것입니다.
이러한 문제들을 해결하기 위해 Spring 프레임워크는 2번째 방식인 DI를 지원합니다.
우선 앞서 본 연필,지우개,샤프,펜은 문방구 아주머니가 파는 "제품"입니다.
앞서 객체간 강한 결합이 문제임을 지적했기 때문에 이를 해결하기 위해 객체지향의 특징 중 하나인 다형성을 이용해 Product 인터페이스를 다음 객체들을 포괄하는 의미로 이용하겠습니다.
Public interface Product {
}
Public Pencil implements Product{
}
Public Pen implements Product{
}
Store에서는 포괄적인 개념으로 이용하는 Product 인터페이스를 앞서 본 4개의 객체 대신 변수로 이용해 강한 결합을 방지하고, 의존하는 객체의 수를 다음과 같이 줄일 수 있습니다.
@Setter
public class Store {
private List<Product> productList;
public Store(){
productList = new ArrayList<>();
}
public void add(Product product){
this.productList.add(product);
}
}
//Store 객체를 사용하려 할 때
public static void main(){
Store store = new Store();
store.add(new Pencil());
store.add(new Pen());
}
다음과 같은 로직으로 작성할 경우, 다음과 같은 이점을 얻을 수 있습니다.
소프트웨어 공학적인 면에서 봤을 땐, 객체간 강한 결합, 즉 의존 관계를 최소화시켜 느슨하게 결합된 프로그래밍을 하는 것을 목표로 만들어진 디자인 패턴입니다.
Spring Framework에서는 제어의 역전(IOC)라는 특징을 통해 IOC 컨테이너가 의존관계에 있는 객체를 가지고 있는 빈 객체를 통해 필드 주입, 수정자 주입, 생성자 주입 세 가지 방식을 이용하여 의존성을 주입해줍니다.
이를 통해 우리는 사용할 객체들에 대해 빈으로 생성하고, 의존 관계가 있는 객체들을 호출하기만 하면 스프링 컨테이너에서 동적으로 의존성을 주입해줘 편리하게 이용할 수 있습니다.
이를 통해 결과적으로 객체간 강한 결합을 줄여 보다 더 유연한 프로그래밍을 가능하게 만들어 줍니다.
wiki DI
baeldung DI
geeksofgeeks DI
의존성 주입 정리
Spring Framework 의존성 종류 세가지