icon
4장 : 클래스와 인터페이스

추상 클래스


우리는 4.3절에서 상속을 통해 클래스 간의 계층 구조를 만들고 코드 재사용성을 높이는 방법을 배웠습니다. 하지만 때로는 부모 클래스가 직접적으로 인스턴스화되는 것(객체로 만들어지는 것)을 원하지 않고, 단지 자식 클래스들이 공유할 공통적인 구조나 동작의 '청사진' 역할만 하도록 만들고 싶을 때가 있습니다. 이러한 경우에 사용하는 것이 바로 추상 클래스(Abstract Classes) 입니다.

추상 클래스는 클래스의 특정 메서드를 추상 메서드(Abstract Method) 로 선언하여, 이 메서드의 구현을 자식 클래스에게 강제하는 특징을 가집니다. 이는 특정 기능을 자식 클래스가 반드시 구현하도록 하여, 설계의 일관성을 유지하고 런타임 오류를 줄이는 데 도움을 줍니다.


추상 클래스 선언과 특징

추상 클래스abstract 키워드를 사용하여 선언합니다. 주요 특징은 다음과 같습니다.

  1. 직접 인스턴스화 불가능: 추상 클래스 자체는 new 키워드를 사용하여 객체로 생성할 수 없습니다. 오직 다른 클래스에게 상속(확장)될 수만 있습니다.
  2. 추상 메서드 포함 가능: abstract 키워드를 붙여 메서드를 선언할 수 있습니다. 추상 메서드는 선언만 하고 구현 내용(코드 블록 { })은 가지지 않습니다.
  3. 일반 속성 및 메서드 포함 가능: 추상 클래스도 일반 클래스처럼 구체적인 속성과 메서드를 가질 수 있습니다.
  4. 자식 클래스의 구현 강제: 추상 클래스를 상속받는 자식 클래스는 부모의 모든 추상 메서드를 반드시 구현해야 합니다. (단, 자식 클래스 또한 추상 클래스라면 구현하지 않아도 됩니다.)
// '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; // 다른 추상 메서드 추가

  // 추상 속성도 가능 (TypeScript 4.0 이상)
  // 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 => {
  console.log(`${s.name}의 넓이: ${s.calculateArea().toFixed(2)}`);
});
// 원의 넓이: 78.54
// 직사각형의 넓이: 50.00

위 예시에서 Shape 클래스는 calculateAreagetPerimeter라는 추상 메서드를 가지고 있으며, 이들의 실제 구현은 CircleRectangle 같은 자식 클래스에게 맡겨집니다. 자식 클래스들은 Shape를 상속받았기 때문에 이 추상 메서드들을 반드시 구현해야 합니다.


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

추상 클래스와 인터페이스는 모두 다른 클래스들이 따라야 할 '규칙'을 정의한다는 점에서 유사합니다. 하지만 둘 사이에는 중요한 차이점이 있습니다.

특징추상 클래스 (abstract class)인터페이스 (interface)
목적- 공통된 구현(코드) 과 추상적인 계약을 함께 제공
- "Is-a" 관계를 표현 (A는 B의 일종이다)
- 순수하게 계약(청사진) 만 정의
- "Can-do" 관계를 표현 (A는 B의 기능을 할 수 있다)
구현 유무- 추상 메서드는 구현부가 없으나, 일반 메서드는 구현 가능
- 속성도 구현 가능 (public, protected, private)
- 모든 멤버는 구현부가 없으며, 클래스에서 구현해야 함
- 속성도 정의만 가능 (타입만)
인스턴스화직접 인스턴스화할 수 없음 (상속을 통해서만 사용)직접 인스턴스화할 수 없음 (클래스에서 구현해야 함)
상속/구현- extends 키워드로 단일 상속만 가능- implements 키워드로 다중 구현 가능
접근 제어자public, protected, private 사용 가능접근 제어자 사용 불가 (모든 멤버는 암묵적으로 public)
상태속성을 가질 수 있으므로 상태를 가질 수 있음속성의 타입만 정의하므로 상태를 직접 가질 수 없음

언제 무엇을 사용해야 할까?

  • 추상 클래스 (abstract class)

    • 여러 클래스가 공통된 기본 구현(코드)을 공유하면서도, 일부 메서드는 각 클래스에 맞게 다르게 구현되어야 할 때
    • 상속 계층을 통해 강한 "Is-a" 관계(예: CircleShape이다)를 명확히 표현하고 싶을 때
    • protected 멤버를 사용하여 자식 클래스에게만 특정 내부 구현을 노출하고 싶을 때
    • 클래스들이 공유하는 상태(속성)가 존재하고, 그 상태를 추상 클래스에서 초기화하거나 관리하고 싶을 때
  • 인터페이스 (interface)

    • 클래스들이 특정 기능을 수행해야 한다는 계약(Contract) 만을 정의하고 싶을 때
    • 다중 구현을 통해 하나의 클래스가 여러 역할(기능 집합)을 수행하도록 하고 싶을 때
    • 서로 다른 상속 계층에 있는 클래스들이 동일한 기능을 수행해야 할 때
    • 단순히 객체의 모양(Shape) 을 정의하고 싶을 때

예를 들어, "모든 직원은 기본 급여를 계산하는 방식은 같지만, 보너스 계산 방식은 직책마다 다르다"와 같은 시나리오에서는 Employee 추상 클래스를 만들고 calculateBonus()를 추상 메서드로 두는 것이 적합합니다. 반면, "모든 탈것은 start()stop() 메서드를 가져야 한다"와 같은 시나리오에서는 Vehicle 인터페이스를 정의하는 것이 적절할 수 있습니다.


추상 클래스는 상속과 인터페이스의 장점을 결합하여, 유연하면서도 강력한 객체 지향 설계를 가능하게 합니다. 클래스 간의 강한 계층 관계를 설정하고, 특정 동작의 구현을 자식 클래스에게 강제함으로써 코드의 일관성과 안정성을 높일 수 있습니다. 인터페이스와의 차이점을 명확히 이해하고, 여러분의 설계에 가장 적합한 도구를 선택하는 것이 중요합니다.