icon안동민 개발노트

추상 클래스


 추상 클래스는 객체 지향 프로그래밍에서 중요한 개념으로, 공통의 특성을 가진 클래스들의 기본 클래스 역할을 합니다.

 타입스크립트에서 추상 클래스는 직접 인스턴스화할 수 없으며, 다른 클래스가 이를 상속받아 구체화하는 용도로 사용됩니다.

추상 클래스의 개념과 목적

 추상 클래스의 주요 목적은 관련된 클래스들의 공통 기능을 정의하고 구조를 제공하는 것입니다.

 이는 일반 클래스와 인터페이스의 중간 형태로, 구체적인 구현과 추상적인 선언을 모두 포함할 수 있습니다.

 일반 클래스와의 차이점

  • 직접 인스턴스화할 수 없음
  • 추상 메서드를 포함할 수 있음

 인터페이스와의 차이점

  • 구체적인 메서드 구현 가능
  • 생성자를 가질 수 있음

추상 클래스 정의와 abstract 키워드

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

abstract class Animal {
    abstract makeSound(): void;
 
    move(): void {
        console.log("Moving...");
    }
}

 위 예제에서 Animal 클래스는 추상 클래스이며, makeSound 메서드는 추상 메서드입니다.

추상 메서드

 추상 메서드는 선언만 있고 구현이 없는 메서드입니다.

 이를 상속받는 클래스에서 반드시 구현해야 합니다.

class Dog extends Animal {
    makeSound(): void {
        console.log("Woof!");
    }
}
 
class Cat extends Animal {
    makeSound(): void {
        console.log("Meow!");
    }
}

구체적 메서드와 추상 메서드의 혼합

 추상 클래스는 구체적인 메서드와 추상 메서드를 함께 포함할 수 있습니다.

 이는 공통 기능을 구현하면서도 특정 기능의 구현을 하위 클래스에 위임할 수 있게 해줍니다.

abstract class Shape {
    abstract calculateArea(): number;
 
    display(): void {
        console.log(`Area: ${this.calculateArea()}`);
    }
}

 이 접근 방식의 장점은 코드 재사용성을 높이고 일관된 인터페이스를 제공하면서도 유연성을 유지할 수 있다는 것입니다.

템플릿 메서드 패턴

 추상 클래스는 템플릿 메서드 패턴을 구현하는 데 이상적입니다.

 이 패턴은 알고리즘의 골격을 정의하고 일부 단계를 하위 클래스에 위임합니다.

abstract class DataProcessor {
    process(): void {
        this.readData();
        this.processData();
        this.writeData();
    }
 
    abstract readData(): void;
    abstract processData(): void;
    abstract writeData(): void;
}
 
class FileDataProcessor extends DataProcessor {
    readData(): void {
        console.log("Reading data from file");
    }
 
    processData(): void {
        console.log("Processing file data");
    }
 
    writeData(): void {
        console.log("Writing processed data to file");
    }
}

 이 예제에서 DataProcessor는 데이터 처리의 일반적인 흐름을 정의하고 구체적인 구현은 FileDataProcessor에 위임합니다.

추상 클래스와 인터페이스의 조합

 추상 클래스와 인터페이스를 함께 사용하면 더 유연한 설계가 가능합니다.

interface Loggable {
    log(message: string): void;
}
 
abstract class AbstractLogger implements Loggable {
    abstract log(message: string): void;
 
    logWithTimestamp(message: string): void {
        const timestamp = new Date().toISOString();
        this.log(`[${timestamp}] ${message}`);
    }
}
 
class ConsoleLogger extends AbstractLogger {
    log(message: string): void {
        console.log(message);
    }
}

 여기서 AbstractLoggerLoggable 인터페이스를 구현하면서 추가적인 기능을 제공합니다.

추상 클래스의 생성자

 추상 클래스도 생성자를 가질 수 있으며, 이를 통해 공통 초기화 로직을 구현할 수 있습니다.

abstract class Vehicle {
    constructor(protected brand: string) {}
 
    abstract start(): void;
 
    getBrand(): string {
        return this.brand;
    }
}
 
class Car extends Vehicle {
    constructor(brand: string) {
        super(brand);
    }
 
    start(): void {
        console.log(`${this.brand} car starting...`);
    }
}

계층 구조 설계와 코드 재사용성

 추상 클래스를 사용한 계층 구조 설계는 코드 재사용성을 크게 향상시킬 수 있습니다.

abstract class Employee {
    constructor(protected name: string, protected id: number) {}
 
    abstract calculateSalary(): number;
 
    getDetails(): string {
        return `ID: ${this.id}, Name: ${this.name}`;
    }
}
 
class FullTimeEmployee extends Employee {
    constructor(name: string, id: number, private annualSalary: number) {
        super(name, id);
    }
 
    calculateSalary(): number {
        return this.annualSalary / 12;
    }
}
 
class PartTimeEmployee extends Employee {
    constructor(name: string, id: number, private hourlyRate: number, private hoursWorked: number) {
        super(name, id);
    }
 
    calculateSalary(): number {
        return this.hourlyRate * this.hoursWorked;
    }
}

 이 구조에서 Employee 추상 클래스는 공통 속성과 메서드를 제공하고, 구체적인 급여 계산 로직은 각 하위 클래스에서 구현합니다.

흔한 실수와 안티 패턴

  1. 과도한 추상화 : 모든 클래스를 추상화하려는 시도는 오히려 복잡성을 증가시킬 수 있습니다.
  2. 깊은 상속 계층 : 너무 깊은 상속 계층은 유지보수를 어렵게 만듭니다.
  3. 추상 클래스의 불필요한 구체적 구현 : 추상 클래스에 너무 많은 구체적인 구현을 포함시키면 유연성이 떨어질 수 있습니다.

설계 원칙과 Best Practices

  1. 단일 책임 원칙(SRP)을 준수하여 추상 클래스를 설계하세요.
  2. 리스코프 치환 원칙(LSP)을 고려하여 상속 관계를 설계하세요.
  3. 추상 클래스와 인터페이스를 적절히 조합하여 사용하세요.
  4. 추상 메서드와 구체적 메서드의 균형을 유지하세요.
  5. 공통 로직은 추상 클래스에, 변화가 잦은 부분은 하위 클래스에 구현하세요.
  6. 추상 클래스의 생성자를 통해 공통 초기화 로직을 구현하세요.
  7. 템플릿 메서드 패턴을 활용하여 알고리즘의 골격을 정의하세요.
  8. 추상 클래스를 통해 코드 중복을 최소화하고 재사용성을 높이세요.
  9. 불필요한 추상화는 피하고, 실제 필요에 의해서만 추상 클래스를 생성하세요.
  10. 추상 클래스의 public API를 신중히 설계하세요. 한 번 공개된 API는 변경이 어렵습니다.

 추상 클래스는 타입스크립트에서 강력한 객체 지향 설계 도구입니다.

 적절히 사용하면 코드의 구조화, 재사용성, 확장성을 크게 향상시킬 수 있습니다.