https://2sa7uk5m8eundh3t.public.blob.vercel-storage.com/images/interface-IamJ9pVS0LYZcssvCMaRmJ69lxNhKS.png

Advanced TypeScript | December 4, 2022

Interface 와 의존성 주입

클래스 간의 결합도가 높아서 생기는 문제는 무엇이며, 어떻게 결합도는 낮고 응집도는 높은 클래스를 설계할 수 있는가?

Interface

TypeScript에서의 인터페이스는 일반적으로 type 검증을 위해 특정 데이터의 타입을 명시할 때 사용됩니다. 또는 인터페이스에서 정의한 프로퍼티와 메서드들을 클래스에서 구현하도록 강제하여 일관성을 유지할 수 있도록 하는 역할을 하기도 합니다.

클래스를 사용하는 환경에서 개발을 한다고 가정해 보죠. 인터페이스는 아주 좋은 역할을 해줍니다. 한번 예제를 통해 인터페이스를 사용하기 전/후의 차이를 봅시다.

예제

예시를 위해 Knife 라는 이름의 클래스를 만들었습니다. damage 프로퍼티가 존재하고, attack 메서드가 존재합니다. attack 메서드에는 Knife 클래스에 한정적인 구현이 존재한다고 가정하겠습니다.

Typescript

1class Knife {
2  damage: number;
3
4  constructor(damage: number) {
5    this.damage = damage;
6  }
7
8  attack(person: Person) {
9    ...
10  }
11}

이제 칼을 휘두를 사람이 필요합니다. Person 이라는 이름의 클래스를 만듭니다. name 프로퍼티가 존재하고, 마찬가지로 attack 메서드가 존재합니다.

Person의 attack 메서드를 호출하면 내부에서 Knife의 attack 메서드를 호출하고, 파라미터로 전달한 사람을 척살합니다.

Typescript

1class Person {
2  name: string;
3
4  constructor(name: string) {
5    this.name = name;
6  }
7
8  attack(person: Person) {
9    const knife = new Knife(20);
10
11    knife.attack(person);
12  }
13}

이제 살인을 할 준비가 되었습니다.

Typescript

1const chulsu = new Person("chulsu");
2const younghui = new Person("younghui");
3
4chulsu.attack(younghui);

철수는 영희를 죽였습니다.

결합도가 높은 클래스의 문제점

그런데 칼 말고 다른 무기를 사용하고 싶으면 어떨까요? 철수는 사실 대검 원툴이고, 영희는 칼 원툴이라 쳐봅시다.

그러나 현재 구현상으로는 Knife 클래스는 Person 클래스에 결합되어 있습니다. 결론적으로 무기를 바꿀 수 없는 것이죠. 귀속템이 돼버렸습니다. 만약 Knife 대신 다른 클래스를 적용하려면 Person 클래스 자체를 뜯어 고쳐야 합니다.

그렇게 되면 Person 클래스의 내부 구현이 변경되고, 철수만 무기를 바꾸려 했으나 칼 원툴인 영희까지 무기가 바뀌게 됩니다.

위와 같이 추가적인 기능을 다른 인스턴스의 영향 없이 유연하게 추가하려면 어떻게 해야 할까요?

Interface를 사용한 추상화

칼을 무기라는 설계도의 구현체로 생각해 보면 어떨까요? 사실상 무기는 공격을 위한 것이므로 damage 프로퍼티와 attack 메서드는 공통적으로 존재할 것입니다.

Typescript

1interface Weapon {
2  damage: number;
3
4  attack(person: Person): void;
5}

무기의 설계도 역할을 해줄 인터페이스를 만들었으니 Knife 클래스에 적용해 봅시다.

implements 예약어를 사용해 Knife 클래스에서 Weapon 인터페이스에 대한 내용을 필연적으로 구현하도록 강제할 수 있습니다.

Typescript

1interface Weapon {
2  damage: number;
3
4  attack(person: Person): void;
5}
6
7class Knife implements Weapon {
8  damage: number;
9
10  constructor(damage: number) {
11    this.damage = damage;
12  }
13
14  attack(person: Person) {
15    ...
16  }
17}
18
19class Sword implements Weapon {
20  damage: number;
21
22  constructor(damage: number) {
23    this.damage = damage;
24  }
25
26  attack(person: Person) {
27    ...
28  }
29}

철수를 위한 대검을 만들려고 Weapon 인터페이스를 구현하는 Sword 클래스도 하나 추가했습니다.

이제 Knife는 곧 Weapon이고, Sword도 곧 Weapon입니다.

🤷🏻‍♂️: 아니 그래서 뭐가 달라진 겁니까?

거 좀 기다려 보십쇼.

의존성 주입 (Dependency Injection)

Person 클래스에서 Weapon 인터페이스를 타입으로 하는 weapon 프로퍼티를 추가합니다. 그리고 Person의 attack 메서드에서는 weapon 프로퍼티의 attack 메서드를 호출하도록 변경해 보겠습니다.

Typescript

1class Person {
2  name: string;
3  weapon: Weapon;
4
5  constructor(name: string, weapon: Weapon) {
6    this.name = name;
7    this.weapon = weapon;
8  }
9
10  attack(person: Person) {
11    this.weapon.attack(person);
12  }
13}

Knife와 Sword 모두 Weapon 인터페이스를 구현하고 있으니, 이는 모두 Weapon이나 마찬가지입니다. 따라서 Person 클래스의 생성자로 Weapon에 대한 인스턴스를 주입해서 사용할 수 있습니다.

철수에게는 Sword 클래스의, 영희에겐 Knife 클래스의 인스턴스를 주입해줍니다. 철수와 영희의 손에 각각 무기를 쥐어줬습니다.

Typescript

1const chulsu = new Person("chulsu", new Sword(50));
2const younghui = new Person("younghui", new Knife(20));
3
4chulsu.attack(younghui);

철수는 영희를 죽였습니다.

이와 같이 클래스 외부에서 생성한 인스턴스를 주입 받는 것을 의존성 주입 (Dependency Injection)이라고 합니다.

특정 클래스가 다른 클래스에 의존되지 않고, 어떤 의존성을 사용할지를 외부에서 제어할 수 있습니다. 그러면 특정 클래스에서 어떤 인스턴스의 기능을 사용할지 지정하지 않고도 주입 받은 클래스의 기능을 사용할 수 있는 것이죠.

결론적으로 다른 종류의 무기가 추가되더라도 Person 클래스에 주입만 해주면 내부 구현을 수정하지 않고도 사용할 수 있게 됩니다. 결합도는 낮아졌고, 응집도는 높아졌습니다.