상속과 구현
extends로 구현을 재사용하는 상속과 implements로 구조 계약을 지키는 구현을 비교해 클래스 관계를 선택합니다.
객체 지향 프로그래밍의 핵심 목표 중 하나는 코드의 재사용성과 확장성입니다.
타입스크립트 클래스는 이를 위해 상속(Inheritance)과 구현(Implementation)이라는 강력한 메커니즘을 제공합니다.
이 두 개념은 클래스 관계를 명확히 하고, 기존 코드 기반을 재활용해 새로운 기능을 구축하도록 돕습니다.
앞의 접근 제어자를 바탕으로, 공유할 코드와 호출자가 요구할 구조를 나눠 읽습니다. Dog/Cat과 Robot/Vehicle은 서로 다른 예제이므로 각 블록을 독립적으로 실행합니다. extends는 런타임 클래스 관계를 만들지만 implements의 구조 검사는 JavaScript에 남는 객체를 생성하지 않습니다.
TYPE / RUNTIME / CONTRACT
extends는 구현 상속, implements는 구조 검사입니다
Dog는 Animal의 상태·동작을 재사용합니다. Robot은 Runnable과 Walkable을 직접 구현합니다. 두 관계는 한 클래스에서 함께 사용할 수도 있습니다.
EXTENDS
Animal을 상속한 Dog·Cat
Dog는 super(name)으로 이름을 초기화하고 move를 재정의하여 부모 move도 호출합니다. Cat.move는 부모 구현을 사용하며 기본 거리는 0입니다.
IMPLEMENTS
본문의 Robot·Vehicle → Runnable
Robot은 speed·run과 Walkable의 walk를 구현합니다. Vehicle은 Runnable을 구현합니다. implements는 메서드 코드를 주입하거나 매개변수 타입을 자동 생성하지 않습니다.
TYPE VIEW
변수 타입이 보이는 멤버를 정합니다
myRunner: Runnable로 Robot을 참조하면 run은 호출할 수 있지만 walk는 해당 변수 타입에 없습니다. 실제 객체가 Robot인 사실과 변수의 정적 계약은 구분됩니다.
COMPOSITION
독립 기능을 객체로 주입하는 확장
교체 가능한 동작은 별도 객체에 위임할 수 있습니다. 이 절에는 composition 실행 코드가 없고 다음 인터페이스 설계 절에서 로거 주입을 다룹니다.
클래스 부모는 하나이며 여러 구조 역할을 만족할 수 있습니다. private 상태가 자식 인스턴스에 존재해도 자식 소스에서 직접 접근할 수는 없습니다.
상속 (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키워드: 클래스 인스턴스가 하나 또는 여러 인터페이스의 필수 계약과 호환되어야 한다고 명시합니다. 선택적 멤버를 반드시 추가해야 하는 것은 아니며,implements가 메서드 구현이나 멤버 타입을 자동 생성하지는 않습니다.- 계약(Contract): 인터페이스는 이러한 기능을 제공해야 한다는 약속(계약)을 정의하고, 클래스는 그 약속을 이행합니다.
- 다중 구현: 자바스크립트는 다중 상속을 지원하지 않지만, 타입스크립트의 클래스는 여러 인터페이스를 동시에
implements할 수 있습니다. 이는 여러 기능 집합을 한 클래스에 부여할 때 유용합니다. - 타입 확인: 인터페이스는 클래스가 특정 형태를 가지고 있음을 보증하므로, 인터페이스 타입으로 변수를 선언하고 해당 클래스의 인스턴스를 할당할 수 있습니다. (예:
let myRunner: Runnable = robot;)
상속과 구현의 차이점 및 사용 시기
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의 역할을 구분하고, 애플리케이션 구조에 맞는 방식을 선택해야 합니다.
상속과 구현을 함께 사용할 때는 “공유할 구현”과 “보장할 역할”을 따로 표시하면 클래스 계층이 과도하게 깊어지는 것을 막을 수 있습니다.
같은 객체를 다른 계약으로 보기
Animal 배열 반복문의 첫 원소는 실제 Dog입니다. 따라서 animal.move(5)는 Dog.move에서 꼬리 메시지를 출력한 다음 super.move(5)를 호출합니다. Cat은 재정의하지 않아 Animal.move(5)를 바로 실행합니다. 부모 타입으로 보았다는 이유로 실행 객체의 오버라이딩이 사라지지는 않습니다.
Robot도 실제 객체에는 walk가 있지만 myRunner: Runnable이라는 변수 타입은 run·speed만 공개합니다. 인터페이스는 객체를 잘라서 복사하지 않습니다. 정적 타입에 드러나는 멤버와 실행 객체의 멤버를 구별합니다.
혼자 호출 경로와 계약 선택하기
① const pet: Animal = new Dog('초코', '푸들'); pet.move();의 두 출력과 기본 이동거리를 예측하세요. pet.bark()를 바로 호출할 수 있을까요? ② const runner: Runnable = new Robot(7); runner.run();의 출력과 runner.walk(2)의 타입 검사 결과를 적습니다. ③ Runner 역할에 구현을 공유하지 않는 새 클래스를 추가할 때 Animal을 상속해야 할까요?
상속 구현과 타입 관점 해설
① 실제 Dog.move의 기본 인수는 5이므로 개가 꼬리를 흔듭니다.와 초코가 5m 이동했습니다.가 출력됩니다. pet의 정적 타입 Animal에는 bark가 없어 바로 호출하면 타입 오류입니다. ② run은 로봇이 7km/h 속도로 달립니다.를 출력하지만 Runnable에 walk는 없으므로 runner.walk(2)는 타입 오류입니다. ③ speed·run 계약을 만족하도록 구현하면 되며, 이 역할 때문에 Animal의 상태·move 구현을 물려받을 필요는 없습니다. implements가 행동의 실제 품질까지 증명하는 것은 아니므로 출력도 검사합니다.