객체지향 프로그래밍(Object Oriented Programming, OOP)
객체지향 프로그래밍(Object Oriented Programming, OOP)은 커다란 문제를 클래스 단위로 나누고
클래스 간의 관계를 추가하면서 코드 중복을 최소화 하는 개발방식이다.
클래스 간의 관계를 추가할 때는 상속이나 포함 관계를 고려하여 추가한다.
OOP를 통해 어플리케이션을 개발하면 코드 중복을 상당히 줄일 수 있다.
타입스크립트는 자바스크립트(ES6)에 비해서 OOP를 지원하는 부분이 훨씬 더 많다.
🏷️ 요약
상속(Extends)
클래스는 'extends'를 붙여 상속을 주고 받으며 부모-자식관계의 클래스들을 생성할 수 있다.
먼저 부모가 될 클래스를 생성한 뒤, 그 뒤 이를 상속받을 자식 클래스는
class 자식클래스명 extends 부모클래스명 순으로 입력하면 된다.
추상 클래스(Abstract Class)
추상 클래스를 정의할 때는 class 앞에 abstract라고 표기합니다.
또한 추상 메서드를 정의할 때도 메서드명 앞에 abstract라고 표기합니다.
추상 메서드는 정의만 있을 뿐 몸체(body)가 구현되어 있지 않다.(추상클래스)
몸체는 추상 클래스를 상속하는 클래스에서 해당 추상 메서드를 통해 필히 구현해야 한다.
인터페이스(Interface)
객체의 타입을 정의할 수 있는 interface라는 키워드를 제공합니다.
인터페이스는 여러가지 타입을 갖는 프로퍼티로 이뤄진 새로운 타입을 정의하는 것과 유사합니다.
인터페이스는 프로퍼티와 메서드를 가질 수 있는 점에서 클래스와 유사하지만
직접 인스턴스를 생성할 수 없고 모든 메서드는 추상메서드입니다.
클래스 상속(Extends) - (IS-A)
- 클래스 간 상속을 구현하는 키워드
- 이미 존재하는 클래스를 기반으로 새로운 클래스를 만들 때 사용
- 상속을 통해 기존 클래스의 프로퍼티와 메서드를 재사용하고,
새로운 기능을 추가하거나 기존 기능을 변경할 수 있다 - extends 키워드를 사용하여 클래스 상속을 구현할 때,
하위 클래스는 상위 클래스의 모든 멤버를 상속받게 된다. - 하위 클래스는 상위 클래스의 멤버를 사용하거나
오버라이딩(재정의)하여 자신만의 동작을 구현할 수 있다. - 자식 class는 1개의 부모 class만 상속 받을 수 있습니다.
constructor & super
- 수퍼 클래스를 상속받은 서브 클래스는
수퍼 클래스의 기능에 더하여 좀 더 많은 기능을 갖도록 설계하는 것이
자식 컴포넌트에서 부모를 상속받는 이유이다.
- 기존 부모의 속성 + 부모 속성 수정 + 새로운 속성 추가 = 자식 클래스(서브 클래스)
constructor
- 상속 받은 부모 클래스의 생성자를 서브 클래스의 생성자로 덮어쓸 수 있도록 함
- 즉 부모의 물려받은 속성을 서브 클래스의 consructor로 그대로 쓰는 것과 동시에 수정이 가능하다.
super()
- 수퍼(부모) 클래스의 생성자에 요구되는 인자를 전달해야 한다.
- 기본 클래스 호출 시 사용된다.
- 생성자에서 this 사용 전에 호출되어야 한다.
- this.name은 name이라고 생각하면 되고, 파생클래스에서 인자를 전달할 때 사용한다.
- 수퍼 클래스 constructor를 덮어쓰기 위해서는 super() 실행이 필요함.
사용예시
1. 매개 변수로 Sohyun을 넣는다.
즉, person이라는 변수 안에 Person클래스(파생클래스)안으로 Sohyun이 들어감.
이 때 파생클래스는 부모 클래스의 내용을 상속을 받았을 뿐이지, 외부 클래스이므로 바로 접근이 가능하다.
2. 파생 class의 생성자 함수(constructor)로 Sohyun이 들어간다.
3. 생성자 함수에서 상위의 기본 클래스인 name으로 Sohyun이 들어가게 된다.
즉, super은 자식 클래스에서 부모 클래스로 연결해주는 징검다리 같은 친구라고 생각하면 됨!
// 기본 클래스
class Actor {
name: string
constructor(name: string) {
this.name = name;
}
actorName() {
return `배우의 이름은 ${this.name}`;
}
}
// 파생 클래스
class Person extends Actor {
constructor(name: string) {
super(name);
}
personName() {
return `${super.actorName()} 사람의 이름은 ${this.name}`;
}
}
const person = new Person('Sohyun');
console.log(person.personName()); // 배우의 이름은 Sohyun 사람의 이름은 Sohyun
📌 포함(HAS-A)
class가 다른 class를 포함하는 관계 입니다.
풀어서 말하면 포함하는게 갖는 것이기 때문에 HAS-A 관계 입니다.
포함(HAS-A) 관계는 크게 2개로 나눠 집니다.
1) 합성(composition) 관계 (강한 관계)
- A Class가 B Class를 생성자함수에서 생성하여 속성으로 포함 하는 것입니다.
- A Class를 instance생성 후 제거 될 시 B Class instance 역시 제거 됩니다.
(instance 생명 주기를 함께 합니다.)
class Engine {
make: string;
constructor() {
this.make = "korea";
}
}
// 포함관계 Class
class KoreaCar {
engine: Engine;
constructor() {
this.engine = new Engine();
}
}
let mycar = new KoreaCar();
2) 집합(aggregation) 관계 (약한 관계)
- A Class가 B Class를 생성자함수의 매개변수로 받아서 속성으로 포함하는 것입니다.
- A Class를 instance생성 후 제거 될 시 B Class instance는 따로 제거해 줘야 합니다.
(instance 생명 주기가 개별 적입니다.)
class Engine {
make: string;
constructor() {
this.make = "korea";
}
}
// 집합 관계 Class
class AmericaCar {
engine: Engine;
constructor(engine: Engine) {
this.engine = engine;
}
}
let engine = new Engine();
let supercar = new AmericaCar(engine);
추상 클래스(Abstract Class) · 추상 메서드(Abstract Method)
- 추상 클래스
하나 이상의 추상 프로퍼티/메서드를 포함하면서
구현 메서드도 포함할 수 있는 클래스이다. (직접 인스턴스 생성 불가) - 추상 메서드
추상 클래스 내에서는 메서드를 적지 않고 call signature만 작성 (구현X)
추상 클래스를 상속받는 클래스들이 반드시 구현(implement)해야하는 메서드 (구현O)
- 추상 메서드 : 선언부만 있는 메서드
- 구현 메서드 : 실제 구현 내용을 포함하는 메서드 - 추상 클래스는 단독으로 객체를 생성할 수 없고
추상 클래스를 상속하고 구현 내용을 추가하는 자식 클래스를 통해 객체를 생성 - 추상 클래스 선언 시 class 앞에 abstract라고 표기합니다.
또한 추상 메서드를 정의할 때도 메서드명 앞에 abstract라고 표기합니다. - 추상클래스를 상속받는 방법 : extends 키워드 사용
📌 추상 클래스는 언제, 왜 사용하는 걸까?
상속을 구현할때 사용한다.
- 객체간에 공통적으로 반복사용되는 기능이 있을 경우
공통적으로 사용되는 코드를 모두 추상 클래스에 모아둠으로써 중복을 제거할 수 있다. - 자식 클래스는 부모 클래스의 모든 기능을 사용할 수 있다.
→ 코드의 재 사용성 - 자식 클래스는 부모 클래스를 확장하여 기능을 추가할 수 있다.
→ 코드의 확장성
단점
자식 클래스는 반드시
부모 클래스의 모든 추상 프로퍼티/메서드를 구현해야 제대로 동작할 수 있고,
부모(추상)클래스 역시 추상 프로퍼티/메서드를 구현하려면
자식 클래스가 있어야 하므로 객체간의 결합도가 매우 높다.
📝 추상클래스 형태
overrding을 사용해서 추상 method를 구현합니다.
abstract class 추상클래스 {
abstract 추상method();
abstract 추상속성:string;
public 구현method():void {
공통적으로 사용할 로직을 추가합니다.
로직에서 필요시 추상 method를 호출합니다.
this.추상method();
}
}
class 자식클래스 extends 추상클래스 {
public 추상속성: sting;
public 추상method(): void{
추상method를 실제로 구현
}
}
abstract class Animal {
name: string;
constructor(name: string) {
this.name = name;
}
abstract makeSound(): void;
move() {
console.log('Moving...');
}
}
class Dog extends Animal {
constructor(name: string) {
super(name);
}
makeSound() {
console.log('Bark!');
}
}
let myDog = new Dog('Spot');
myDog.makeSound(); // Output: 'Bark!'
myDog.move(); // Output: 'Moving...'
사용예시
// ▶추상 클래스
abstract class Human {
spendOneDay() {
console.log("기상...!");
console.log(this.work());
console.log("수면...!");
}
abstract work(): string; // 추상 메서드
}
// ▶추상 클래스에 대한 자식 클래스(구현)
class Student extends Human{
// 추상 클래스를 상속받은 곳에서 추상 메서드 구현
work() {
return "학교에서 공부합니다.";
}
}
// ▷추상 클래스(추상화)에 타입 의존
const student: Human = new Student();
student.spendOneDay();
// ▷정상적으로 true 반환
console.log(student instanceof Human);
📌 객체의 타입 비교 검사 (instanceof)
객체가 특정 클래스의 인스턴스인지 확인하는 데 사용
instancof는 비교연산자로, 해당하는 변수가 사용하고 있는 객체 체인을 검사한다.
쉽게 말해서, 해당하는 변수의 클래스와 비교해서 true/false를 반환해준다고 생각하면 된다.
// obj가 Class에 속하거나 Class를 상속받는 클래스에 속하면 true반환
obj instanceof Class
변수 instanceof 자료형
📌 객체의 타입 변환 (as)
상위 클래스를 하위 클래스로 변경하는 기능
상위 클래스에 하위 클래스의 실체를 넣었다 하더라도, 하위 클래스의 변수를 이용하고 싶을 때
자기꺼 함수는 쓸 수 있는데, 자기꺼 필드는 사용을 못하니까 생김
(객체 as 타입)
(변수 as 자료형)
사용예시
class Names {
name:string;
constructor(){
this.name = '이름';
}
}
class Products extends Names{
count:number;
constructor(){
super();
this.count = 0;
}
}
let b:Names = new Products();
console.log(b instanceof Products); // true
(b as Products).count = 5; // 자기꺼 필드 조작 가능
인터페이스(Interface)
- interface 키워드를 사용하여 정의
- 객체가 반드시 구현해야할 프로퍼티/메서드의 선언부만 모아놓은 것
인터페이스에 명시된 프로퍼티/메서드의 구현은
해당 인터페이스를 실행하는 변수, 함수, 클래스에서 하며,
인터페이스에 선언된 프로퍼티/메서드를 구현하지 않을 경우 에러가 발생한다. - 인터페이스는 다음과 같은 범주에 대해 약속을 정의할 수 있습니다.
- 객체의 스펙(속성과 속성의 타입)
- 함수의 파라미터
- 함수의 반환 타입
- 배열과 객체의 인덱싱 방식
- 클래스 - 인터페이스는 프로퍼티와 메소드를 가질 수 있는 점에서 클래스와 유사하지만
직접 인스턴스를 생성할 수 없고 모든 메소드는 추상메소드입니다. - 인터페이스(interface)는 클래스에서 구현 부분이 빠진 타입으로 이해하면 된다.
- 인터페이스는 컴파일 후에 사라지게 된다.
📌 인터페이스는 언제, 왜 사용하는 걸까?
- 컴파일 타임에 객체의 타입을 체크하기 위해 사용한다.
→ 타입 안정성을 높인다. - 객체의 프로퍼티/메서드의 구현을 강제함으로써 객체의 구조를 고정시킨다.
→ 객체의 유지보수/확장이 쉬워진다. - 서로 다른 객체들이 동일한 기능을 수행해야할 때 유용하다. (Duck Typing)
→ 객체간의 결합도를 낮춘다.
특징
1. 인터페이스는 런타임에 존재하지 않는다.
- 컴파일 타임에 타입 안정성을 확보 하기 위한 용도로 쓰여지고
컴파일 이후에는 제거되기 때문이다.
- 런타임에 존재하지 않기 때문에 typeof를 이용해서 인터페이스의 타입을 알아낼 수 없다.
2. 인터페이스끼리 상속 및 다중상속이 가능하다.
3. 인터페이스는 클래스도 상속받을 수 있다.
📝 인터페이스 선언 형태
인터페이스는 객체의 타입을 정의하는 것이 목적이기 때문에,
{}로 프로퍼티이름과 프로퍼티 타입을 나열하는 형태로 사용합니다.
interface 인터페이스명{
property name[?]: property type[,...]
}
변수와 인터페이스
인터페이스는 변수의 타입으로 사용이 가능합니다.
인터페이스를 타입으로 선언한 변수는 해당 인터페이스를 준수해야합니다.
그리고 인터페이스를 사용해서 함수의 파라미터 타입을 선언할 수도 있습니다.
interface Todolist {
id: number;
title: string;
done: boolean;
}
let todo: Todolist;
todo = { id: 1, title: 'typescript 학습하기', done: false };
함수와 인터페이스
인터페이스는 함수의 타입으로도 사용할 수 있습니다.
함수의 인터페이스에는 타입이 선언된 파라미터 리스트와 리턴 타입을 정의하고 함수는 이를 준수해야합니다.
interface Print {
(input: string | number): void;
}
const print: Print = (input) => {
console.log(input);
}
클래스와 인터페이스
클래스 선언문의 implements 뒤에 인터페이스를 선언하면
클래스는 지정된 인터페이스를 반드시 구현해야합니다.
인터페이스는 프로퍼티와 메소드를 가질 수 있지만 직접 인스턴스를 생성할 수는 없습니다.
그리고 인터페이스는 프로퍼티 뿐만아니라 메서드도 포함할 수 있습니다.
단, 모든 메서드는 추상메소드여야 합니다.
따라서 인터페이스를 구현하는 클래스는 인터페이스에서 정의한
프로퍼티와 추상 메서드를 반드시 구현해야합니다.
interface ITodolist{
id: string;
title: string;
done: boolean;
pass: () => void;
};
class Todolist implements ITodolist{
constructor(
public id: string,
public title: string,
public done: boolean
) { }
pass = () => {
console.log("NO!!!")
}
};
const todos = new Todolist('efese-fefe1', 'typescript 공부하기', true);
console.log(todos);
/*
Todolist {
id: 'efese-fefe1',
title: 'typescript 공부하기',
done: true,
pass: [Function (anonymous)]
}
*/
interface IAnimal {
name: string;
makeSound(): void;
}
class Dog implements IAnimal {
name: string;
constructor(name: string) {
this.name = name;
}
makeSound() {
console.log('Bark!');
}
}
let myDog = new Dog('Spot');
myDog.makeSound(); // Output: 'Bark!'
덕 타이핑
FrontEnd 클래스는 IDeveloper 인터페이스를 상속받아 생성됐고,
이 클래스를 통해 생성된 인스턴스인 frontEndDeveloper는 startCoding의 파라미터인 IDeveloper의 타입과 일치합니다.
반면 backEndDeveloper는 IDeveloper 인터페이스를 구현하지 않았지만 coding 메서드를 갖고 있습니다.
이 상태에서 startCoding 함수에 IDeveloper 인터페이스를 구현하지 않은 BackEnd의 인스턴스
backEndDeveloper를 파라미터로 전달해도 에러가 나지 않습니다.
Typescript에서는 해당 인터페이스에서 정의한 프로퍼티나 메서드가 있다면 해당 인터페이스를 구현한 것으로 인정합니다.
그리고 이것을 덕타이핑 혹은 구조적 타이핑이라고 합니다.
인터페이스는 개발 단계에서 도움을 주기위해 제공되는 기능입니다.
즉 트랜스 파일링시에 삭제되는 기능입니다.
interface IDeveloper{
coding(): void;
}
class FrontEnd implements IDeveloper{
coding() {
console.log("React!")
}
}
class BackEnd {
coding() {
console.log("Spring!");
}
}
const frontEndDeveloper = new FrontEnd();
const backEndDeveloper = new BackEnd();
const startCoding = (developer: IDeveloper): void => {
developer.coding();
}
startCoding(frontEndDeveloper); // React!
startCoding(backEndDeveloper); // Spring!
interface Sohyun {
name: string;
}
function getName(sohyun: Sohyun):void {
console.log(`이름은 ${sohyun.name} 입니다.`)
}
const me = { name: 'Kwon Sohyun', age: 29 };
getName(me); // 이름은 Kwon Sohyun 입니다.
는 트랜스파일링 이후
function getName(sohyun) {
console.log("\uC774\uB984\uC740 ".concat(sohyun.name, " \uC785\uB2C8\uB2E4."));
}
var me = { name: 'Kwon Sohyun', age: 29 };
getName(me);
이와 같이 사라집니다.
인터페이스 상속
- 인터페이스간 상속은 extends 키워드를 사용한다.
- 2개 이상 상속 가능하다.
- 상속받은 속성은 모두 인터페이스 구조에 추가된다.
- 클래스도 상속 받을 수 있다.
interface A {
a: number;
}
interface B extends A {
// a: number;
b: number;
}
// 2개 이상 상속 가능
interface C extends A, B {
// a: number;
// b: number;
c: number;
}
➕ 클래스 상속
- 클래스의 모든 멤버(public, protected, private)가 상속되지만 함수는 추상 메서드로 상속된다.
- 구현부를 제외한 모든 구조가 상속된다.
class AB {
constructor(public a: number, public b: number) {}
print() {}
}
interface C extends AB {
// a: number
// b: number
// print() {}
c: number;
}
❓클래스와 인터페이스의 공통점
- 코드를 추상화 하여 관리하기 위함
- 재사용성을 높일 수 있다.
- 타입체크를 위한 중요한 개념
❓클래스와 인터페이스의 차이점
클래스 | 인터페이스 |
객체 생성 | 타입 체크 |
속성, 메서드 구현 | 타입 정의 |
인스턴스 생성 | 객체 생성 안 함 |
상속 지원 | 상속을 통해서 구현만 가능 |
📌 인터페이스(Interface) vs 추상 클래스(Abstract Class)
인터페이스(Interface)
클래스에서 공통된 메서드와 속성을 정의할 때 사용
implements를 통해 다중 상속을 대체
추상 클래스(Abstract Class)
클래스에서 공통된 메서드와 속성을 정의할 때 사용
구현을 제공하고 하위 클래스에서 구현을 확장할 때 사용
코드의 중복을 줄이고 상속을 통해 계층 구조를 만들 때 사용
1. 내부 로직 포함 여부
인터페이스 예시를 들여다보면
인터페이스 내에는 해당 인터페이스가 구현을 강제할 메서드의 이름과 인자, 반환 타입 만이 명시되어있다.
그와 달리 추상 클래스 내에는 인터페이스와 마찬가지인 메서드도 존재하는 반면(abstract 메서드),
실제 로직이 작성된 메서드도 존재한다.
따라서 첫 번째 차이는 실제 런타임에서 작동하는 코드가 포함되어 있는지 여부이다.
추상 클래스는 보통 자식 클래스에서 공유할 공통 로직을 부모 추상 클래스에 작성하여
불필요한 코드 재반복 및 향후 수정 소요를 줄이는 방식으로 활용한다.
2. 자식 클래스 구현 방식
인터페이스 예시를 보면,
Student 클래스가 IHuman 인터페이스에 의존한다는 것을 명시하기 위해 implements를 사용한다.
반면 추상 클래스 방식에서는 extends를 사용하여 부모 클래스를 상속하게 된다.
여기서 알 수 있는 두 번째 차이는 자식 클래스 구현 시 사용하는 implements, extends 여부이다.
implements와 extends에는 차이가 존재하는데,
implements를 통해서는 여러 인터페이스를 동시에 상속할 수 있는
반면 extends를 통해서는 오직 하나의 부모 클래스를 가질 수 있다.
추상 클래스는 단일 상속만 지원합니다.
따라서
implements를 사용하는 관계를 has-A 관계,
extends를 사용하는 관계를 is-A 관계라고 한다.
왜 단일 상속만 지원할까요?
만약 두 개이상의 클래스가 동일한 추상 클래스를 상속하고
그 클래스 들이 하나에 클래스에게 상속될 경우 "다이아몬드 문제"가 발생할 수 있습니다.
위와 같이 DogCat 클래스가 Dog와 Cat 클래스를 상속받을 수 있다면
super.makeSound() 호출 시 어떤 클래스의 makeSound 메서드를 호출해야 할지 알 수 없습니다.
이러한 문제를 피하기 위해 TypeScript는 단일 상속만을 지원하며
interface를 통해 구현(implements)하는 방식으로 다중 상속을 대체합니다.
3. 런타임 존재 여부
각 예시의 가장 마지막 라인을 보면
instanceof 를 이용하여 해당 인스턴스가 무엇에서 파생된 인스턴스인지 확인하는 로직이 존재한다.
해당 로직은 추상 클래스 방식에서는 문제없이 작동하지만,
인터페이스 방식에서는 컴파일 에러가 발생한다.
이유는 바로 인터페이스는 컴파일 시 사라지는, 런타임에서 존재하지 않는 코드이기 때문이다.
자바와 같은 언어와 달리, 타입스크립트에서는 인터페이스가 컴파일 타임에만 존재하게 된다.
따라서 인터페이스를 통한 추상화를 구현한 로직에서 해당 객체가 어떤 인터페이스의 구현체인지 확인하기 위해서는
타입 가드를 따로 작성해주어야한다.
반면 추상 클래스는 컴파일 시 일반 클래스로 변환되어 런타임에도 존재하기 때문에
instanceof를 통한 간편한 타입 비교가 가능하다.
즉 세번째 차이는 런타임에 코드가 존재하는지에 대한 여부이다.
'📌 Front End > └ TypeScript' 카테고리의 다른 글
[TypeScript] 제네릭(Generic) (0) | 2024.08.04 |
---|---|
[TypeScript] 오버라이딩(Overriding), 오버로딩(Overloading) (0) | 2024.08.02 |
[TypeScript] 타입스크립트 클래스 · 객체 지향 문법 총 정리 (0) | 2024.07.31 |
[TypeScript] 열거형(Enum)타입 (0) | 2024.07.31 |
[TypeScript] 클래스(Class) (1) | 2024.07.31 |