상속과 구현
객체 지향 프로그래밍의 핵심 원칙 중 하나는 코드의 재사용성과 확장성입니다. 타입스크립트의 클래스는 이러한 목표를 달성하기 위해 상속(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 ) |
---|---|---|
관계 | "Is-a" 관계 (A는 B의 일종이다) | "Can-do" 관계 (A는 B의 기능을 할 수 있다) |
코드 재사용 | 부모 클래스의 구현(코드)을 물려받아 재사용 | 인터페이스에 정의된 멤버를 클래스에서 "반드시" 구현해야 함 |
클래스 개수 | 단일 상속만 가능 (하나의 부모 클래스만 가질 수 있음) | 다중 구현 가능 (여러 인터페이스를 구현할 수 있음) |
목적 | 공통된 속성/행동을 가진 클래스 간의 계층 구조 형성, 코드 재사용 및 확장 | 특정 동작을 수행해야 함을 계약으로 명시, 다형성 증진, 코드 구조화 |
누가 누구인가? | 자식 클래스는 부모 클래스의 한 종류이다. | 클래스는 인터페이스의 계약을 따른다. |
언제 무엇을 사용해야 할까?
-
상속 (
extends
)- 두 클래스 사이에 명확한 "Is-a" 관계가 성립할 때 (예:
Dog
는Animal
이다). - 공통된 기본 구현을 공유하고, 자식 클래스에서 해당 구현을 확장하거나 오버라이딩해야 할 때.
- 코드 중복을 줄이고 싶을 때.
- 두 클래스 사이에 명확한 "Is-a" 관계가 성립할 때 (예:
-
구현 (
implements
)- 특정 기능을 제공해야 한다는 "계약"을 강제하고 싶을 때.
- 서로 다른 상속 계층에 있는 클래스들이 동일한 기능을 수행해야 할 때 (예:
Robot
과Vehicle
은 둘 다Runnable
일 수 있음). - 클래스의 역할을 명확히 하고, 다형성을 통해 유연한 코드를 작성하고 싶을 때.
- 자바스크립트의 단일 상속 제약으로 인해 다중 상속이 불가능하지만, 여러 인터페이스의 기능을 한 클래스에 부여하고 싶을 때 (다중 구현).
대부분의 경우, 이 두 가지 개념은 상호 보완적으로 사용됩니다. 클래스는 특정 부모 클래스를 상속받으면서 동시에 여러 인터페이스를 구현하여 다양한 역할을 수행할 수 있습니다.
상속과 구현은 객체 지향 설계를 이해하고 타입스크립트를 효과적으로 활용하는 데 필수적인 개념입니다. extends
와 implements
키워드의 의미와 역할을 명확히 구분하고, 여러분의 애플리케이션 구조에 맞는 적절한 방식을 선택하여 견고하고 확장 가능한 코드를 작성하시길 바랍니다.