추상 클래스
인스턴스화할 수 없는 추상 클래스에 공통 구현과 필수 메서드를 함께 정의하고 interface와의 설계 차이를 비교합니다.
우리는 4장 3절에서 상속으로 클래스 계층을 만들고 코드 재사용성을 높이는 방법을 배웠습니다.
하지만 경우에 따라 부모 클래스를 직접 인스턴스화하길 원하지 않고, 자식 클래스가 따라야 할 공통 구조와 동작의 청사진으로만 두고 싶을 때가 있습니다.
이때 사용하는 것이 추상 클래스(Abstract Classes)입니다.
추상 클래스는 클래스의 특정 메서드를 추상 메서드(Abstract Method)로 선언하고, 해당 구현을 자식 클래스에 강제합니다.
덕분에 구체 클래스의 필수 멤버 구현 누락을 타입 검사에서 줄일 수 있습니다. abstract의 직접 생성 제한도 TypeScript 검사 규칙이며, 반지름 같은 런타임 값 조건이나 계산 결과의 정확성을 자동으로 증명하지는 않습니다.
추상 클래스 선언과 특징
추상 클래스는 abstract 키워드를 사용하여 선언합니다.
주요 특징은 다음과 같습니다.
직접 인스턴스화 불가능: 추상 클래스 자체는 new 키워드를 사용하여 객체로 생성할 수 없습니다.
TypeScript에서는 구체 하위 클래스의 인스턴스를 부모 타입으로 참조하며 사용합니다. 추상 클래스 자체도 JavaScript 출력에 공통 생성자·메서드를 가진 클래스로 남지만 abstract 표기는 제거됩니다.
추상 메서드 포함 가능: abstract 키워드를 붙여 메서드를 선언할 수 있습니다.
추상 메서드는 선언만 하고 구현 내용(코드 블록 { })은 가지지 않습니다.
일반 속성 및 메서드 포함 가능: 추상 클래스도 일반 클래스처럼 구체적인 속성과 메서드를 가질 수 있습니다.
자식 클래스의 구현 강제: 추상 클래스를 상속받는 자식 클래스는 부모의 모든 추상 메서드를 반드시 구현해야 합니다. (단, 자식 클래스 또한 추상 클래스라면 구현하지 않아도 됩니다.)
// 'Shape'라는 추상 클래스를 정의합니다.
abstract class Shape {
// 일반 속성
protected name: string;
// 일반 생성자
constructor(name: string) {
this.name = name;
}
// 일반 메서드
displayInfo(): void {
console.log(`이 도형은 ${this.name}입니다.`);
}
// 추상 메서드: 구현부가 없습니다.
// 이 메서드는 이 클래스를 상속받는 모든 자식 클래스에서 반드시 구현해야 합니다.
abstract calculateArea(): number;
abstract getPerimeter(): number; // 다른 추상 메서드 추가
// 추상 속성도 선언 가능
// abstract color: string;
}
// 오류: 추상 클래스는 직접 인스턴스화할 수 없습니다.
// const myShape = new Shape("도형"); // Error: Cannot create an instance of an abstract class.
// Shape를 상속받는 구체적인 클래스 'Circle'
class Circle extends Shape {
radius: number;
constructor(name: string, radius: number) {
super(name); // 부모 생성자 호출
this.radius = radius;
}
// 추상 메서드 'calculateArea'를 반드시 구현해야 합니다.
calculateArea(): number {
return Math.PI * this.radius * this.radius;
}
// 추상 메서드 'getPerimeter'도 반드시 구현해야 합니다.
getPerimeter(): number {
return 2 * Math.PI * this.radius;
}
}
// Shape를 상속받는 구체적인 클래스 'Rectangle'
class Rectangle extends Shape {
width: number;
height: number;
constructor(name: string, width: number, height: number) {
super(name);
this.width = width;
this.height = height;
}
// 추상 메서드 구현
calculateArea(): number {
return this.width * this.height;
}
// 추상 메서드 구현
getPerimeter(): number {
return 2 * (this.width + this.height);
}
}
const circle = new Circle("원", 5);
circle.displayInfo(); // 이 도형은 원입니다.
console.log(`원의 넓이: ${circle.calculateArea().toFixed(2)}`); // 원의 넓이: 78.54
console.log(`원의 둘레: ${circle.getPerimeter().toFixed(2)}`); // 원의 둘레: 31.42
const rectangle = new Rectangle("직사각형", 10, 5);
rectangle.displayInfo(); // 이 도형은 직사각형입니다.
console.log(`직사각형의 넓이: ${rectangle.calculateArea()}`); // 직사각형의 넓이: 50
console.log(`직사각형의 둘레: ${rectangle.getPerimeter()}`); // 직사각형의 둘레: 30
// 다형성을 이용하여 추상 클래스 타입으로 배열을 만들 수 있습니다.
const shapes: Shape[] = [circle, rectangle];
shapes.forEach(s => {
s.displayInfo(); // protected name은 공개 메서드로 읽습니다.
console.log(`넓이: ${s.calculateArea().toFixed(2)}`);
});
// 이 도형은 원입니다.
// 넓이: 78.54
// 이 도형은 직사각형입니다.
// 넓이: 50.00위 예시에서 Shape 클래스는 calculateArea와 getPerimeter라는 추상 메서드를 가지고 있으며, 이들의 실제 구현은 Circle과 Rectangle 같은 자식 클래스에게 맡겨집니다.
이 예제의 구체 자식 클래스들은 Shape의 추상 메서드들을 모두 구현해야 인스턴스를 만들 수 있습니다. 중간 자식 클래스도 abstract로 선언하면 구현을 다음 구체 클래스에 맡길 수 있습니다.
다음 다이어그램은 Shape가 공통 상태와 표시 메서드를 제공하고, 구체 도형들이 계산 계약을 구현하는 상속 관계입니다. 화살표는 계산의 실행 순서가 아닙니다.
TYPE / RUNTIME / CONTRACT
Shape의 공통 구현을 두 도형이 상속합니다
Shape는 name과 displayInfo를 제공합니다. Circle·Rectangle은 calculateArea와 getPerimeter를 구현합니다. 이 예제에서 displayInfo가 계산 메서드를 호출하지는 않습니다.
COMMON
이름과 표시 동작
Shape의 name은 protected입니다. displayInfo를 공개해 원 또는 직사각형이라는 이름을 출력합니다. name을 하위 클래스에서 사용할 수 있지만 외부에서 직접 읽을 수는 없습니다.
REQUIRED METHODS
두 계산 계약을 구현
구체 Circle과 Rectangle은 모두 calculateArea·getPerimeter를 구현해야 합니다. 부모의 공통 표시 구현은 두 클래스가 그대로 사용합니다.
CIRCLE
반지름 5의 결과
원의 면적은 πr²로 78.54, 둘레는 2πr로 31.42입니다. 본문은 toFixed(2)로 출력하며 반지름 검증 코드는 없습니다.
RECTANGLE
너비 10·높이 5의 결과
면적은 50, 둘레는 30입니다. Shape[] 배열에서는 공통 메서드를 호출하고 실제 객체의 계산 구현이 선택됩니다.
위쪽 화살표는 하위 클래스가 Shape를 상속하는 방향입니다. 이 코드는 공통 구현과 추상 슬롯을 보여 주며, 계산 호출 순서를 고정한 template method 예제는 아닙니다.
추상 클래스와 인터페이스의 비교
TYPE / RUNTIME / CONTRACT
공통 구현을 공유할지, 구조 계약만 정할지 고릅니다
Shape는 name 상태와 displayInfo 구현을 공유하고 계산 메서드는 하위 클래스에 맡깁니다. interface는 구현 코드가 없는 정적 구조 계약입니다.
ABSTRACT CLASS
상태와 기본 구현을 공유
본문 Shape는 protected name을 생성자로 저장하고 displayInfo를 제공합니다. Circle·Rectangle은 계산 메서드를 구현하는 상속 계층입니다.
INTERFACE
필요한 구조를 선언
interface는 객체·클래스 인스턴스가 만족할 형태를 검사합니다. JavaScript 출력에 interface 객체가 생성되지 않습니다. 선택 멤버를 반드시 구현하는 규칙은 아닙니다.
ABSTRACT RULE
TypeScript의 인스턴스화 제한
추상 Shape의 직접 new는 오류입니다. 구체 하위 클래스는 추상 멤버를 구현해야 하며 추상 하위 클래스는 구현을 다시 미룰 수 있습니다. abstract 키워드 자체는 런타임 입력 검증이 아닙니다.
COMPOSITION / ROLES
상속과 독립 역할을 함께 검토
단일 클래스 상속과 여러 interface 역할은 함께 사용할 수 있습니다. 독립 교체할 동작의 위임은 설계 확장이며 이 절에는 그 실행 코드가 없습니다.
추상 클래스도 abstract 메서드 없이 공통 상태와 구현을 제공할 수 있습니다. 공유할 코드와 필요한 구조를 먼저 비교합니다.
추상 클래스와 인터페이스는 모두 다른 클래스들이 따라야 할 규칙을 정의한다는 점에서 유사합니다.
하지만 둘 사이에는 중요한 차이점이 있습니다.
| 특징 | 추상 클래스 (abstract class) | 인터페이스 (interface) |
|---|---|---|
| 목적 | - 공통된 구현(코드)과 추상적인 계약을 함께 제공 - Is-a 관계를 표현 (A는 B의 일종이다) | - 순수하게 계약(청사진)만 정의 - Can-do 관계를 표현 (A는 B의 기능을 할 수 있다) |
| 구현 유무 | - 추상 메서드는 구현부가 없으나, 일반 메서드는 구현 가능 - 속성도 구현 가능 ( public, protected, private) | - 멤버의 타입 계약만 정의하며, 클래스나 객체가 필수 계약과 호환되는 구조를 만족해야 함 - 선택적 멤버를 반드시 추가할 필요는 없음 |
| 인스턴스화 | 직접 인스턴스화할 수 없음 (상속을 통해서만 사용) | 런타임 값이 아님; 객체 리터럴이나 클래스 인스턴스가 구조를 만족 |
| 상속/구현 | - extends 키워드로 단일 상속만 가능 | - implements 키워드로 다중 구현 가능 |
| 접근 제어자 | public, protected, private 사용 가능 | 접근 제어자 사용 불가 (모든 멤버는 암묵적으로 public) |
| 상태 | 속성을 가질 수 있으므로 상태를 가질 수 있음 | 속성의 타입만 정의하므로 상태를 직접 가질 수 없음 |
-
추상 클래스 (
abstract class)- 여러 클래스가 공통된 기본 구현(코드)을 공유하면서도, 일부 메서드는 각 클래스에 맞게 다르게 구현되어야 할 때
- 상속 계층을 통해 강한 Is-a 관계(예:
Circle은Shape이다)를 명확히 표현하고 싶을 때 protected멤버를 사용하여 자식 클래스에게만 특정 내부 구현을 노출하고 싶을 때- 클래스들이 공유하는 상태(속성)가 존재하고, 그 상태를 추상 클래스에서 초기화하거나 관리하고 싶을 때
-
인터페이스 (
interface)- 클래스들이 특정 기능을 수행해야 한다는 계약(Contract)만을 정의하고 싶을 때
- 다중 구현을 통해 하나의 클래스가 여러 역할(기능 집합)을 수행하도록 하고 싶을 때
- 서로 다른 상속 계층에 있는 클래스들이 동일한 기능을 수행해야 할 때
- 단순히 객체의 모양(Shape)을 정의하고 싶을 때
예를 들어 모든 직원의 기본 급여 계산 방식은 같지만 보너스 계산 방식은 직책마다 다르다는 시나리오에서는 Employee 추상 클래스를 만들고 calculateBonus()를 추상 메서드로 두는 것이 적합합니다.
반면 "모든 탈것은 start()와 stop() 메서드를 가져야 한다" 같은 시나리오에서는 Vehicle 인터페이스를 정의하는 편이 더 적절할 수 있습니다.
추상 클래스는 공통 상태나 기본 구현을 공유하면서 일부 동작을 하위 클래스에 맡길 때 사용합니다.
구조적 계약만 필요하면 인터페이스가 더 단순하고, 상속 계층과 재사용할 구현이 함께 필요하면 추상 클래스가 더 적합합니다.
추상 클래스와 인터페이스를 고를 때는 공통 상태, 기본 구현, 다중 역할, 테스트 대체 가능성을 한 번에 비교하면 설계 방향이 더 선명해집니다.
표시와 계산 호출을 구별하기
원의 반지름 5에서 면적은 π×5×5≈78.5398이므로 toFixed(2)가 78.54를 만들고, 둘레는 2×π×5≈31.4159이므로 31.42입니다. 직사각형은 10×5=50, 2×(10+5)=30입니다. Shape[] 반복문은 displayInfo 다음 calculateArea를 호출부에서 실행합니다. 원 displayInfo 자체가 calculateArea를 호출하거나 계산 순서를 강제하는 것은 아닙니다.
혼자 도형과 계약 확장하기
원 예제 정의를 유지하고 new Circle('작은 원', 2)와 new Rectangle('새 직사각형', 3, 4)를 만듭니다. 각 면적·둘레를 소수점 둘째 자리 문자열로 적으세요. 이어 Shape를 상속한 구체 클래스가 calculateArea만 구현하고 getPerimeter를 빠뜨리면 어떻게 될까요? 공통 이름 상태나 displayInfo 구현이 필요 없는 면적 계산 객체에는 반드시 Shape 상속을 써야 하는지도 판단합니다.
산술과 설계 선택 해설
작은 원은 면적 4π와 둘레 4π이므로 둘 다 12.57입니다. 새 직사각형은 면적 12.00, 둘레 14.00입니다. 구체 하위 클래스가 getPerimeter를 빠뜨리면 추상 계약 미구현으로 타입 오류가 납니다. 그 하위 클래스도 abstract로 선언해 구현을 더 미루는 경우는 다릅니다. 면적 메서드의 구조만 필요하면 interface AreaProvider { calculateArea(): number } 같은 계약으로 충분할 수 있습니다. 구현 재사용과 호출에 필요한 역할을 분리해 선택합니다.