프레임워크를 사용하거나 공부하다 보면 의존성 주입, 제어의 역전과 같은 말을 많이 접하게 됩니다. 대충 이런 뜻이겠거니 하며 짐작은 가지만 정확히 무엇을 말하는지 모르겠습니다. 이번 글에서는 의존성 주입이 무엇인지 왜 이런 개념들이 등장했고 왜 사용하는지 알아보겠습니다.
의존성
의존성 주입을 알아보기 전에 먼저 의존성에 대해 알아봅시다. 프로그래밍에서 `의존성(Dependency)`는 변경 혹은 에러로부터 영향을 받는다는 의미로 이해할 수 있습니다. 즉, "A가 B에 대한 의존성을 갖는다."라는 말은 A가 B의 변경 혹은 에러로부터 영향을 받는다는 의미로 받아들일 수 있습니다.
예시 1 - 의존성
아주 간단한 예시를 들어보겠습니다. 사람이 음료수를 먹는 예시입니다. 이 글은 아래 예시 코드를 고쳐가며 설명이 이어집니다. 참기 힘든 코드여도 조금만 참고 끝까지 읽어 주시기 바랍니다.
public class Main {
public static void main(String[] args) {
Person mike = new Person("Mike", 1000);
mike.drinkOrangeJuice();
System.out.println(mike.getEnerge());
}
}
class Person {
private String name;
private int energe;
Person(String neme, int energe){
this.name = name;
this.energe = energe;
}
String getName(){ return this.name; }
int getEnerge(){ return this.energe; }
void drinkOrangeJuice(){
Orange freshOrange = new Orange();
freshOrange.peeled();
OrangeJuice orangeJuice = new OrangeJuice(freshOrange);
this.energe += orangeJuice.getCalories();
}
}
위 코드를 보면 사람 인스턴스를 생성하고 오렌지주스를 마십니다. 위 코드에서 의존성이 존재할까요? Orange 클래스의 peeled() 메서드를 static 메서드로 변경한다면 freshOrange.peeled()를 Orange.peel(freshOrange)로 수정해야 합니다. 또 OrangeJuice 클래스의 생성자의 인자로 설탕이 추가된다면 Person 클래스의 drinkOrangeJuice()를 수정해 줘야겠죠. 이렇듯 Orange 클래스와 OrangeJuice 클래스가 변경되면 Person 클래스의 drinkOrangeJuice() 메서드의 구현부를 변경해 줘야 합니다. Person 클래스는 Orange 클래스와 OrangeJuice 클래스에 강한 의존성을 갖고 있습니다.
예시 2 - 관심사 분리
위 코드에는 많은 문제가 있겠지만 가장 큰 문제는 `관심사 분리(Separation of Concerns)`가 잘 되지 않은 것입니다. 사람은 오렌지주스가 어떻게 만들어지는지 알 필요가 없는데도 오렌지주스를 만드는 과정에 영향을 받고 있습니다. 어떻게 하면 사람이 오렌지주스 생성 과정을 몰라도 오렌지주스를 마실 수 있을까요?
public class Main {
public static void main(String[] args) {
Person mike = new Person("Mike", 1000);
// 외부에서 의존성 인스턴스 생성
Orange freshOrange = new Orange();
freshOrange.peeled();
OrangeJuice orangeJuice = new OrangeJuice(freshOrange);
// 외부에서 의존성 인스턴스 주입
mike.drinkOrangeJuice(orangeJuice);
System.out.println(mike.getEnerge());
}
}
class Person {
private String name;
private int energe;
Person(String neme, int energe){
this.name = name;
this.energe = energe;
}
String getName(){ return this.name; }
int getEnerge(){ return this.energe; }
void drinkOrangeJuice(OrangeJuice orangeJuice){
this.energe += orangeJuice.getCalories();
}
}
현실처럼 외부에서 오렌지주스를 생성해서 사람에게 넣어주면 됩니다. 이제 더 이상 Person 클래스는 오렌지주스가 어떻게 만들어지는지 몰라도 됩니다. 사람은 오렌지주스를 마신 후에 대해서만 신경 쓰면 됩니다. 관심사가 분리된 것이지요.
예시 3 - 인터페이스와 다형성
아직도 문제가 있습니다. 위 코드에서는 사람은 오렌지주스만 먹을 수 있습니다. 다양한 음식을 먹을 수 있도로 바꿔줍시다.
public class Main {
public static void main(String[] args) {
Person mike = new Person("Mike", 1000);
try {
// 외부에서 의존성 인스턴스 생성
Orange freshOrange1 = new Orange();
Orange freshOrange2 = new Orange();
freshOrange1.peeled();
freshOrange2.peeled();
OrangeJuice orangeJuice = new OrangeJuice(freshOrange1);
// 외부에서 의존성 인스턴스 주입
mike.eat(orangeJuice);
mike.eat(freshOrange2);
System.out.println(mike.getEnerge());
} catch (Exception e){
System.out.println(e.getMessage());
}
}
}
class Person {
private String name;
private int energe;
Person(String neme, int energe){
this.name = name;
this.energe = energe;
}
String getName(){ return this.name; }
int getEnerge(){ return this.energe; }
void eat(Food food){ this.energe += food.getCalories(); }
}
interface Food { int getCalories(); }
class Orange implements Food {
static enum Status { FREASH, STALE, PEELED }
private Status status = Status.FREASH;
Status getStatus() { return this.status; }
void peeled() { this.status = Status.PEELED; }
public int getCalories() { return status == Status.STALE ? -10 : 47; }
}
class OrangeJuice implements Food {
private int calories;
OrangeJuice(Orange orange) throws Exception {
if(orange.getStatus() != Orange.Status.PEELED)
throw new Exception("Orange is not peeled!");
this.calories = 20 + orange.getCalories();
}
public int getCalories() { return this.calories; }
}
코드가 길지만 Person의 eat() 메서드와 Food 인터페이스, main() 메서드만 보면 됩니다. SOLID 원칙의 `인터페이스 분리 원칙(Interface Segregation Principle)`을 적용해 Person 클래스 내부에서 사용되는 음식들의 공통부분을 추출해 인터페이스를 최소화시켰습니다. 이제 사람은 Food 인터페이스의 구현체라면 모두 먹을 수 있습니다. 또 그게 어떤 음식인지 어떻게 만들어졌는지 알지도, 알필요도 없습니다. 또, 누구에게 어떤 것을 먹일지는 main()에서 런타임에서 결정할 수 있습니다.
의존성 주입
DI가 어떤 느낌인지 알았으니 위키피디아에서 DI를 설명하는 글을 읽어봅시다.
소프트웨어 엔지니어링에서 DI는 객체가 의존하는 다른 객체를 수신하는 프로그래밍 기술이다.
앞서 살펴본 예시에서 Person 클래스의 객체는 Person 클래스가 의존했었던 OrangeJuice 클래스의 객체를 인자로 받고 있습니다.(Person 클래스는 Food 인터페이스의 구현체 객체를 주입받고 있음)
DI는 객체 구성(생성) 및 사용에 대한 관심사를 분리하여 느슨하게 결합된 프로그램을 만드는 것을 목표로 한다. 이 패턴은 주어진 서비스를 사용하려는 개체나 함수가 해당 서비스를 구성하는 방법을 알 필요가 없도록 한다. 대신, 받는 클라이언트(객체)는 인식하지 못하는 외부 코드(인젝터)에 의해 종속성을 제공받는다.
Person 클래스는 더 이상 OrangeJuice가 어떻게 만들어지는지 몰라도 됩니다. OrangeJuice의 생성자에 설탕이 추가되더라도 Person 클래스는 변경할 필요가 없어졌고 느슨하게 결합되어 있습니다. 받는 클라이언트는 Person, 외부코드(인젝터)는 main으로 읽으시면 이해가 되실 겁니다.
DI는 암시적 종속성을 명시적으로 만들고 다음 문제를 해결하는 데 도움이 됩니다.
1. 클래스가 의존하는 객체의 생성으로부터 어떻게 독립적일 수 있는가?
2. 응용 프로그램과 응용 프로그램에서 사용하는 객체는 어떻게 서로 다른 구성을 지원할 수 있는가?
3. 코드를 직접 편집하지 않고 어떻게 코드 조각의 동작을 변경할 수 있는가?
정리
DI란 외부에서 두 객체 간의 관계를 결정해 주는 프로그래밍 기술로, 자바에서는 인터페이스와 다형성을 이용해 클래스 간의 고정된 의존관계가 아닌 런타임에서 결정되는 객체 간의 동적 의존성으로 프로그램의 유연성을 확보하고 결합도를 낮추는 기술입니다.
DI를 적용하면 다음과 같은 이점이 있습니다.
- 클래스 간의 고정된 의존관계가 아닌 런타임에서 객체 간의 관계가 결정되기 때문에 프로그램의 결합도가 낮고 유연성이 높다.
- 관심사가 분리되어 클래스는 독립적으로 설계하고 객체 간의 관계는 컨테이너에서 동적으로 맺는다.
'Computer Science > etc.' 카테고리의 다른 글
[Linux] 커널 패치로 TCP 성능 40% 향상시키기(by Google) (0) | 2024.03.27 |
---|---|
모듈러(Modular)와 C, Java, Python, Javascript의 나머지 연산자 (0) | 2023.09.08 |
[Design Pattern] 싱글톤 패턴(Singleton Pattern) (0) | 2023.08.02 |