DI(Dependency Injection, 의존성주입)는 IOC(Inversion Of Control, 제어역전)를 구현한 패턴 중 하나다.
IOC에대해 간략히 설명했던 글
보통 DI를 설명할때 예시로 드는 자바코드를 JS코드로 변환해보았다.
class Player {
Attack(){
const gun = new Gun();
gun.Attack();
}
}
const player = new Player();
player.Attack();
위 코드의 문제점은 무엇인가?
만약 플레이어가 다른 무기로 공격한다면 Attack()
내부 구현을 변경해야한다.
아마 if
문이나 switch-case
등으로 처리 되어야 할 것이다.
또한 gun
밖에 사용할 수 없다.
해당 메서드의 제어를 역전하여 무기의 선택권을 외부에 맡겨보자.
class Player {
Attack(weapon){
weapon.Attack();
}
}
class Gun{
Attack(){
//...공격로직
}
}
class Sword{
Attack(){
//...공격로직
}
}
const player = new Player();
const gun = new Gun();
const sword = new Sword();
player.Attack(gun);
player.Attack(sword);
제어를 역전하여 무기의 선택권을 외부(player.Attack()
메서드의 외부)에 맡겼다. 이를 종속성을 주입하였다고 표현한다.
해당 메서드가 weapon
이라는 파라미터를 무조건 받아오게 바뀌었기 때문이다.
모든 기술과 패턴이 그렇듯 장점만 있는 건 아니다. 현재 player.Attack()
메서드는 weapon
이라는 객체가 Attack()
이라는 메서드를 갖고있다고 가정하고있다.
만약 Attack()
메서드를 지니지 않은 weapon
객체가 들어올 시, 오류가 발생한다.
따라서 해당 의존성을 추상화하여 강제하는 방법을 두면 좋다.
class I_Weapon {
Attack(){
throw new Error()
}
}
class Gun extends I_Weapon {
//Attack()메서드를 override하지 않을시 오류가 발생하게 만들었다.
Attack(){
//...
}
}
js에서는 타입이 존재하지 않아 해당 방법밖에 사용할 수 없다.
만약 ts를 사용한다면 아래와 같이 interface를 정의할 수 있다.
interface I_Weapon {
Attack(): void;
}
class Gun implements I_Weapon{
Attack(){
//...
}
}
다만 ts의 interface는 런타임에 사라지므로 만약 런타임도중 검사가 필요하다면 추상클래스를 활용하는 편이 좋다.
abstract class Weapon {
abstract Attack(): void; // 추상 메서드
}
class Gun extends Weapon{
Attack(){...}
}
그러나 추상메서드 또한 컴파일되면 사라지므로...안전하게 작성하고 싶다면 추상클래스+첫번째 방법을 사용하는 것이 좋아보인다.
코딩을 시작한지 얼마 되지 않았을때, 그러니까 작년 겨울에는 DI라는 용어를 봤을때 무슨소린지 전혀 이해되지 않았다.
그러나 시간이 지나고나니 어쩌면 당연한 패턴이라고 생각된다.
프론트엔드에서 객체지향으로 프로그래밍한 적이 없으니 함수로 예를 들어보겠다.
const Grape = {
name:'포도'
}
const getFruitName = () => {
const { name } = Grape;
console.log(`과일의 이름은 ${name} 입니다`);
}
누가 봐도 억지스러운 코드다. 이를 개선해보자.
const getFruitName = (fruit) => {
const { name } = fruit; //객체
console.log(`과일의 이름은 ${name} 입니다`);
}
객체를 많이 사용해보지 않아서 그런 것일까? 아주 당연하게 과일을 파라미터로 받아와야 할 것 같다.
너무 간단한 예시로 들어서 그런 지도 모른다. 큰 규모의 복잡한 프로젝트를 겪어보기 전까지는...🙄