
일전에 아는 형이 면접질문으로 해당 질문을 받았다고 얘기했습니다. 그래서 이번 기회에 정리해보려고 합니다.
class 앞에 abstract라고 표기합니다. 또한 추상 메서드를 정의할 때도 abstract라고 표기합니다.abstract 키워드를 통해 추상 메서드가 없는 추상 클래스를 만들 수도 있습니다. (어떤 분이 설명이 하시길 추상 클래스는 "객체를 설계할 때, 추상화 작업, 즉 객체들의 공통 부분을 추출하는 부분에서 다형성을 구현할 수 있는 방법 중 하나"라고 설명하셨습니다.)1abstract class Animal {
2 name: string;
3
4 constructor(name: string) {
5 this.name = name;
6 }
7
8 // 추상 메서드
9 abstract makeSound(): void;
10 // 일반 메서드
11 move(): void {
12 console.log("roaming the earth...");
13 }
14}
abstract 키워드를 사용합니다. 추상 클래스 자체를 인스턴스화 할 수 없는 이유는 추상 메서드가 구현되지 않았기 때문입니다.추상 클래스를 상속받은 클래스는 반드시 추상 클래스의 추상 메서드를 구현해야 합니다. 또한 하위 클래스에서 상위 클래스에 있는 메서드를 재정의할 수 있습니다.
1class Dog extends Animal {
2 // 메서드 재정의
3 move() {
4 console.log("running...");
5 }
6
7 // 추상 메서드 구현
8 makeSound() {
9 console.log("bow wow");
10 }
11}
12
13class Cat extends Animal {}
14// Error: Non-abstract class 'Cat' does not implement inherited abstract member 'makeSound' from class 'Animal'.
동적 타입 언어인 JavaScript에서는 객체의 형태를 정의할 수 없습니다. 이러한 문제를 해결하기 위해 TypeScript에서는 인터페이스를 제공합니다. 인터페이스는 객체의 형태를 정의하는 역할을 합니다.
interface 키워드를 사용하여 정의합니다. 인터페이스는 상호 간에 정의한 약속 혹은 규칙입니다. Typescript에서 인터페이스는 다음과 같은 범주에 대해 약속을 정의할 수 있습니다.현재 글에선 추상 클래스와 인터페이스의 차이에 대해 집중하고 있기 때문에 클래스에 대한 인터페이스만 다루겠습니다.
클래스에 대한 인터페이스는 클래스의 구조를 정의합니다. 클래스의 구조를 정의하기 위해서는 클래스의 멤버 변수와 메서드를 포함해야 합니다. 인터페이스는 클래스의 구조만을 정의하고 구현은 포함하지 않습니다. (즉, 껍데기만 정의합니다.)
1interface Animal {
2 name: string;
3 makeSound(): void;
4 move(): void;
5}
implements 키워드를 사용하여 클래스를 구현할 수 있습니다.1class Dog implements Animal {
2 name: string;
3
4 constructor(name: string) {
5 this.name = name;
6 }
7
8 makeSound() {
9 console.log("bow wow");
10 }
11
12 move() {
13 console.log("running...");
14 }
15}
추상 클래스와 인터페이스의 차이점에 대해 3가지 관점으로 접근해보겠습니다.
extends)와 구현(implements)앞서 설명했듯이 추상 클래스는 추상 메서드를 포함할 수 있습니다. 또한 상위 클래스에서 구현한 메서드를 해당 클래스를 상속받은 모든 클래스가 공유합니다. 이는 추상 클래스가 일반적으로 공통의 구현을 제공하고 하위 클래스에서 구현을 확장하는 데 사용된다는 것을 의미합니다.
1abstract class Animal {
2 name: string;
3
4 constructor(name: string) {
5 this.name = name;
6 }
7
8 abstract makeSound(): void;
9
10 move(): void {
11 console.log("roaming the earth...");
12 }
13}
14
15class Dog extends Animal {
16 constructor(name: string) {
17 super(name);
18 }
19
20 // 추상 메서드 구현
21 makeSound() {
22 console.log("bow wow");
23 }
24
25 // 상위 클래스의 메서드를 재정의
26 move() {
27 // 상위 클래스의 메서드 호출
28 super.move();
29 console.log("running...");
30 }
31}
implements)하면 해당 클래스는 인터페이스에 정의된 모든 메서드를 구현해야 합니다.1interface Animal {
2 name: string;
3 makeSound(): void;
4 move(): void;
5}
6
7class Dog implements Animal {
8 name: string;
9
10 constructor(name: string) {
11 this.name = name;
12 }
13
14 makeSound() {
15 console.log("bow wow");
16 }
17
18 move() {
19 console.log("running...");
20 }
21}
22
23class Cat implements Animal {
24 name: string;
25
26 constructor(name: string) {
27 this.name = name;
28 }
29
30 makeSound() {
31 console.log("meow");
32 }
33}
34// Error: Class 'Cat' incorrectly implements interface 'Animal'.
extends 키워드를 사용하여 상속받을 수 있습니다. 추상 클래스는 단일 상속만 지원합니다.왜 단일 상속만 지원할까요? 만약 두 개이상의 클래스가 동일한 추상 클래스를 상속하고 그 클래스 들이 하나에 클래스에게 상속될 경우 "다이아몬드 문제"가 발생할 수 있습니다.
1class Animal {
2 makeSound() {
3 console.log("roaming the earth...");
4 }
5}
6
7class Dog extends Animal {
8 makeSound() {
9 console.log("bow wow");
10 }
11}
12
13class Cat extends Animal {
14 makeSound() {
15 console.log("meow");
16 }
17}
18
19// Error
20class DogCat extends Dog, Cat {
21 makeSound() {
22 super.makeSound(); // ??
23 }
24}

상속 관계가 다음과 같은 구조이기 때문에 다이아몬드 문제라고 부릅니다
DogCat 클래스가 Dog와 Cat 클래스를 상속받을 수 있다면 super.makeSound() 호출 시 어떤 클래스의 makeSound 메서드를 호출해야 할지 알 수 없습니다.interface를 통해 구현(implements)하는 방식으로 다중 상속을 대체합니다.1interface Animal {
2 makeSound(): void;
3}
4
5interface Movable {
6 move(): void;
7}
8
9class Dog implements Animal, Movable {
10 makeSound() {
11 console.log("bow wow");
12 }
13
14 move() {
15 console.log("running...");
16 }
17}
추상 클래스는 "public", "protected", "private"와 같은 접근 제한자를 사용할 수 있습니다. 반면 인터페이스는 접근 제한자를 사용할 수 없습니다. (다만 "readonly"와 같은 속성 제한자는 사용할 수 있습니다.)
1class Animal {
2 // 어디서든 접근 가능
3 public name: string;
4 // 클래스 내부와 상속받은 클래스에서만 접근 가능
5 protected age: number;
6 // 클래스 내부에서만 접근 가능
7 private weight: number;
8}
앞서 설명한 내용을 정리하면 다음과 같습니다.
Interface: 클래스에서 공통된 메서드와 속성을 정의할 때 사용, implements를 통해 다중 상속을 대체Abstract Class: 클래스에서 공통된 메서드와 속성을 정의할 깨 사용, 구현을 재공하고 하위 클래스에서 구현을 확장할 때 사용, 코드의 중복을 줄이고 상속을 통해 계층 구조를 만들 때 사용implements를 통해 다중 상속을 대체할 수 있고 추상 클래스는 기본적인 구현을 재공할 수 있으며 하위 클래스에서 구현을 확장할 수 있습니다.
어느 것이 더 좋다고 말하기 보단 상황에 맞게 사용하는 것이 중요합니다.