이번 포스팅에서는 인터페이스(interface) 와 추상 클래스(abstract class)의 차이점에 대해 알아보고자 한다. (기본적으로 인터페이스와 추상 클래스가 무엇인지 알고 있다는 전제 하에 차이점에만 초점을 맞춰보았다,,)
< 목차 >
- 인터페이스 vs 추상 클래스
- 왜 추상 클래스가 필요한가?
1. 인터페이스 vs 추상 클래스
인터페이스(interface)와 추상 클래스(abstract class)의 차이점에 대해 크게 다음의 관점으로 살펴보고자 한다.
- 구현 방식
- 상속과 구현
- 접근 제한자
(1) 구현 방식
인터페이스는 객체의 구조를 정의하는 역할을 하며, 이를 구현(implements)한 클래스는 인터페이스에 명시된 모든 속성과 메소드를 가지고 있어야 한다. 다시 말해, 인터페이스는 메소드와 속성의 시그니처만을 정의하며, 구현 세부 사항을 가지지 않는다. 따라서 클래스가 인터페이스를 구현(implements)할 때는 인터페이스에 정의된 모든 메소드와 속성을 구현해야만 한다.
interface IAnimal {
name: string;
makeSound(): void;
}
class Dog implements IAnimal {
name: string;
constructor(name: string) {
this.name = name;
}
makeSound() {
console.log('Bark!');
}
}
let myDog = new Dog('Spot');
myDog.makeSound(); // Output: 'Bark!'
위의 코드에서 IAnimal 인터페이스는 name 속성과 makeSound() 메소드를 정의했다. Dog 클래스는 IAnimal 인터페이스를 구현하므로 반드시 name 속성과 makeSound() 메소드를 제공해야 한다.
반면 추상 클래스는 자체적으로 메소드나 속성을 구현할 수 있으며, 일부는 하위 클래스에서 구현하도록 남겨둘 수도 있다. 추상 클래스 내에서 기본 구현을 제공할 수도 있고, 하위 클래스에서 특정 기능을 필요에 따라 재정의 하도록 할 수도 있다.
abstract class Animal {
name: string;
constructor(name: string) {
this.name = name;
}
abstract makeSound(): void;
move() {
console.log('Moving...');
}
}
class Dog extends Animal {
constructor(name: string) {
super(name);
}
makeSound() {
console.log('Bark!');
}
}
let myDog = new Dog('Spot');
myDog.makeSound(); // Output: 'Bark!'
myDog.move(); // Output: 'Moving...'
이번에는 Animal을 추상 클래스로 만들었다. makeSound()는 추상 메소드로 Animal을 상속 받은 Dog 클래스에서 구현되고 있으며 move() 메소드는 Animal 클래스 안에서 구현된 일반 메소드이다. 추상 클래스는 자체적으로 메소드 구현이 가능하기 때문에 move() 메소드처럼 상속 받는 모든 클래스가 공유하는 메소드를 만들 수 있다. 따라서 추상 클래스의 경우 주로 상속(extends)과 함께 사용한다. 이는 추상 클래스가 일반적으로 공통의 구현을 제공하고 하위 클래스에서 이를 재정의 하거나 추가 구현을 제공할 수 있도록 하기 위함이다.
정리하면, 인터페이스는 클래스가 어떤 메소드와 속성을 가지고 있어야 하는지를 정의하고, 추상 클래스는 클래스가 어떤 메소드와 속성을 가지고 있어야 하는지를 정의하면서 동시에 일부 메소드에 대한 기본 구현을 제공할 수도 있다.
(2) 상속과 구현
TypeScript에서는 기본적으로 클래스의 다중 상속을 지원하지 않는다. 예를 들어 두 개 이상의 클래스가 동일한 상위 클래스를 상속하고 그 클래스들이 모두 하나의 클래스에 의해 상속될 경우, "다이아몬드 문제"가 발생한다.
class Problem {
test(arg: string = 'problem') {
console.log(arg);
}
}
class Test1 extends Problem {
test(arg: string = 'test1') {
super.test(arg);
}
}
class Test2 extends Problem {
test(arg: string = 'test2') {
super.test(arg);
}
}
class Diamond extends Test1, Test2 { // 에러 발생
test() {
super.test();
}
}
const instance = new Diamond();
instance.test();
위와 같이 Diamond 클래스가 Test1과 Test2를 모두 상속 받을 수 있다고 한다면 test() 메소드를 사용할 때 누구를 가리키는지 알 수 없게 된다. 다중 상속이 불러오는 문제를 피하기 위해 TypeScript는 인터페이스를 사용하여 여러가지 타입을 구현(implements) 하는 방식을 채택했다고 볼 수 있다. 따라서 추상 클래스를 사용할 경우 한 개의 추상 클래스만 상속(extends) 할 수 있는 반면 인터페이스의 경우 여러 개의 인터페이스를 구현(implements) 할 수 있다.
(3) 접근 제한자
추상 클래스는 "public" , "protected" , "private" 접근 제한자를 가질 수 있는 반면 인터페이스는 접근 제한자를 가질 수 없다. (하지만 인터페이스에서도 readonly 속성을 사용하여 읽기 전용 속성을 정의할 수는 있다.)
- public : public 키워드를 사용하면 어디에서든 해당 멤버 변수에 접근할 수 있다.
- private : private 키워드를 사용하면 해당 멤버 변수에 대한 접근이 해당 클래스 내로 제한된다.
- protected : protected 키워드는 private과 비슷하지만 파생된 클래스에서는 접근이 가능하다는 차이점이 있다. A 클래스 안에 protected 키워드가 명시된 멤버 변수가 있을 때 A 클래스를 상속 받은 B 클래스에서는 해당 멤버 변수에 접근이 가능하지만 A 클래스의 인스턴스에서는 직접적인 접근이 불가능하다.
2. 왜 추상 클래스가 필요한가?
인터페이스(interface) 와 추상 클래스(abstract class)의 가장 큰 차이점은 기본 구현을 제공하는 능력에 있지 않을까 생각한다. 인터페이스는 클래스에 공통된 메소드와 속성의 시그니처를 정의하는데 사용되지만 구현 세부 사항을 가지지는 않는다. 반면 추상 클래스는 기본적인 구현을 제공할 수 있으며 필요한 경우 하위 클래스에서 이를 재정의 하거나 추가 구현을 제공할 수 있다. 따라서 추상 클래스는 다음과 같은 상황에 유용할 수 있다.
- 공통된 기능을 가진 클래스들의 기본 구현을 제공하고, 상속을 통해 하위 클래스에서 특정 동작을 재정의 하거나 확장할 수 있다.
- 추상 클래스를 사용하면 코드의 중복을 줄이고 상속을 사용하여 계층적인 클래스 구조를 만들 수 있다. 이를 통해 관련된 클래스들을 논리적으로 그룹화하여 코드의 가독성과 유지 보수성을 높일 수 있다.
정리하면, 인터페이스는 여러 개의 인터페이스를 implements 하여 다양한 기능을 조합할 수 있지만 구현을 가지지는 않는다. 반면 추상 클래스는 구체적인 구현 세부 사항을 제공할 수 있고 계층적인 클래스 구조를 만들 수 있지만 단일 상속만 가능하다.
'TypeScript' 카테고리의 다른 글
TypeScript - tsconfig.json (0) | 2022.06.09 |
---|---|
TypeScript 기초 - 개념 정리 (0) | 2022.06.09 |