icon안동민 개발노트

인터페이스를 활용한 설계


 인터페이스는 타입스크립트에서 객체의 형태를 정의하는 강력한 도구입니다.

 이는 코드의 구조를 명확히 하고, 타입 안정성을 제공하며, 다형성을 구현하는 데 중요한 역할을 합니다.

인터페이스의 개념과 목적

 인터페이스는 객체가 따라야 할 구조나 계약을 정의합니다.

 주요 목적은 다음과 같습니다.

  • 타입 검사 제공
  • 코드의 재사용성 향상
  • 구현과 명세의 분리

 클래스와의 주요 차이점

  • 인터페이스는 구현을 포함하지 않음
  • 다중 인터페이스 구현 가능
  • 런타임에 존재하지 않음 (타입 검사용)

인터페이스 정의와 구현

 기본적인 인터페이스 정의와 클래스에서의 구현

interface Vehicle {
    start(): void;
    stop(): void;
}
 
class Car implements Vehicle {
    start() {
        console.log("Car started");
    }
    stop() {
        console.log("Car stopped");
    }
}

인터페이스 상속과 다중 구현

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

interface AudioSystem {
    playAudio(): void;
}
 
interface ElectricVehicle extends Vehicle {
    charge(): void;
}
 
class ElectricCar implements ElectricVehicle, AudioSystem {
    start() { console.log("Electric car started"); }
    stop() { console.log("Electric car stopped"); }
    charge() { console.log("Charging"); }
    playAudio() { console.log("Playing audio"); }
}

 주의사항 : 다중 인터페이스 구현 시 메서드 시그니처 충돌에 주의해야 합니다.

선택적 프로퍼티와 읽기 전용 프로퍼티

 인터페이스에서 선택적 프로퍼티와 읽기 전용 프로퍼티를 정의할 수 있습니다.

interface Config {
    readonly apiKey: string;
    timeout?: number;
}
 
const config: Config = {
    apiKey: "abc123"
};
// config.apiKey = "def456"; // 오류: 읽기 전용 프로퍼티
config.timeout = 3000; // 정상: 선택적 프로퍼티

인터페이스를 타입으로 사용

 인터페이스를 타입으로 사용하여 다형성을 구현할 수 있습니다.

interface Printable {
    print(): void;
}
 
function printDocument(doc: Printable) {
    doc.print();
}
 
class Invoice implements Printable {
    print() { console.log("Printing invoice"); }
}
 
class Email implements Printable {
    print() { console.log("Printing email"); }
}
 
printDocument(new Invoice());
printDocument(new Email());

 이 예제에서 printDocument 함수는 Printable 인터페이스를 구현한 어떤 객체도 받아들일 수 있습니다.

계약 기반 프로그래밍

 인터페이스를 통한 계약 기반 프로그래밍은 코드의 신뢰성과 유지보수성을 향상시킵니다.

interface UserService {
    getUser(id: number): Promise<User>;
    createUser(user: UserInput): Promise<User>;
    updateUser(id: number, user: Partial<UserInput>): Promise<User>;
    deleteUser(id: number): Promise<void>;
}
 
class UserServiceImpl implements UserService {
    // 메서드 구현...
}

 이 접근 방식의 장점

  • 명확한 API 정의
  • 테스트 용이성
  • 모듈 간 의존성 감소

인덱스 시그니처

 동적 프로퍼티를 갖는 객체를 정의할 때 인덱스 시그니처를 사용할 수 있습니다.

interface Dictionary<T> {
    [key: string]: T;
}
 
const numberDict: Dictionary<number> = {
    "one": 1,
    "two": 2
};
 
const stringDict: Dictionary<string> = {
    "name": "John",
    "city": "New York"
};

인터페이스 병합

 타입스크립트는 동일한 이름의 인터페이스를 자동으로 병합합니다.

interface Box {
    height: number;
    width: number;
}
 
interface Box {
    scale: number;
}
 
// 최종 Box 인터페이스
// interface Box {
//     height: number;
//     width: number;
//     scale: number;
// }

 이 기능은 라이브러리의 타입 정의를 확장할 때 유용합니다.

의존성 주입 패턴

 인터페이스를 사용한 의존성 주입 패턴은 코드의 유연성과 테스트 용이성을 높입니다.

interface Logger {
    log(message: string): void;
}
 
class ConsoleLogger implements Logger {
    log(message: string) {
        console.log(message);
    }
}
 
class UserManager {
    constructor(private logger: Logger) {}
 
    createUser(name: string) {
        // 사용자 생성 로직
        this.logger.log(`User created: ${name}`);
    }
}
 
// 사용
const logger = new ConsoleLogger();
const userManager = new UserManager(logger);
userManager.createUser("Alice");

 이 패턴의 장점

  • 의존성 분리
  • 단위 테스트 용이성
  • 유연한 구현 교체

Best Practices

  1. 인터페이스 분리 원칙(ISP) 준수 : 큰 인터페이스보다 작고 특정한 인터페이스 선호
  2. 명확하고 의미 있는 이름 사용
  3. 필요한 경우에만 선택적 프로퍼티 사용
  4. 읽기 전용 프로퍼티를 활용하여 불변성 강화
  5. 인터페이스 상속 시 일관성 유지
  6. 구현 클래스보다 인터페이스를 참조하여 코드 작성
  7. 인덱스 시그니처 사용 시 신중히 고려
  8. 인터페이스 병합 기능 적절히 활용
  9. 제네릭을 활용하여 재사용 가능한 인터페이스 설계
  10. 의존성 주입을 위한 인터페이스 활용
// 제네릭을 활용한 재사용 가능한 인터페이스 예시
interface Repository<T> {
    findAll(): Promise<T[]>;
    findById(id: string): Promise<T | null>;
    create(item: T): Promise<T>;
    update(id: string, item: Partial<T>): Promise<T>;
    delete(id: string): Promise<void>;
}
 
class UserRepository implements Repository<User> {
    // 구현...
}
 
class ProductRepository implements Repository<Product> {
    // 구현...
}

 인터페이스는 타입스크립트에서 강력한 추상화와 타입 안정성을 제공하는 도구입니다.

 적절히 사용하면 코드의 구조를 개선하고, 유지보수성을 높이며, 확장 가능한 설계를 가능하게 합니다.

 인터페이스를 통해 계약을 정의하고, 이를 기반으로 구현을 분리함으로써 모듈화된, 테스트 가능한 코드를 작성할 수 있습니다.

 특히 대규모 애플리케이션에서 인터페이스는 각 컴포넌트 간의 명확한 경계를 제공하고 시스템의 다양한 부분을 독립적으로 개발하고 테스트할 수 있게 해줍니다.

 의존성 주입 패턴과 결합하여 사용하면, 유연하고 확장 가능한 아키텍처를 구축할 수 있습니다.