본문 바로가기

Language/TypeScript

[TypeScript] 추상 클래스 vs 인터페이스 - 직원 조직도로 이해하기

목차


     

    TypeScript에서 추상 클래스(abstract class)인터페이스(interface)는 둘 다 “규약”을 정의하는 도구이다. 하지만 실제 목적과 사용법은 다르다.

     

    이번 글에서는 직원 조직도 비유를 통해 이 차이를 쉽게 정리해보고자 한다.

    추상 클래스 vs 인터페이스 — 핵심 차이

    인터페이스 (interface)

    • 컴파일 후 사라짐: 타입 정보만 제공, JS 출력 없음.
    • 구조적 타이핑: 이름이 아니라 모양(shape) 이 같으면 호환.
    • 다중 구현 가능: class A implements I1, I2
    • 선언 병합 가능: 같은 이름의 인터페이스를 여러 번 정의하면 자동으로 합쳐짐.
    • 상태(필드) 불가: 런타임 상태나 공통 구현은 가질 수 없음.

    추상 클래스 (abstract class)

    • 런타임에 남음: 공통 메서드, 필드, 생성자 등이 JS로 출력됨.
    • 단일 상속만 가능: extends는 하나뿐.
    • 부분 구현 가능: 공통 로직을 구현하면서 일부 메서드는 abstract로 강제.
    • 상태/접근제어자 지원: protected, private 멤버를 정의 가능.

    정리: “공통 구현 + 상태 공유”가 필요하다면 추상 클래스, “모양(계약)”만 필요하다면 인터페이스.

    의사결정 체크리스트

    1. 공통 로직을 실제로 공유하고 싶은가?
      → 추상 클래스
    2. 객체/함수의 모양만 강제하고 싶은가?
      → 인터페이스
    3. 여러 계약을 동시에 강제하고 싶은가?
      → 클래스는 추상 클래스 하나 extends, 여러 인터페이스 implements
    4. 타입을 점진적으로 확장하고 싶은가?
      → 인터페이스 (선언 병합 지원)
    5. 트리 셰이킹/번들 크기 중요?
      → 인터페이스 (런타임에 코드가 남지 않음)

    직원 조직도 예시로 이해해보기

    추상 클래스 = 모든 직원이 공유하는 속성과 기능

    회사를 생각해 봅시다. 직원이라면 누구나 공통적으로 출근과 퇴근을 한다. 또한 직원마다 출근 시간과 퇴근 시간 같은 공통 속성을 갖는다.이런 공통 속성과 기능을 정의하는 데 적합한 것이 바로 추상 클래스다.

    abstract class Employee {
      constructor(
        public name: string,
        public startTime: string,
        public endTime: string
      ) {}
    
      abstract work(): void; // 일하는 방식은 직원마다 다름
    
      clockIn() {
        console.log(`${this.name} 출근: ${this.startTime}`);
      }
    
      clockOut() {
        console.log(`${this.name} 퇴근: ${this.endTime}`);
      }
    }
    
    • Employee 추상 클래스는 모든 직원이 공통으로 가지는 필드와 메서드를 담고 있다.
    • 하지만 work() 같은 메서드는 직원마다 달라질 수 있으니 abstract로 정의해 구현을 강제한다.

    인터페이스 = 역할(포지션)마다 필요한 기능 명세

    이제 직원들은 포지션에 따라 다른 일을 한다.

    • 정규직 직원은 회의 참석이 필요할 수 있고,
    • 계약직 직원은 보고서 제출이 필요할 수 있다.

    이처럼 포지션마다 추가되는 계약(규칙)인터페이스로 표현한다.

    interface RegularWorker {
      attendMeeting(): void;
    }
    
    interface ContractWorker {
      submitReport(): void;
    }
    
    • 인터페이스는 “이 포지션이라면 반드시 이런 기능을 가져야 한다”라는 약속을 정의한다.
    • 런타임에는 존재하지 않고, 타입 검사 용도로만 쓰인다.

    추상 클래스 + 인터페이스 조합

    이제 실제 직원 클래스를 만들어 보자.

    class RegularEmployee extends Employee implements RegularWorker {
      work() {
        console.log(`${this.name}이(가) 팀 프로젝트를 진행합니다.`);
      }
      attendMeeting() {
        console.log(`${this.name}이(가) 회의에 참석합니다.`);
      }
    }
    
    class ContractEmployee extends Employee implements ContractWorker {
      work() {
        console.log(`${this.name}이(가) 맡은 업무를 수행합니다.`);
      }
      submitReport() {
        console.log(`${this.name}이(가) 보고서를 제출합니다.`);
      }
    }
    

     

    사용 예시:

    const kim = new RegularEmployee("김철수", "09:00", "18:00");
    kim.clockIn();      // 공통 기능 (추상 클래스)
    kim.work();         // 직원마다 다름
    kim.attendMeeting(); // 정규직 인터페이스 기능
    kim.clockOut();
    
    const lee = new ContractEmployee("이영희", "10:00", "17:00");
    lee.clockIn();
    lee.work();
    lee.submitReport(); // 계약직 인터페이스 기능
    lee.clockOut();
    

     

    정리

    • 추상 클래스(Employee)
      • 모든 직원이 공유하는 속성(출퇴근 시간)기능(출근/퇴근) 정의
      • 특정 기능(work)은 “각자 다르게” 구현하도록 강제
    • 인터페이스(RegularWorker, ContractWorker)
      • 각 포지션(정규직/계약직)이 반드시 수행해야 하는 추가 기능 계약 정의
      • 런타임에는 사라지고, 타입 검사에만 사용됨

    한마디로 다음과 같이 정리할 수 있다.

    • 추상 클래스 = 직원 전체의 공통 기반
    • 인터페이스 = 포지션별 역할 명세

    경계 사례

    • Java와 달리 TypeScript 인터페이스는 구현을 가질 수 없다. (자바의 default 메서드와 다름)
    • type vs interface: 객체 모양만 정의할 때는 둘 다 가능. 다만 interface는 선언 병합이 가능하고, type은 유니온/조건부 타입 표현이 유리.
    • 구조적 타이핑 주의: 의도치 않게 모양만 같아도 호환되므로, 공개 API에는 꼭 필요한 속성만 노출하는 게 좋다.

    마무리

    • 인터페이스: 타입 계약만, 컴파일 후 사라짐
    • 추상 클래스: 공통 상태/구현 공유, 런타임에도 존재
    • 선택 규칙: “공통 로직 공유 → 추상 클래스, 계약만 정의 → 인터페이스”

    TypeScript에서 추상 클래스와 인터페이스는 대체제가 아니다.
    공통된 속성과 구현을 공유하고 싶다면 추상 클래스, 역할별로 어떤 기능이 필요하다고 약속만 하고 싶다면 인터페이스를 먼저 고려해보자.

     

    공식 문서 참고