안동민 개발노트

본문 시작

추상 클래스

인스턴스화할 수 없는 추상 클래스에 공통 구현과 필수 메서드를 함께 정의하고 interface와의 설계 차이를 비교합니다.

우리는 4장 3절에서 상속으로 클래스 계층을 만들고 코드 재사용성을 높이는 방법을 배웠습니다.

하지만 경우에 따라 부모 클래스를 직접 인스턴스화하길 원하지 않고, 자식 클래스가 따라야 할 공통 구조와 동작의 청사진으로만 두고 싶을 때가 있습니다.

이때 사용하는 것이 추상 클래스(Abstract Classes)입니다.

추상 클래스는 클래스의 특정 메서드를 추상 메서드(Abstract Method)로 선언하고, 해당 구현을 자식 클래스에 강제합니다.

덕분에 특정 기능의 구현 누락을 줄이고, 설계 일관성과 런타임 안정성을 함께 확보할 수 있습니다.


추상 클래스 선언과 특징

추상 클래스는 abstract 키워드를 사용하여 선언합니다.

주요 특징은 다음과 같습니다.

직접 인스턴스화 불가능: 추상 클래스 자체는 new 키워드를 사용하여 객체로 생성할 수 없습니다.

오직 다른 클래스에게 상속(확장)될 수만 있습니다.

추상 메서드 포함 가능: 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 CLASS · UML

추상 클래스의 공통 구현과 계산 계약

Shape는 이름과 displayInfo()를 제공하고 Circle과 Rectangle은 면적과 둘레 계산을 구현합니다.

추상 클래스의 공통 구현과 계산 계약 Shape의 protected name과 public displayInfo(), 추상 calculateArea와 getPerimeter 계약을 Circle과 Rectangle이 구현하는 상속 관계입니다. «abstract»Shape# name · + displayInfo()CONCRETECircle+ 면적 계산 · + 둘레 계산CONCRETERectangle+ 면적 계산 · + 둘레 계산extendsextendsABSTRACT RESTRICTIONS ARE CHECKED BY TYPESCRIPT · RUNTIME INVARIANTS NEED GUARDS
  1. Shape

    protected name 상태와 public displayInfo() 구현을 제공합니다.

  2. 추상 슬롯

    calculateArea()와 getPerimeter() 구현을 구체 하위 클래스에 요구합니다.

  3. Circle

    반지름으로 면적과 둘레를 계산합니다. 이 예제에는 반지름 값 검증이 없습니다.

  4. Rectangle

    너비·높이로 같은 면적·둘레 계약을 구현합니다.

상속 extends실체화 implements합성 owns집합 has연관 association의존 uses

추상 클래스는 emit되는 실제 runtime class이고 interface는 소거되는 구조 계약이므로 공유 구현과 상태가 필요할 때만 상속 축을 사용합니다.


추상 클래스와 인터페이스의 비교

abstract class와 interface 선택

RUNTIME OR ERASED · FLOWCHART

abstract class와 interface 선택

공통 런타임 상태·constructor·기본 구현이 필요하면 abstract class를, 소거되는 구조 역할이면 interface를 선택합니다.

abstract class와 interface 선택 공유 구현과 상태, 알고리즘 골격, 독립 구조 역할, 다중 조합 요구에 따라 abstract class, interface, composition을 고르는 흐름입니다. QUESTION런타임 구현이 필요한가?state · constructor · methodSHARED RUNTIMEabstract class상태 + 기본 구현ERASED ROLEinterface구조 계약만MULTIPLE ROLESinterface 조합여러 역할 만족LOW COUPLINGcomposition독립 동작 위임예아니요다중 역할교체 필요ABSTRACT CLASS USES THE SINGLE INHERITANCE AXIS · INTERFACES DO NOT EMIT
  1. 공유 런타임

    상태와 구현, template method가 필요하면 abstract class를 선택합니다.

  2. 구조 역할

    런타임 artifact가 필요 없으면 interface로 계약만 표현합니다.

  3. 여러 역할

    서로 독립적인 interface를 함께 구현할 수 있습니다.

  4. 독립 교체

    상속 결합이 필요 없으면 composition으로 위임합니다.

abstract 제한 자체는 TypeScript 검사 규칙이며 외부 값의 런타임 불변식은 constructor guard나 factory가 별도로 보장합니다.

추상 클래스와 인터페이스는 모두 다른 클래스들이 따라야 할 규칙을 정의한다는 점에서 유사합니다.

하지만 둘 사이에는 중요한 차이점이 있습니다.

특징추상 클래스 (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 인터페이스를 정의하는 편이 더 적절할 수 있습니다.


추상 클래스는 공통 상태나 기본 구현을 공유하면서 일부 동작을 하위 클래스에 맡길 때 사용합니다.

구조적 계약만 필요하면 인터페이스가 더 단순하고, 상속 계층과 재사용할 구현이 함께 필요하면 추상 클래스가 더 적합합니다.

추상 클래스와 인터페이스를 고를 때는 공통 상태, 기본 구현, 다중 역할, 테스트 대체 가능성을 한 번에 비교하면 설계 방향이 더 선명해집니다.