상속과 구현
객체 지향 프로그래밍의 핵심 목표 중 하나는 코드의 재사용성과 확장성입니다.
타입스크립트 클래스는 이를 위해 상속(Inheritance)과 구현(Implementation)이라는 강력한 메커니즘을 제공합니다. 이 두 개념은 클래스 관계를 명확히 하고, 기존 코드 기반을 재활용해 새로운 기능을 구축하도록 돕습니다.
이 절에서는 타입스크립트에서 상속과 구현이 어떻게 작동하는지, 그리고 언제 어떤 것을 사용해야 하는지 자세히 살펴보겠습니다.
상속 (Inheritance)
상속은 한 클래스(자식 클래스 또는 서브클래스)가 다른 클래스(부모 클래스 또는 슈퍼클래스)의 속성과 메서드를 물려받아 재사용하고 확장하는 개념입니다. 이를 통해 코드 중복을 줄이고, 논리적인 계층 구조를 형성할 수 있습니다. 타입스크립트에서는 extends 키워드를 사용하여 상속을 구현합니다.
// 부모 클래스(Superclass) 정의
class Animal {
name: string;
constructor(name: string) {
this.name = name;
}
// 부모 클래스의 메서드
move(distanceInMeters: number = 0): void {
console.log(`${this.name}가 ${distanceInMeters}m 이동했습니다.`);
}
}
// 자식 클래스(Subclass) 정의: Animal 클래스를 상속받습니다.
class Dog extends Animal {
breed: string; // Dog 클래스만의 새로운 속성
constructor(name: string, breed: string) {
// 자식 클래스의 생성자에서는 반드시 super()를 호출하여
// 부모 클래스의 생성자를 먼저 실행해야 합니다.
super(name);
this.breed = breed;
}
// 부모 클래스의 메서드를 오버라이딩(Overriding)할 수 있습니다.
move(distanceInMeters: number = 5): void {
console.log("개가 꼬리를 흔듭니다.");
// super 키워드를 사용하여 부모 클래스의 메서드를 호출할 수 있습니다.
super.move(distanceInMeters);
}
// Dog 클래스만의 새로운 메서드
bark(): void {
console.log(`${this.name} (${this.breed})가 멍멍 짖습니다!`);
}
}
class Cat extends Animal {
constructor(name: string) {
super(name);
}
// 새로운 메서드
purr(): void {
console.log(`${this.name}가 야옹하고 골골거립니다.`);
}
}
const myDog = new Dog("바둑이", "시바견");
myDog.bark(); // 바둑이 (시바견)가 멍멍 짖습니다!
myDog.move(10); // 개가 꼬리를 흔듭니다., 바둑이가 10m 이동했습니다. (오버라이딩된 메서드 호출)
const myCat = new Cat("나비");
myCat.purr(); // 나비가 야옹하고 골골거립니다.
myCat.move(); // 나비가 0m 이동했습니다. (부모의 메서드 호출)
// 타입 검사: Dog와 Cat은 Animal 타입으로 간주될 수 있습니다. (다형성)
let animalFarm: Animal[] = [myDog, myCat];
animalFarm.forEach(animal => {
animal.move(5); // 모든 Animal 타입은 move 메서드를 가집니다.
});extends키워드: 자식 클래스가 부모 클래스의 모든public및protected멤버(속성과 메서드)를 상속받습니다.private멤버는 상속되지 않습니다.super()호출: 자식 클래스의 생성자에서는this를 사용하기 전에 반드시super()를 호출하여 부모 클래스의 생성자를 실행해야 합니다. 이는 부모 클래스에 정의된 속성들을 초기화하기 위함입니다.- 메서드 오버라이딩 (Overriding): 자식 클래스에서 부모 클래스와 동일한 이름의 메서드를 재정의할 수 있습니다.
super키워드를 사용하여 오버라이딩된 메서드 내에서 부모의 원본 메서드를 호출할 수도 있습니다. - 다형성 (Polymorphism): 상속 계층 구조에 있는 클래스들은 서로 다른 타입을 가질 수 있지만, 공통의 부모 타입으로 취급될 수 있습니다. (예:
Dog인스턴스가Animal타입의 배열에 들어갈 수 있음)
구현 (Implementation)
구현은 클래스가 특정 인터페이스(Interface)에 정의된 모든 속성과 메서드를 반드시 갖추고 있음을 약속하는 개념입니다. 즉, 클래스가 인터페이스의 계약(Contract)을 이행한다고 선언하는 것입니다. 타입스크립트에서는 implements 키워드를 사용하여 구현을 정의합니다.
// 'Runnable' 인터페이스 정의: 'run' 메서드를 가져야 한다고 약속합니다.
interface Runnable {
run(): void;
speed: number;
}
// 'Walkable' 인터페이스 정의: 'walk' 메서드를 가져야 한다고 약속합니다.
interface Walkable {
walk(distance: number): void;
}
// 'Robot' 클래스가 Runnable과 Walkable 인터페이스를 구현합니다.
class Robot implements Runnable, Walkable {
speed: number; // Runnable 인터페이스의 speed 속성 구현
constructor(initialSpeed: number) {
this.speed = initialSpeed;
}
run(): void { // Runnable 인터페이스의 run 메서드 구현
console.log(`로봇이 ${this.speed}km/h 속도로 달립니다.`);
}
walk(distance: number): void { // Walkable 인터페이스의 walk 메서드 구현
console.log(`로봇이 ${distance}m 걸었습니다.`);
}
// Robot 클래스만의 추가 메서드
scan(): void {
console.log("주변을 스캔합니다.");
}
}
// 'Vehicle' 클래스는 Runnable 인터페이스만 구현합니다.
class Vehicle implements Runnable {
speed: number;
constructor(initialSpeed: number) {
this.speed = initialSpeed;
}
run(): void {
console.log(`차량이 ${this.speed}km/h 속도로 주행합니다.`);
}
}
const robot = new Robot(10);
robot.run(); // 로봇이 10km/h 속도로 달립니다.
robot.walk(5); // 로봇이 5m 걸었습니다.
robot.scan(); // 주변을 스캔합니다.
const car = new Vehicle(100);
car.run(); // 차량이 100km/h 속도로 주행합니다.
// 타입 검사: 인터페이스 타입으로 변수를 선언할 수 있습니다.
let myRunner: Runnable = robot; // Robot 인스턴스는 Runnable 인터페이스를 만족합니다.
myRunner.run(); // 로봇이 10km/h 속도로 달립니다.
// myRunner.walk(1); // Error: 'Runnable' 형식에 'walk' 속성이 없습니다. (Runnable에는 walk가 정의되어 있지 않음)
myRunner = car; // Vehicle 인스턴스도 Runnable 인터페이스를 만족합니다.
myRunner.run(); // 차량이 100km/h 속도로 주행합니다.implements키워드: 클래스가 하나 또는 여러 개의 인터페이스에 정의된 모든 멤버를 반드시 구현해야 한다고 명시합니다. 만약 인터페이스의 멤버를 모두 구현하지 않으면 컴파일 오류가 발생합니다.- 계약(Contract): 인터페이스는 이러한 기능을 제공해야 한다는 약속(계약)을 정의하고, 클래스는 그 약속을 이행합니다.
- 다중 구현: 자바스크립트는 다중 상속을 지원하지 않지만, 타입스크립트의 클래스는 여러 인터페이스를 동시에
implements할 수 있습니다. 이는 여러 기능 집합을 한 클래스에 부여할 때 유용합니다. - 타입 확인: 인터페이스는 클래스가 특정 형태를 가지고 있음을 보증하므로, 인터페이스 타입으로 변수를 선언하고 해당 클래스의 인스턴스를 할당할 수 있습니다. (예:
let myRunner: Runnable = robot;)
다음 다이어그램은 한 클래스가 부모 구현은 extends로 재사용하고, 여러 역할은 implements로 보장하는 구조를 정리한 것입니다.
상속과 구현의 차이점 및 사용 시기
상속과 구현에서 타입 경계, 추론 결과, 런타임 영향을 정리한 것입니다.
extends와 implements는 비슷해 보이지만 설계 의도가 다릅니다. 부모 클래스의 구현을 재사용하고 필요한 동작을 오버라이딩해야 한다면 extends를, 클래스가 어떤 역할이나 형태를 반드시 만족해야 한다는 계약을 표현하려면 implements를 선택합니다.
| 특징 | 상속 (extends) | 구현 (implements) |
|---|---|---|
| 관계 | Is-a 관계 (A는 B의 일종이다) | Can-do 관계 (A는 B의 기능을 할 수 있다) |
| 코드 재사용 | 부모 클래스의 구현(코드)을 물려받아 재사용 | 인터페이스에 정의된 멤버를 클래스에서 반드시 구현해야 함 |
| 클래스 개수 | 단일 상속만 가능 (하나의 부모 클래스만 가질 수 있음) | 다중 구현 가능 (여러 인터페이스를 구현할 수 있음) |
| 목적 | 공통된 속성/행동을 가진 클래스 간의 계층 구조 형성, 코드 재사용 및 확장 | 특정 동작을 수행해야 함을 계약으로 명시, 다형성 증진, 코드 구조화 |
| 누가 누구인가? | 자식 클래스는 부모 클래스의 한 종류이다. | 클래스는 인터페이스의 계약을 따른다. |
-
상속 (
extends)- 두 클래스 사이에 명확한 Is-a 관계가 성립할 때 (예:
Dog는Animal이다). - 공통된 기본 구현을 공유하고, 자식 클래스에서 해당 구현을 확장하거나 오버라이딩해야 할 때.
- 코드 중복을 줄이고 싶을 때.
- 두 클래스 사이에 명확한 Is-a 관계가 성립할 때 (예:
-
구현 (
implements)- 특정 기능을 제공해야 한다는 계약을 강제하고 싶을 때.
- 서로 다른 상속 계층에 있는 클래스들이 동일한 기능을 수행해야 할 때 (예:
Robot과Vehicle은 둘 다Runnable일 수 있음). - 클래스의 역할을 명확히 하고, 다형성을 통해 유연한 코드를 작성하고 싶을 때.
- 자바스크립트의 단일 상속 제약으로 인해 다중 상속이 불가능하지만, 여러 인터페이스의 기능을 한 클래스에 부여하고 싶을 때 (다중 구현).
대부분의 경우, 이 두 가지 개념은 상호 보완적으로 사용됩니다. 클래스는 특정 부모 클래스를 상속받으면서 동시에 여러 인터페이스를 구현하여 다양한 역할을 수행할 수 있습니다.
상속과 구현은 객체 지향 설계를 이해하고 타입스크립트를 효과적으로 활용하는 데 필수적인 개념입니다. extends와 implements의 역할을 구분하고, 애플리케이션 구조에 맞는 방식을 선택해야 합니다.
상속과 구현을 함께 사용할 때는 “공유할 구현”과 “보장할 역할”을 따로 표시하면 클래스 계층이 과도하게 깊어지는 것을 막을 수 있습니다.
다음 다이어그램은 상속과 구현을 재사용 방식과 결합도 기준으로 비교한 표입니다.
이 다이어그램은 상속과 구현 학습을 선언 위치, 타입 좁히기, 재사용 경계 순서로 마무리합니다.
아래 다이어그램은 extends 상속과 implements 구현을 역할과 사용 시점으로 구분합니다.