객체지향 프로그래밍(OOP, Object-Oriented Programming)은 애플리케이션을 개발할 때 코드 중복을 획기적으로 줄일 수 있는 방법이다. 객체지향 프로그래밍은 커다란 문제를 클래스라는 단위로 나누고 클래스 간의 관계를 추가하면서 코드 중복을 최소화하는 개발 방식이다. 클래스 간의 관계는 상속이나 포함 관계를 고려해 추가한다.
class Rectangle {
x: number,
y: number,
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
getArea(): number { return this.x * this.y; }
}
이렇게 선언한 Rectangle 클래스는 클래스 타입(class type)이 된다. 클래스 타입은 다음의 인터페이스 타입과 정확히 일치한다.
interface Rectangle {
x: number;
y: number;
getArea(): number;
}
클래스 내부에는 생성자인 constructor를 정의한다. 생성자는 객체를 생성할 때 클래스에 필요한 설정을 매개변수로 전달받아 멤버 변수를 초기화한다. 클래스를 선언할 때 생성자를 생략하면 기본 생성자(default constructor)를 호출한다. 만약 클래스에 초기화할 내용이 없다면 다음처럼 클래스 선언 때 생성자를 생략 가능하다.
class Rectangle { }
클래스는 멤버 변수와 멤버 메서드 등으로 구성된 틀이며 클래스를 실제로 사용하려면 객체로 생성해야 한다. 예를 들어 Rectangle 클래스를 객체로 생성하려면 다음처럼 선언해줘야 한다.
let rectangle = new Rectangle(1, 5);
위 코드에서 보면 new 키워드를 이용해 Rectangle 객체를 생성해 객체 참조변수(object reference variable)에 할당했다. 생성된 객체는 실제 메모리에 위치하고 객체의 참조가 객체 참조변수에 할당되는 과정을 인스턴스화라고 한다.
객체지향 프로그래밍에서는 클래스 간의 관계를 크게 두가지로 나눠볼 수 있다.
상속(inheritance)은 코드의 재사용성을 높인다. 예를 들어 자식 클래스가 부모 클래스를 상속하면 자식 클래스는 부모 클래스에 선언된 멤버 변수나 멤버 메서드를 상속받아 사용할 수 있다. 상속 관계를 다른 말로 IS-A 관계라고도 한다. 포함은 한 클래스에 다른 클래스를 멤버 변수로 선언하는 것으로 HAS-A 관계로 표현된다.
class dinosour extends animal {
constructor() {
super();
}
}
포함 관계는 클래스가 다른 클래스를 포함하는 HAS-A 관계이다. 클래스 내부에 다른 클래스를 포함하는 관계는 대표적으로 두가지로 나뉜다.
합성 관계는 전체가 부분을 포함하며 강한 관계이다.
class Engine { }
class Car {
private engine;
constructor() {
this.engine = new Engine();
}
}
let myCar = new Car();
myCar = null;
Car 클래스에 선언된 engine 객체는 Car 객체가 new로 생성될 때 함께 생성되고 객체(myCar)가 null이 되면 함께 제거된다. 수명주기를 함께 하므로 강한 관계가 된다.
class Engine { }
class Car {
private engine;
constructor(engine: Engine) {
this.engine = new Engine();
}
}
let engine = new Engine();
let myCar = new Car(engine);
car 객체에 null이 할당돼 제거되더라도 engine 객체는 Car 클래스 외부에 선언돼있으므로 제거되지 않는다. 수명주기를 함께 하지 않으므로 약한 관계가 된다.
생성자의 매개변수에 접근 제한자를 추가하면 매개변수 속성(parameter properties)이 돼 멤버 변수가 되는 효과가 있다.
class Cube {
constructor(public width: number, private length: number, protected height: number) { }
getVolume() {
return this.width * this.length * this.height;
}
}
let [cWidth, cLength, cHeight] = [1,2,3];
let cube = new Cube(cWidth, cLength, cHeight);
console.log("1번 세로 : ", cube.width, "cm");
console.log("2번 부피 : ", cube.getVolume(), "ml");
}
기본 접근 제한자(default access modifier)는 접근 제한자 선언을 생략할 때 적용된다. 기본 접근 제한자가 적용될 수 있는 대상은 클래스 멤버 변수, 멤버 메서드, 클래스 Get/Set 프로퍼티, 생성자의 매개변수이다. 다음은 Account 클래스에 선언된 멤버들이 접근 제한자를 생략했을 때 기본 접근 제한자가 무엇인지를 나타낸다.
class Account {
public balance: number;
public getBalance() { ... }
public setBalance(amount: number) { ... }
constructor( defaultBalance: number = 1000, protected bankName: string = "happy bank") { ... }
// 접근 제한자가 생략되면 기본 접근 제한자가 없어 생성자 내부에서만 사용 가능함
getBankName() { return this.bankName; }
getDefaultBalance() { return this.defaultBalance; } //접근 불가
}
추상 클래스(abstract class)는 구현 메서드와 추상 메서드(abstract method)가 동시에 존재할 수 있다. 구현 메서드는 실제 구현 내용을 포함한 메서드이고 추상 메서드는 선언만 된 메서드이다. 추상 클래스는 단독으로 객체를 생성할 수 없고 추상 클래스를 상속하고 구현 내용을 추가하는 자식 클래스를 통해 객체를 생성해야 한다.
추상 클래스는 abstract 키워드를 클래스 선언 앞에 붙여서 선언하고 추상 메서드를 선언할때도 사용할 수 있다.
abstract class 추상클래스 {
abstract 추상메서드();
abstract 추상멤버변수: string;
public 구현메서드(): void {
공통적으로 사용할 로직을 추가함
로직에서 필요 시 추상 메서드를 호출해 구현 클래스의 메서드가 호출되게 함
this.추상메서드();
}
}
추상 클래스에 선언한 추상 메서드는 오버라이딩(overriding)해서 자식 클래스에서 반드시 구현해서 사용해야 한다.
인터페이스는 ES6가 지원하지 않는 타입스크립트만의 특징이다. 인터페이스는 타입이며 컴파일 후에 사라진다. 추상 클래스는 선언과 구현이 모두 존재하지만 인터페이스는 선언만 존재하며, 멤버 변수와 멤버 메서드를 선언할 수 있지만 접근 제한자는 설정할 수 없다.
interface Car {
speed: number;
}
알아둘 점은 자식 인터페이스는 여러 부모 인터페이스를 다중 상속할 수 있다.
인터페이스는 객체 리터럴을 정의하는 타입으로 사용될 수 있다. 먼저 객체 리터럴의 구조를 인터페이스로 다음과 같이 정의할 수 있다.
interface Person {
name: string;
city: string;
}
let person4: Person[] = [
{ name: "a", city: "seoul" },
{ name: "b", city: "daejeon" },
{ name: "c", city: "daegu" }
];
console.log(JSON.stringify(person4));
인터페이스는 익명 함수에 대한 함수 타입을 정의할 수 있는 기능으로도 사용된다.
interface IFormat {
(data: string, toUpper?: boolean): string;
}
let format: IFormat = function (str: string, isUpper: boolean) {
...
}
오버라이딩(overriding)은 부모 클래스에 정의된 메서드를 자식 클래스에서 새로 구현하는 것을 일컫는 개념이다. 여기서 오버라이딩할 대상이 있는 부모 클래스를 오버라이든 클래스(overridden class)라 한다. 오버라이든 클래스에는 오버라이든 메서드(overridden method)가 존재한다. 오버라이든 메서드는 파생 클래스에 정의된 메서드에 오버라이딩돼 오버라이딩 메서드로 새롭게 재정의된다.
오버라이딩으로 메서드가 재정의되려면 기본적으로 오버라이든 메서드와 오버라이딩 메서드는 서로 이름이 같아야 한다. 그리고 오버라이딩을 위해 다음 두 조건을 만족해야 한다.
조건1: 오버라이든 메서드의 매개변수 타입은 오버라이딩 메서드의 매개변수 타입과 같거나 상위 타입이어야 한다.
조건2: 오버라이든 메서드의 매개변수 개수가 오버라이딩 매서드의 매개변수 개수와 같거나 많아야 한다. (단, 조건1이 성립된다는 전제가 있어야 함)
메서드 오버로딩(method overloading)은 메서드의 이름이 같지만 매개변수의 타입과 개수를 다르게 정의하는 방법을 일컫는다.
다형성(polymorphism)은 '여러 모양'을 의미하는 그리스 단어이고 다형성에서 형은 타입(type)을 의미한다. 프로그래밍 언어에서 다형성이란, 여러 타입을 받아들임으로써 여러 형태를 가지는 것을 의미한다.
자식 클래스가 부모 클래스를 상속하고 있을 때 부모 클래스를 타입으로 가지는 객체 참조변수에 자식 클래스의 객체가 할당(구조 타이핑)됨으로써 다형성을 지니게 된다.
class Planet {
public diameter: number; //지름
protected isTransduction: boolean = true; //공전
getIsTransduction(): boolean {
return this.isTransduction;
}
stopTransduction(): void {
console.log("stop1");
this.isTransduction = false;
}
}
class Earth extends Planet {
public features: string[] = ["soil", "water", "oxyzen"];
stopTransduction(): void {
console.log("stop2");
this.isTransduction = false;
}
}
let earth: Planet = new Earth();
earth.diameter = 12656.2;
console.log("1번 : " + earth.diameter); // 12656.2
console.log("2번 : " + earth.getIsTransduction()); //true
earth.stopTransduction(); //stop2
console.log("3번 : " + earth.getIsTransduction()); // false
console.log(earth.features); // 접근불가
이러한 상속 관계에서 부모 클래스(Planet)의 타입으로 지정된 객체 참조변수(earth)는 자식 클래스의 객체(new Earth())를 할당받더라도 실제 동작은 부모 클래스를 기준으로 실행된다. 따라서 earth는 부모 클래스에 선언된 getIsTransduction())에 접근할 수 있지만 자식 클래스에 선언된 멤버 변수(features)에는 접근할 수 없다.
여기서 유의해서 볼 점은 stopTransduction() 메서드는 오버라이든 메서드보다 오버라이딩 메서드가 우선으로 호출된다. 이처럼 런타임 시에 호출될 메서드가 결정되는 특성을 런타임 다형성(runtime polymorphism)이라 한다.
인터페이스의 다형성
인터페이스 또한 부모 클래스의 타입으로 할당되었을때 구현 클래스에 새롭게 추가된 메서드에 접근할 수 없다.
매개변수의 다형성(유니언 타입 이용)
매서드의 매개변수 타입을 유니언 타입을 이용함으로써 객체가 다형성의 성질을 띠도록 만들수 있다. 예를 들어 string 타입이나 number 타입을 받아들이도록 다형성 메서드를 구현하려면 매개변수의 타입을 유니언 타입으로 선언해 주면 된다.
매개변수의 다형성(인터페이스 이용
좋은 글 작성해주셔서 감사합니다.
정리가 정말 잘되어있네요!!