이 글은 프로그래밍 초심자에게 자신이 알고 싶었던 3가지를 추려서 가능한 단적으로 설명하여 요점을 이해할수 있도록 하기 위해 쓰여졌습니다.
- DI란 무엇인가?
- 왜 필요한가?어떤 잇점이 있나?
- 구체적인 내용은?
DI란 무엇인가?
한마디로 말하자면 "사용하려는 오브젝트를 전달하는 것" 입니다.
이것만으로는 오히려 알아듣기 힘들지도 모르겠지만, 이게 요점입니다.
마구마구 전달합시다.
// 사용하려는 오브젝트
$d = new Di();
// 생성자로 전달했다.
$obj = new Obj($d);
// Setter로 전달했다.
$obj = new Obj();
$obj->setDi($d)
// Property로 전달했다.
$obj = new Obj();
$obj->di = $d
보통은 의존성 주입
이라고 설명되어집니다.
왜 필요한가? 어떤 잇점이 있나?
한마디로 말하자면
"사용하는 쪽과 사용당하는쪽의 관계성을 나누기가 쉬워집니다"
사용하려는 오브젝트를 내부에서 생성하지 않고 외부에서 넘겨 받기 때문에, 의존성이 낮아집니다. (느슨한 커플링 상태)
// DI를 사용하지 않은 Dinner
class Dinner
{
private $menu;
public function __construct()
{
$this->menu = new Pasta();
}
public function eat()
{
echo $this->menu->getName() . '먹었다';
}
}
$dinner = new Dinner();
$dinner->eat(); // 파스타를 먹었다!
$dinner = new Dinner();
$dinner->eat(); // 파스타를 먹었다!
$dinner = new Dinner();
$dinner->eat(); // 파스타를 먹었다!
// 영원히 파스타 밖에 못먹음
// DI를 사용한 Dinner
class Dinner
{
private $menu;
public function __construct($menu)
{
$this->menu = $menu;
}
public function eat()
{
echo $this->menu->getName() . '먹었다!';
}
}
$dinner = new Dinner(new Pasta());
$dinner->eat(); // 파스타를 먹었다.
$dinner = new Dinner(new Soba());
$dinner->eat(); // 소바를 먹었다.
$dinner = new Dinner(new Tonkatsu());
$dinner->eat(); // 돈까쓰를 먹었다.
// 매일 저녁메뉴를 바꿀수 있어서 햄볶
Dinner를 먹을때, 메뉴에 의존성이 낮기 때문에 자유롭게 고를수 있습니다.
구체적으로 말하자면?
위에서 말한 내용만으로는 너무 추상적이어서 구체적으로 말하자면 필요에 의해 오브젝트를 넘겨받아 사용하는 것으로 인해, 사용하는 오브젝트의 자유도가 높아집니다.
자주 사용되는 예는,
- 테스트를 작성하기 쉬워진다
- 환경에 변화에 유연하게 대응할수 있다
위에 적은 DI를 사용하지 않은 Dinner를 예로 들면, Dinner
클래스의 테스트를 하려면 Pasta
클래스를 완성해야만 합니다.
DI를 적용한 Dinner 의 경우에는, Pasta
클래스가 완성되지 않아도 Fake menu
클래스를 만들어서 테스트를 할 수 있습니다.
그때 중요해지는 것이 Pasta
클래스와 FakeMenu
클래스 모두 getName
메소드를 갖고 있을것이 약속되어있어야 한다는 것입니다.
흔히 말하는 추상화입니다.
추상화를 염두해두고 위의 코드를 수정하자면
// DI를 사용한 Dinner
// Menu Interface
interface Menu
{
public function getName(): string;
}
// Menu Interface implemented Class
class Pasta implements Menu
{
public function getName(): string
{
// return 'Pasta'; not Completed
}
}
// Menu Interface implemented Class
class FakeMenu implements Menu
{
public function getName(): string
{
return '초밥';
}
}
class Dinner
{
private $menu;
public function __construct(Menu $menu)
// get the Class implemented Menu Interface
{
$this->menu = $menu;
}
public function eat()
{
echo $this->menu->getName() . '먹었다!';
}
}
$dinner = new Dinner(new FakeMenu());
$this->assertSame('초밥 먹었다!', $dinner->eat()); // Pastaクラス未実装でもDinnerクラスのテストができた
물론 테스트 외에도 여러가지 메리트가 있습니다.
// 추상화
Interface Datasource
{
public function save(Data $data);
}
// 구현 클래스
class MySql implements Datasource
{
public function save(Data $data)
{
// MySql에 저장하는 코드
}
}
// 구현 클래스
class PostgreSql implements Datasource
{
public function save(Data $data)
{
// PostgreSql에 저장하는 코드
}
}
// 사용하는 클래스
class Model
{
private $db;
public function __construct(Datasource $db)
// Datasource인터페이스의 구현클래스를 넘겨받음
{
$this->db = $db;
}
public function save()
{
$this->db->save(new Data(1));
}
}
// 데이터를 넣을 클래스
class Data {
private $id;
public function __construct($id) {
$this->id = $id;
}
}
// MySQL에 저장하자!
$model = new Model(new MySql());
$model->save();
// 오늘부터는 PostgreSQL를 사용할거야
$model = new Model(new PostgreSql());
$model->save();
이런 느낌으로 사용하는 (추상화된) 클래스를 외부에서 넘겨받는 것으로 인해 자유도가 높아집니다.
하지만, 눈치채셨겠지만 이대로는 넘기는 코드가 복잡해서 , 절대로 원래 의미대로 사용하기 쉬운 형태라고 말하기 어렵습니다.
그래서 DI 컨테이너의 형태를 사용하는것으로 의존성 주입부분을 자동화하는 것이 가능합니다.
하지만 그 부분은 또 다음에 하는것으로..