icon안동민 개발노트

상속과 구현


 타입스크립트에서 상속과 구현은 객체 지향 프로그래밍의 핵심 개념을 구현하는 강력한 메커니즘입니다.

 이를 통해 코드 재사용성을 높이고, 다형성을 구현하며, 유연한 설계를 가능하게 합니다.

클래스 상속

 타입스크립트에서 클래스 상속은 extends 키워드를 사용하여 구현합니다.

 이를 통해 기존 클래스의 특성을 새로운 클래스에서 재사용하고 확장할 수 있습니다.

class Animal {
    name: string;
    constructor(name: string) {
        this.name = name;
    }
    move(distanceInMeters: number = 0) {
        console.log(`${this.name} moved ${distanceInMeters}m.`);
    }
}
 
class Dog extends Animal {
    bark() {
        console.log('Woof! Woof!');
    }
}
 
const dog = new Dog('Rex');
dog.bark();  // 출력: Woof! Woof!
dog.move(10);  // 출력: Rex moved 10m.

 상속의 장점

  • 코드 재사용성 증가
  • 계층적 관계 표현 가능
  • 다형성 구현 용이

 단점

  • 과도한 상속은 복잡성 증가
  • 부모 클래스 변경 시 자식 클래스에 영향

생성자와 super 키워드

 자식 클래스에서 생성자를 정의할 때 반드시 super() 를 호출하여 부모 클래스의 생성자를 실행해야 합니다.

class Animal {
    name: string;
    constructor(name: string) {
        this.name = name;
    }
}
 
class Dog extends Animal {
    breed: string;
    constructor(name: string, breed: string) {
        super(name);  // 부모 클래스 생성자 호출
        this.breed = breed;
    }
}
 
const dog = new Dog('Rex', 'German Shepherd');

 주의 : super() 호출은 this 키워드를 사용하기 전에 이루어져야 합니다.

메서드 오버라이딩

 자식 클래스에서 부모 클래스의 메서드를 재정의할 수 있습니다.

 TypeScript 3.7부터는 @override 데코레이터를 사용하여 명시적으로 오버라이딩을 표시할 수 있습니다.

class Animal {
    move() {
        console.log('Animal moving');
    }
}
 
class Dog extends Animal {
    @override
    move() {
        console.log('Dog running');
    }
}

 @override 데코레이터는 실수로 잘못된 메서드를 오버라이드하는 것을 방지하고 코드의 가독성을 높입니다.

protected 멤버와 상속

 protected 접근 제어자를 사용하면 상속 계층 간에 정보를 공유할 수 있습니다.

class Animal {
    protected species: string;
    constructor(species: string) {
        this.species = species;
    }
}
 
class Dog extends Animal {
    showSpecies() {
        console.log(`I am a ${this.species}`);
    }
}
 
const dog = new Dog('Canine');
dog.showSpecies();  // 출력: I am a Canine
// dog.species;  // 오류: 'species'는 보호된 속성이며 'Animal' 클래스와 그 하위 클래스 내에서만 접근 가능합니다.

인터페이스 구현

 타입스크립트에서는 implements 키워드를 사용하여 인터페이스를 구현합니다.

 이는 다중 상속의 제한을 극복하는 방법이 될 수 있습니다.

interface Flyable {
    fly(): void;
}
 
interface Swimmable {
    swim(): void;
}
 
class Duck implements Flyable, Swimmable {
    fly() {
        console.log('Duck flying');
    }
    swim() {
        console.log('Duck swimming');
    }
}

추상 클래스

 추상 클래스는 abstract 키워드를 사용하여 정의하며 하나 이상의 추상 메서드를 포함할 수 있습니다.

 이는 템플릿 메서드 패턴 구현에 유용합니다.

abstract class Animal {
    abstract makeSound(): void;
    
    move() {
        console.log('Moving...');
    }
}
 
class Dog extends Animal {
    makeSound() {
        console.log('Woof!');
    }
}

클래스 상속과 인터페이스 구현 동시에

 클래스는 다른 클래스를 상속하면서 동시에 여러 인터페이스를 구현할 수 있습니다.

interface Swimmable {
    swim(): void;
}
 
class Animal {
    name: string;
    constructor(name: string) {
        this.name = name;
    }
}
 
class Fish extends Animal implements Swimmable {
    constructor(name: string) {
        super(name);
    }
    swim() {
        console.log(`${this.name} is swimming.`);
    }
}

 이 접근 방식의 장점은 코드 재사용성과 유연성을 동시에 얻을 수 있다는 것이지만 복잡성이 증가할 수 있다는 단점이 있습니다.

다형성과 설계 패턴

 상속과 인터페이스 구현은 다형성을 구현하는 핵심 메커니즘입니다.

 이를 활용한 전략 패턴의 예시를 살펴보겠습니다.

interface SortStrategy {
    sort(data: number[]): number[];
}
 
class BubbleSort implements SortStrategy {
    sort(data: number[]): number[] {
        // 버블 정렬 구현
        return data;
    }
}
 
class QuickSort implements SortStrategy {
    sort(data: number[]): number[] {
        // 퀵 정렬 구현
        return data;
    }
}
 
class Sorter {
    constructor(private strategy: SortStrategy) {}
 
    setStrategy(strategy: SortStrategy) {
        this.strategy = strategy;
    }
 
    sort(data: number[]): number[] {
        return this.strategy.sort(data);
    }
}
 
const sorter = new Sorter(new BubbleSort());
console.log(sorter.sort([3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5]));
 
sorter.setStrategy(new QuickSort());
console.log(sorter.sort([3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5]));

 이 예시에서 Sorter 클래스는 다양한 정렬 전략을 유연하게 사용할 수 있습니다.

설계 원칙과 Best Practices

  1. 인터페이스 분리 원칙(ISP)을 따라 작은 단위의 인터페이스를 설계하세요.
  2. 리스코프 치환 원칙(LSP)을 준수하여 상속 관계를 설계하세요.
  3. 합성을 선호하고 상속은 신중히 사용하세요.
  4. 추상화에 의존하고 구체적인 구현에 의존하지 마세요(DIP).
  5. 단일 책임 원칙(SRP)을 따라 클래스와 인터페이스를 설계하세요.
  6. 상속 계층이 깊어지지 않도록 주의하세요.
  7. 인터페이스를 통해 계약을 정의하고, 클래스로 구현을 제공하세요.
  8. 추상 클래스는 공통 기능을 제공할 때 사용하고, 인터페이스는 기능의 계약을 정의할 때 사용하세요.
  9. 오버라이딩 시 @override 데코레이터를 사용하여 의도를 명확히 하세요.
  10. 상속보다는 합성을 통한 재사용을 고려하세요.

 타입스크립트의 상속과 구현 메커니즘을 효과적으로 활용하면 유지보수가 용이하고 확장성 있는 객체 지향 설계를 할 수 있습니다.

 이러한 기능들을 적절히 조합하여 사용함으로써, 복잡한 시스템을 모듈화하고 유연하게 관리할 수 있습니다.