...
타입스크립트의 객체 지향(OOP)
자바스크립트는 프로토타입 기반 언어라, 객체 지향으로 코드를 구성하려면 많은 애로 사항이 있었다.
그러다 ES6으로도 알려진 ECMAScript 2015를 시작으로 클래스 문법이 추가되면서 JavaScript 프로그래머들은 이런 객체-지향적 클래스-기반의 접근 방식을 사용해서 애플리케이션을 만들 수 있게 되었다.
그리고 자바스크립트의 바톤을 그대로 이은 타입스크립트에서는 더욱 더 객체 지향 문법들을 더 끌어 모아, 보다 자바(JAVA) 스럽게 클래스를 구성 할수 있다.
이번 타입스크립트 객체 지향 강의에서는 자바(JAVA) 혹은 자바스크립트(JavaScript)의 객체 지향 문법을 미리 익혀두는 것을 강력하게 추천한다.
자바스크립트의 클래스 문법은 다음 포스팅을 참고하길 바란다.
클래스 (Class)
타입스크립트의 클래스 문법은 자바스크립트와 크게 다르지 않다.
다만 자바스크립트에는 없고 자바에는 있는 몇몇 객체 지향 문법을 끌어와 쓰기 때문에 객체 지향적 몇몇 제약이 새로 생겼다.
예를 들어 자바스크립트에서는 클래스 인스턴스 변수를 굳이 선언하지 않아도 생성자 메소드(constructor)에서 this 키워드로 자동으로 생성시킬 수 있다.
class Animal {
constructor(name) {
this.name = name;
}
}
하지만, 타입스크립트에서는 생성자 메소드(constructor)와 함께 클래스 바디에 인스턴스 변수를 정의 해주어야 한다.
class Animal {
name: string; // 인스턴스 변수 선언
constructor(name: string) {
this.name = name;
}
}
클래스 상속 (Inheritance)
타입스크립트 인터페이스를 extends 로 상속하였듯이, 클래스도 부모 / 자식 관계를 맺으면서 상속이 가능하다.
상속 개념은 클래스-기반 프로그래밍의 가장 기본적인 패턴 중 하나로, 이미 존재하는 클래스를 확장하는 개념이다.
class Animal {
name: string;
constructor(name: string) {
this.name = name;
}
}
// Animal 클래스를 상속함으로서 부모 클래스의 인스턴스 name 을 사용할수 있게 된다.
class Cat extends Animal {
getName(): string {
return `Cat name is ${this.name}.`; // Cat 클래스에는 name 이라는 인스턴스가 없기 때문에, 부모 Animal 클래스에서 가져온다
}
}
let cat: Cat = new Cat('Lucy');
console.log(cat.getName()); // Cat name is Lucy.
위의 코드는 상속 기능을 보여주는 가장 기본적인 예제 이다.
여기서 Cat 클래스는 extends 키워드를 사용하여 Animal이라는 부모 클래스를 상속 받았다.
이때 상속을 해주는 상위 클래스를 수퍼 클래스(Super Class)라고 하고, 상속을 받는 하위 클래스를 서브 클래스(Sub Class)라고 한다.
클래스를 상속하게 되면, 부모 클래스로부터 프로퍼티와 메서드를 상속받게 되어 부모 클래스의 자원들을 마음 껏 사용할 수 있게 된다.
단, 유의할점은 자식이 상속할 수 있는 클래스의 갯수는 무조건 1개인 점이다. 여러개의 클래스를 상속할수는 없다.
반대로 부모 클래스는 여러 개의 자식 클래스를 가질 수 있다.
클래스 상속 특징
- 부모 클래스(parent class) 와 자식 클래스(children class)는 자바 지정예약어 extends에 의하여 정해짐
- 하나의 부모 클래스(parent class)는 여러개의 자식 클래스(children)을 가질 수 있다.
- 반대로 하나의 클래스는 여러개의 클래스로부터 상속을 받을수는 없다.
- 부모 클래스(parent class)로부터 상속받은 자식 클래스는 부모 클래스의 자원(source) 모두를 사용 할 수 있다.
- 반대로 부모클래스는 자식클래스의 자원을 가져다 쓸 수는 없다. (is a 관계)
- 자식클래스는 또다른 클래스의 부모 클래스가 될 수 있다.
- 자식클래스는 부모클래스로부터 물려받은 자원을 override 하여 수정해서 사용 할 수 있다.
- 부모의 부모클래스가 상속받은 자원도 자식클래스가 사용 가능
클래스 IS-A 관계
클래스에 타입이라는 것이 추가되면서 자바스크립트 클래스 개념에 없던 객체 지향 개념이 하나 추가됬는데 바로 is a 관계이다. 이부분은 기본 자바(JAVA) 지식이 없다면 굉장히 난해한 코스니 정신을 가담 듬길 바란다.
class Animal {
name: string;
constructor(name: string) {
this.name = name;
}
move(distanceInMeters: number = 0) {
console.log(`${this.name} moved ${distanceInMeters}m.`);
}
}
class Snake extends Animal {
leg: number = 0;
// @Override Animal
constructor(name: string) {
super(name);
}
// @Override Animal
move(distanceInMeters = 5) { // 오버라이드 되서 굳이 또 매개변수 타입을 선언 할 필요 없다.
super.move(distanceInMeters);
}
poison() {
console.log('shoot poison !!');
}
}
class Horse extends Animal {
leg: number = 4;
// @Override Animal
constructor(name: string) {
super(name);
}
run() {
console.log('start Run !!');
}
}
이번에도 extends 키워드를 사용하여 Animal의 부모클래스를 자식 클래스 둘(Horse와 Snake)가 상속했다.
이전 예제와 한 가지 다른 부분은 상속된 클래스의 생성자 함수는 부모 클래스의 생성자를 실행할 super() 를 호출해야 한다는 점이다. (super 키워드가 없으면 에러 난다)
이 부분은 TypeScript 뿐만 아니라 JavaScript에서도 통용되는 객체 지향의 중요한 규칙이다.
또한 자식 클래스는 부모클래스에 정의되어있는 move() 라는 메소드를 오버라이드(Override) 하였는데, 타입스크립트의 오버로딩(Oveloading)이 비슷한 형태의 메서드를 유연하게 여러개 생성하는 것이라면, 오버라이드(Override)는 그냥 완전히 덮어씌워 버려 기존 함수를 개조하는 개념으로 이해하면 된다.
이제 클래스를 구현했으니 호출해서 객체를 사용해보자.
let sam: Snake = new Snake('Sammy the Python'); // 자식 클래스 생성자로 객체 생성
sam.leg; // 자식 클래스의 인스턴스 변수 : 0
sam.move(); // 오버라이드 한 부모클래스의 메소드 : 'Sammy the Python moved 5m'
sam.poison(); // 자식 클래스의 메소드 : 'shoot poison !!'
let tom2: Horse = new Horse('Tommy the Palomino');
tom2.leg; // 자식 클래스의 인스턴스 변수 : 4
tom2.move(34); // 오버라이드 한 부모클래스의 메소드 : 'Tommy the Palomino moved 34m'
tom2.run(); // 자식 클래스의 메소드 : 'start Run !!'
자바스크립트 클래스를 다룬 독자 분들이라면 이 부분은 크게 어렵지는 않을 것이다.
이번에는 tom 변수의 타입을 Horse 가 아닌 Animal로 지정했다. 그리고 자식 클래스인 Horse 생성자를 받았다.
그 뒤 tom 객체 변수의 프로퍼티에 접근하였는데 다음 코드 동작이 어떻게 될지 추론해보자.
let tom: Animal = new Horse('Tommy the Palomino');
tom.leg;
tom.move(34);
tom.run();
이것이 자바스크립트 클래스와의 차이점이다.
이러한 현상이 생기는 이유는, 부모클래스로 타입 형을 선언하고 자식클래스를 생성해서 할당 하였기 때문이다.
부모 클래스의 인스턴스들을 살펴보면 name 과 move 2개밖에 없다. 이 클래스를 타입으로 하고 name, leg, move, run 4개의 인스턴스가 있는 Horse를 생성자로 받고, 사용하려고 하니까 이러한 에러가 생기는 것이다.
위의 클래스를 고대로 타입스크립트 인터페이스로 바꾸고 보면 오히려 객체가 시각화되서 잘 보인다.
당연히 해당 인터페이스를 타입으로 지정한 변수에는 타입에 맞는 객체 데이터를 할당 받아야 한다. 객체를 대입해주는 부분은 클래스로 따지면 new 클래스() 와 같다고 보면 된다.
그리고 너무도 당연하게도 없는 프로퍼티에 접근하니 에러를 내뿜을 수 밖에 없다.
interface Animal {
name: string;
move(distanceInMeters: number): void;
}
interface Snake extends Animal {
// ...
}
interface Horse extends Animal {
leg: number;
run(): void;
}
let tom: Animal = {
name: '',
move: () => {},
};
tom.leg; // Error !! - 위의 대입한 객체에서 해당 데이터가 없다
tom.move(34); // Tommy the Palomino moved 34m
tom.run(); // Error !! - 위의 대입한 객체에서 해당 데이터가 없다
let tom2: Horse = {
leg: 4,
name: '',
move: () => {},
run: () => {},
};
tom2.leg;
tom2.move(34); // Tommy the Palomino moved 34m
tom2.run();
클래스 접근 제어자
자바(JAVA) 혹은 C#을 해본 분들이라면 매우 반갑고도 익숙한 접근 제어자 키워드가 타입스크립트에도 나온다. (반면에 자바스크립트 유저들은 또 새로 배워야 한다 지못미..)
다만 타입스크립트에는 자바와는 달리 패키지라는 개념이 없기 때문에 default 키워드는 제외 되었다. 그래서 타입스크립트의 기본 접근 제어자는 public이 된다. (수식어를 생략하고 변수를 선언하면 자동 public 지정)
접근 제어자(Access Modifiers)는 클래스, 메서드 및 기타 멤버의 접근 가능성을 설정하는 객체 지향 언어의 키워드이다.
접근 제어자 수식어 종류는 다음과 같다.
수식어 | 기능 | 적용 범위 |
public | 어디서나 자유롭게 접근 가능 (기본값. 생략 가능) | 속성, 메소드 |
protected | 내 클래스를 상속한 자식 클래스 내에서 까지만 접근 가능 | 속성, 메소드 |
private | 내 클래스에서만 접근 가능 | 속성, 메소드 |
public 키워드
public은 단어 그대로 모든 곳에 공개 되어있어, 어디에서나 클래스 멤버에게 접근 가능하다는 것을 알려주는 키워드 이다.
자바 같은 경우 공개하고 싶다면 반드시 public을 일일히 붙여주어야 하는데, 타입스크립트에서는 public이 기본값으로 지정되어 있어 생략해도 자동으로 붙여진다.
따라서 우리가 이때까지 그냥 변수를 선언한 것이 사실 public으로 선언 한 것과 같다.
class Animal {
public name: string;
public constructor(theName: string) {
this.name = theName;
}
public move(distanceInMeters: number) {
console.log(`${this.name} moved ${distanceInMeters}m.`);
}
}
// 둘이 완전히 같은 구조이다.
class Animal {
name: string;
constructor(theName: string) {
this.name = theName;
}
move(distanceInMeters: number) {
console.log(`${this.name} moved ${distanceInMeters}m.`);
}
}
protected 키워드
protected는 단어그대로 보호된 멤버로서, 외부에서 접근이 불가능하고 오로지 내 클래스와 상속한 자식 클래스에서만 접근이 가능한 키워드이다.
다음 예제의 Animal 클래스의 name 속성은 protected로 선언 되었기 때문에, 상속된 자식 클래스(Cat)에서 this.name으로 참조할 수는 있지만, 인스턴스에서 cat.name으로는 접근할 수 없는걸 볼 수 있다.
class Animal {
// protected 수식어 사용
protected name: string;
constructor(name: string) {
this.name = name;
}
}
class Cat extends Animal {
getName(): string {
return `Cat name is ${this.name}.`;
}
}
let cat = new Cat('Lucy');
console.log(cat.getName()); // Cat name is Lucy.
console.log(cat.name); // Error - TS2445: Property 'name' is protected and only accessible within class 'Animal' and its subclasses.
cat.name = 'Tiger'; // Error - TS2445: Property 'name' is protected and only accessible within class 'Animal' and its subclasses.
console.log(cat.getName());
private 키워드
private는 단어그대로 사적인 멤버라서 오로지 멤버를 선언한 클래스 내에서만 접근 가능한 키워드이다.
단어 어감이 어떻게 느끼지는 모르겠지만 protect 보다 더 보호 한다는 측면으로 보면 된다.
다음 예제의 Animal 클래스의 name 속성은 private이기 때문에 파생된 자식 클래스(Cat)에서 this.name으로 참조할 수 없고, 인스턴스에서도 cat.name으로 접근할 수도 없다.
오로지 private 키워드 멤버를 포함한 내 클래스에서만 접근 가능한 것이다.
class Animal {
// private 수식어 사용
private name: string;
constructor(name: string) {
this.name = name;
}
}
class Cat extends Animal {
getName(): string {
return `Cat name is ${this.name}.`; // Error - TS2341: Property 'name' is private and only accessible within class 'Animal'
}
}
let cat = new Cat('Lucy');
console.log(cat.getName());
console.log(cat.name); // Error - TS2341: Property 'name' is private and only accessible within class 'Animal'.
cat.name = 'Tiger'; // Error - TS2341: Property 'name' is private and only accessible within class 'Animal'.
console.log(cat.getName());
ES2021에 들어서면서 최신 자바스크립트에서도 private 키워드를 지원한다.
# 키워드를 쓰면 변수를 private로 만들수 있다. 타입스크립트는 자바스크립트의 모든 문법을 포함하기 때문에 이 역시 사용이 가능하다.
final 키워드 흉내내기
final 키워드는 자바에는 있지만, 타입스크립트에는 없는 키워드이다.
자바의 final 키워드는,
- 변수에 선언하면 상수화 시켜 값을 변경 불가능하게 하고
- 메소드에 선언하면 메소드의 오버라이딩을 불가능하게 하고
- 클래스에 선언하면 클래스 상속을 불가능하게 만든다.
타입스크립트를 좀 더 객체 지향 적으로 짜고 싶다면 비록 final 키워드를 지원하지 않더라고 비스무리하게 흉내는 낼 수 있다.
final 키워드 흉내내는 법은 간단하다.
class의 constructor를 private으로 생성하면 해당 class를 생성, 확장할 수 없게 된다.
이를 응용해 자바의 싱글톤 패턴도 만들어 낼 수 있다.
싱글톤 패턴 (singleton pattern)
싱글톤은 단 하나의 객체만을 생성하게 강제하는 패턴이다.
즉 클래스를 통해 생성할 수 있는 객체는 한 개만 되도록 만드는 것이 싱글톤이다.
Static / Readonly 키워드
속성 | 설명 | 적용 범위 |
static | 정적으로 사용 | 속성, 일반 메소드 |
readonly | 읽기 전용으로 사용 | 속성 |
자바스크립트 ES6에서도 static 으로 정적 메소드만 생성할 수 있었는데, 타입스크립트에서도 당연 가능하다.
정적 속성은 클래스 바디에서 속성의 타입 선언과 같이 사용하며, 정적 메소드와 다르게 클래스 바디에서 값을 초기화할 수 없기 때문에 constructor 혹은 직접 초기화가 필요하다.
class Cat {
// 정적 프로퍼티
static tail: number = 1; // 직접 초기화 하거나
static legs: number;
constructor() {
Cat.legs = 4; // Init static 프로퍼티.
}
static setLegs(n: number): void {
Cat.legs = n; // set static 프로퍼티.
}
}
console.log(Cat.tail); // 1
console.log(Cat.legs); // undefined
new Cat();
console.log(Cat.legs); // 4
Cat.setLegs(10)
console.log(Cat.legs); // 104
readonly 을 사용하면 해당 속성은 읽기 전용 이 된다. (메소드에는 readonly를 부여할 수 없다)
class Animal {
readonly name: string; // 읽기 전용 속성
constructor(n: string) {
this.name = n; // 읽기 전용은 초기화 할때만 값 대입 가능
}
}
let dog = new Animal('Charlie');
console.log(dog.name); // Charlie
// 그러나 초기화가 아닌 추후에 접근해서 할당은 불가능
dog.name = 'Tiger'; // Error - TS2540: Cannot assign to 'name' because it is a read-only property.
const와 readonly 차이점은, const는 일반 변수에 쓰는거고, readonly는 클래스 내부 변수에 쓰이는 것이라고 이해하면 된다.
물론 static과 readonly 그리고 접근 제어자 3개를 동시에 사용도 할 수 있다.
단, 접근 제어자를 먼저 작성해야 한다.
class Cat {
private readonly name: string;
protected static eyes: number;
public static readonly tail: number = 1;
constructor(n: string) {
this.name = n; // public 속성 초기화
Cat.eyes = 2; // static 속성 초기화
}
}
const cat: Cat = new Cat('Tom');
console.log(cat.name); // Error !! - private 라 접근 불가능
console.log(Cat.eyes); // Error !! - protected 라 접근 불가능
Cat.tail = 5;; // Error !! - readonly 라 할당 불가능
접근 제어자 생성자 (멤버 생략)
흥미로운 부분은 생성자 메소드에서 인수 타입 선언과 동시에 접근 제어자 / readonly를 사용하면 바로 속성 멤버로 정의할 수 있다는 점이다.
위에서 타입스크립트는 클래스 멤버를 직접 선언 안하면 에러가 난다고 언급했었다.
이때 간단하게 접근 제어자만 붙이면 생성 로직을 간단하게 구현할 수 있다. 이렇게 하면 멤버 변수를 굳이 선언하지 않아 코드가 간결해진다는 장점이 있다.
class Cat {
constructor(name: string, age: number) {} // ERROR !!!!!!!!!!
getName() {
return this.name; // ERROR !!!!!!!!!!
}
getAge() {
return this.age; // ERROR !!!!!!!!!!
}
}
// -------------------------------------------------------------------------
// 생성자 함수 인수에 public, readonly 키워드 사용
class Cat2 {
constructor(public name: string, readonly age: number) {}
getName() {
return this.name;
}
getAge() {
return this.age;
}
}
const cat = new Cat2('Neo', 2);
console.log(cat.getName()); // Neo
console.log(cat.getAge()); // 2
클래스 Getter / Setter
객체 지향 프로그래밍에서 객체의 데이터는 객체 외부에서 직접적으로 접근하는 것을 지양한다.
왜냐하면 객체 데이터를 외부에서 읽고 변경 시 객체의 무결성이 깨질 수 있기 때문이다. 따라서 객체 지향 프로그래밍에서는 메소드를 통해 데이터를 변경하는 방법을 선호한다.
데이터는 외부에서 접근하지 않도록 막고, 메소드는 공개해서 외부에서 메소드를 통해 데이터에 접근하도록 유도한다.
이러한 역할을 하는 메소드를 Getter / Setter 라고 부른다.
거기다 멤버를 단순히 get과 set 할 뿐만아니라 블록 내에서 객체 데이터에 대한 제어를 상세히 할수 도 있어 추천되는 방법이다.
이미 자바스크립트에서도 지원하는 문법이니 다음 포스팅을 참고하길 바란다.
class Employee {
private readonly fullNameMaxLength = 10;
private name = '';
// Getter 메소드
get fullName(): string {
return this.name; // 이름을 get
}
// Setter 메소드
set fullName(newName: string) {
// 만일 이름 문자가 10글자를 초과하면 에러를 낸다
if (newName && newName.length > this.fullNameMaxLength) {
throw new Error('fullName has a max length of ' + this.fullNameMaxLength);
}
this.name = newName; // 이름을 set
}
}
let employee = new Employee();
employee.fullName = 'Bob Smith'; // 이름을 10글자 넘으면 안됨
if (employee.fullName) {
console.log(employee.fullName);
}
클래스를 매개변수로 사용
구성 시그니처(Construct signature)
함수 매개변수에 클래스를 전달해서, 함수를 호출하면 클래스 초기화 되는 로직을 구현하고 싶다면 인터페이스의 구성 시그니처(Construct signature)를 사용하여야 한다.
interface IName {
new (PARAMETER: PARAM_TYPE): RETURN_TYPE // Construct signature
}
구성 시그니처는 인터페이스의 함수 타입 정의인 호출 시그니처(Call signature)와 비슷하지만, 앞에 new 키워드를 사용한다는 점이 다르다.
즉, new 클래스() 초기화 생성자 함수 구조 타입을 정의한 것이라고 보면 된다.
이러탄 new 생성자 함수 자체 타입을 정의하는 이유는, 함수에 클래스 자체를 인자로 넘길때 인식시키기 위해서 이다. (클래스 자체를 타입으로 넘기면 에러)
interface IFullName {
firstName: string;
lastName: string;
}
interface IFullNameConstructor {
new (firstName: string): IFullName; // Construct signature
}
class Anderson implements IFullName {
public lastName: string;
constructor(public firstName: string) { // public firstName 접근 제어자로 선언해서 자동으로 this.firstName으로 만들어줌
this.lastName = 'Anderson';
}
}
// 클래스를 받아 대신 new 클래스 초기화 해주는 메소드
function makeSon(c: IFullNameConstructor, firstName: string) {
return new c(firstName);
}
function getFullName(son: IFullName) {
return `${son.firstName} ${son.lastName}`;
}
const tomas = makeSon(Anderson, 'Tomas');
const jack = makeSon(Anderson, 'Jack');
getFullName(tomas); // Tomas Anderson
getFullName(jack); // Jack Anderson
클래스 오버라이딩 / 오버로딩
- 오버로딩 : 기존에 없는 새로운 메소드를 추가하는 것
- 오버라이딩 : 상속받은 메소드를 재정의 하는 것
구분 | Overriding | Overloading |
접근 제어자 | 부모 클래스의 메소드의 접근 제어자보다 더 넓은 범위의 접근 제어자를 자식 클래스의 메소드에서 설정할 수 있다. | 모든 접근 제어자를 사용할 수 있다. |
리턴형 | 동일해야 한다. | 달라도 된다. |
메소드명 | 동일해야 한다. | 동일해야 한다. |
매개변수 갯수 / 타입 | 동일해야 한다. | 달라야만 한다. |
적용 범위 | 상속관계에서 적용된다. | 같은 클래스 내에서 적용된다. |
오버라이딩(Overriding)
오버라이딩이란 조상 클래스로부터 상속받은 메서드의 내용을 변경(재 정의)하여 사용하는 것이다.
상속받은 메서드를 그대로 사용할 수도 있지만, 필요에 따라 메서드를 재정의 하여 사용해야 하는 경우가 있기 때문이다.
[오버라이딩 조건]
- 매개변수 숫자가 같아야 한다. 단, 매개변수명은 달라도 된다.
- 매개변수 타입이 같거나, 하위 타입이여야 한다.
- 리턴 타입이 같아야 한다.
/* 부모 class */
class Gun {
// 원본 함수
constructor(public name: string) {}
// 원본 함수
shot(times: number) {
for (let i = 0; i < times; i++) {
console.log('빵야!!!');
}
}
}
/* 부모를 상속한 자식 class */
class RailGun extends Gun {
constructor(public name: string) {
super(name);
}
shot(times2: number) {
for (let i = 0; i < times2; i++) {
console.log('삐융~!!!');
}
}
}
let railgun = new RailGun('플라즈마', 99);
railgun.shot(3);
만약 오버라이딩한 부모의 메서드를 직접 불러오고 싶다면, super 키워드로 불러올 수가 있다.
shot(times2: number) {
super.shot(times2); // 부모 메서드 호출
for (let i = 0; i < times2; i++) {
console.log('삐융~!!!');
}
}
오버로딩(Overloading)
오버로딩이란 하나의 클래스 안에서 같은 이름의 메서드를 여러개 정의하는 것을 말한다.
일반적으로 하나의 클래스 안에 같은 이름의 메서드를 정의하게 되면 에러가 발생하게 되지만, 멀티 적인 기능을 위해 오버로딩의 조건을 만족하면 같은 이름의 메서드를 여러개 정의 할 수 있다.
[오버로딩 조건]
- 매개변수의 개수나 타입이 달라야 한다.
- 만일 매개변수를 늘려서 오버로딩 하고 싶다면, 추가된 매개변수에 옵셔널 프로퍼티(?:) 를 사용한다.
class User {
public name: string = '';
public age: number = 0;
// @Overloading
join(name: string, age: number): User;
join(name: string, age: string, bool: boolean): string;
join(name: any, age: any, bool?: any): any {
if (bool) console.log(`bool is ${bool}`);
if (typeof age === 'number') {
return { name, age };
} else {
return '나이는 숫자로 입력해 주세요';
}
}
}
class Inpa extends User {
// ...
}
/* 오버로딩한 함수 호출 */
const user: Inpa = new Inpa();
console.log(user.join('홍길동', 99));
// { name: '홍길동', age: 99 }
console.log(user.join('홍길동', '99', true));
// bool is true
// 나이는 숫자로 입력해 주세요
자바(Java)와 달리 자바스크립트(Javascript)에서는 원래 오버로딩 개념은 없다. 그래서 자바의 클래스 상속을 통한 다형성(오버로딩, 오버라이딩)으로 생각하면 안된다.
위의 예제는 OOP 이해를 위해 타입스크립트의 함수 오버로딩 기능은 클래스에 메소드 오버로딩으로 흉내낸 것 뿐이니 참고정도로만 보자.
추상 클래스
추상(Abstract) 클래스는 다른 클래스가 파생될 수 있는 기본 클래스로, 인터페이스와 유사하다. (사실 인터페이스도 일종의 추상 개념이다)
인터페이스는 클래스나 변수가 정의할 타입 기본 뼈대를 정의하는 것이라고 했었다.
추상 클래스는 클래스 전용으로 클래스가 정의할 타입 뼈대를 정의 하는 것이라고 이해하면 된다.
이외에도 인터페이스와의 차이점은 타입 뼈대 뿐만 아니라 아예 일반 클래스처럼 맴버를 정의할 수 있다는 점이 있다.
추상 클래스로 만들기 위해서는 abstract 키워드를 써야하는데, 클래스뿐만 아니라 속성과 메소드에도 적용 할 수 있다. (그러면 추상 속성, 추상 메소드 가 된다)
추상 클래스는 인터페이스와 같이 직접 인스턴스를 생성할 수 없기 때문에 인터페이스를 implements 한 것처럼 자식 클래스에 상속 extends 하면 된다.
/* ** Interface ** */
interface IAnimal {
name: string;
getName(): string;
}
class Dog implements IAnimal {
constructor(public name: string) {}
getName() {
return this.name;
}
}
const dog: Dog = new Dog('buldog');
/* ** 추상 클래스 ** */
abstract class Animal {
abstract name: string; // 추상 속성. 상속된 클래스에서 구현
abstract getName(): string; // 추상 메소드. 상속된 클래스에서 구현
// 추상 클래스는 인터페이스와 달리 이렇게 직접 멤버를 선언할수 도 있다.
// 이 멤버들은 상속 클래스에서 구현해도 되고 구현안해도 된다.
protected constructor(public legs: number) {}
getLegs() {
return this.legs;
}
getHello() {
return 'Hello World';
}
}
class Cat extends Animal {
constructor(public name: string, legs: number) {
super(legs);
}
getName() {
return this.name;
}
}
//! const animal: Animal = new Animal(); // Error - 추상 클래스는 직접 초기화 불가능. 무조건 상속을 해야함
const cat = new Cat('Lucy', 4);
console.log(cat.getName()); // Lucy
console.log(cat.legs); // 4
여기까지 보면 인터페이스와 큰 차이를 못느끼겠지만, 추상 클래스의 강점은 일반 멤버를 명시할 수 있다는 점이다. (인터페이스는 불가능)
abstract 클래스 Animal 을 보면 abstract 키워드가 들어가지 않은 메소드 constructor() , getLegs() , getHello() 를 볼수 있는데, 추상클래스를 상속한 Cat 클래스에도 따로 이들을 오버로딩 하거나 오버라이딩 하지 않은 걸 볼 수 있다.
그렇지만 상속받은 클래스에서 이런식으로 추상클래스의 멤버에 바로 접근해서 직접적인 사용할 수 있다.
// 추상클래스를 상속한 Cat 쿨래스에서는 직접 명시 안해도, 추상클래스의 메소드를 이렇게 바로 사용도 할 수 있다.
console.log(cat.getLegs()) // 4
console.log(cat.getHello()) // 'Hello World'
[인터페이스 vs 추상클래스]
둘이 비슷한 역할을 하면 어떤 것을 사용하는게 좋을까?
자바 진영 쪽에서는 인터페이스를 강력히 권장하지만, 자바스크립트 진영에선 솔직히 마음 가는대로 사용해도 문제없다고 생각 된다.
그래도 타입스크립트의 인터페이스는 기능이 많기 때문에 범용적인 상황에선 인터페이스를 애용하는게 좋고, 만약 클래스를 정의하는 타입으로서 '만' 사용할경우 클래스 한정으로 유연한 abstract를 사용하는게 좋다고 본다.
최근 타입스크립트 버전에서는 추상(abstract) 메소드에 async 키워드를 사용할 수 없다.
프로미스를 반환하는 메소드일지라도 abstract 키워드만 있으면 알아서 인식하니 일반 추상 메소드 처럼 선언해주면 된다.
abstract 생성자
위의 클래스를 매개변수로 사용하는 구성 시그니처 파트에서 다뤄봤듯이, 추상 클래스에서도 new 생성자 함수 자체 타입이 존재한다.
type AbstractConstructor = abstract new (...args: any[]) => any;
다음은 추상클래스를 함수로 재생성해서 클래스 Dog에 extends 하는 예제이다.
꽤 난이도 있으니까 코드를 잘 살펴보자.
type AbstractConstructor = abstract new (...args: any[]) => any;
// 추상 클래스
abstract class Animal {
abstract walk(): void;
breath() {
console.log('breath');
}
}
// 매개변수 Ctor은 constructor 약자
function animatableAnimal(Ctor: AbstractConstructor) {
abstract class StopWalking extends Ctor {
animate() {
console.log('animate');
}
}
return StopWalking;
}
class Dog extends animatableAnimal(Animal) { // 추상클래스 만드는 함수를 extends
walk() {
console.log('walk');
}
}
const dog = new Dog();
dog.breath(); // 'breath'
dog.walk(); // 'walk'
dog.animate(); // 'animate'
# 참고자료
https://heropy.blog/2020/01/27/typescript/
https://typescript-kr.github.io/pages/classes.html
이재승 타입스크립트 시작하기
https://www.edureka.co/blog/method-overloading-and-overriding-in-java/
이 글이 좋으셨다면 구독 & 좋아요
여러분의 구독과 좋아요는
저자에게 큰 힘이 됩니다.