객체지향 프로그래밍이라 함은 코드 중복을 줄일 수 있는 프로그래밍 패러다임이다. 객체지향 프로그램이라 함은 큰 문제를 클래스로 쪼개고 클래스 간의 관계를 상속이나 포함으로 고려해가면서 추가하는 것이다.
객체지향 프로그래밍을 하려면 언어차원의 지원이 필요한데, 자바스크립트에도 class가 존재하지만 접근 제한자 등의 부족한 점이 있다. 그에 반해 타입스크립트는 부족함이 없다 볼 수 있다.
타입스크립트는 자바스크립트와 동일하게 클래스 생성시 class키워드를 이용하여 생성한다.
class Rectangle{
x:number;
y:number;
constructor(x:number,y:number){
this.x = x;
this.y = y;
}
getArea() : number = {return this.x * this.y}
}
이렇게 선언한 클래스는 클래스 타입이 되고 이를 인터페이스로 정의하면 다음과 같이 할 수 있다.
interface Rectangle {
x:number;
y:number;
getArea():nunber
}
클래스는 변수와 메소드 등으로 구성된 틀 이고 클래스를 사용하기 위해선 틀을 이용하여 객체를 생성해야한다.
const redRectangle = new Rectangle(2,5)
new 키워드를 클래스에 이용하여 객체를 생성하여 인스턴스에 할당했다. 생성된 객체는 실제 메모리에 존재하고, 해당 객체의 참조가 변수에 할당되는 과정을 인스턴스화 라고 한다. 이 코드를 클래스가 나오기 이전 버전인 es5로 컴파일하면
var Rectangle = (function (){
function Rectangle(x,y){
this.x=x;
this.y=y;
}
Rectangle.prototype.getArea= function(){
return this.x * this.y
}
return Rectangle
}())
ES5는 아직 클래스가 나오기 전이라 모듈 패턴을 이용하여 클래스를 구현한다. 모듈 패턴은 클로저를 이용하여 내부 메소드를 캡슐화 하여 전역 네임스페이스를 더럽히지 않는다. 또한 prototype을 이용하여 프로토타입 체이닝을 구현해서 선언된다.
객체지향 프로그래밍에서 클래스간 관계는 크게 두 가지로 나누어 볼 수 있다.
상속은 말 그대로 부모에게서 자식이 받는 것이다. 자식 클래스가 부모 클래스를 상속받게 되면 부모 클래스에 존재하는 메소드나 변수를 사용할 수 있게 되어 코드의 재사용성을 높인다. 포함관계는 클래스 안에 다른 클래스를 변수로 선언하는 것이다.
상속은 클래스 계층을 만들어 코드 중복을 줄이는 객체지향 프로그래밍의 방법이다. 상속 관계에서 부모 클래스를 기반 혹은 슈퍼 클래스 라고 하며 상속받는 클래스를 파생 혹은 서브 클래스 라고 한다.
class 학생 {...}
class 대학생 extends 학생{...}
이처럼 상속은 extends 키워드를 이용하여 선언한다. 타입스크립트에서는 클래스의 단일 상속만을 지원함으로 하나의 부모 클래스만을 상속받을 수 있다. 또한 자식 클래스가 부모 클래스를 상속 받을 때는 자식 클래스의 생성자 함수에서 super메소드를 이용하여 부모 클래스의 생성자 함수를 호출해 줘야 한다.
class 학생 {...}
class 대학생 extends 학생{
constructor(){
super()
}
}
포함관계는 클래스가 다른 클래스를 포함하고 있는 관계이다. 이 포함 관계는 크게 두 가지로 구분할 수 있다.
합성관계는 전체가 부분을 포함하여 강한 관계라고도 한다.
class 공격 {...}
class 캐릭터{
private attack;
constructor(){
this.attack = new 공격();
}
}
const newChar = new 캐릭터();
이를 강한 관계라고 하는 이유는 클래스가 객체로 생성될 때 같이 생성되어 인스턴스가 재할당되어 참조되지 않으면 자신을 포함한 클래스와 같이 사라지는 수명주기를 같이 하는 특성 때문이다.
집합 관계는 합성관계와 비슷하지만 약한 관계라고한다.
class 공격 {...}
class 캐릭터{
private attack;
constructor(attack){
this.attack = attack;
}
}
const attAct = new 공격()
const newChar = new 캐릭터(attAct);
합성과 달리 외부에서 공격클래스로 생성된 객체를 전달하고 있다. 때문에 캐릭터 클래스의 인스턴스를 재할당하더라도 공격클래스의 인스턴스는 사라지지 않기 때문에 수명주기를 같이 하지 않는 약한관계라고 할 수 있다.
class EnergyBolt {
constructor(public power){}
}
class Magician{
constructor(public intelligence:number){ }
checkPower():number{
return this.intelligence
}
}
class iceMagician extends Magician {
energyBolt : EnergyBolt;
constructor(public intelligence:number,public type:string){
super(intelligence);
this.energyBolt = new EnergyBolt;
}
checkPower (){return this.intelligence}
getMagicType (){return this.type}
}
마법사 클래스를 상속받은 얼음 마법사클래스, 그리고 에너지 볼트 클래스를 얼음 마법사 클래스와 합성시킨 코드이다.
ES6의 클래스는 접근제한자를 제공하지 않는다. 반면 타입스크립트에서는 접근 제한자를 제공한다.
접근 제한자의 구분은 개발 정도이다. public은 개방 정도가 가장 크기 때문에 자식 클래스와 객체를 통한 접근이 가능하고, protected는 자식 클래스만, private는 현재 클래스만 접근 가능하다.
public제한자는 클래스 외, 내부에서 모두 접근이 가능하게 공개하는 제한자이다. public제한자로 변수나 메소드를 선언하면 객체 내부, 외부 상속도 가능하다.
class 공격 {
public attack = 0
}
class 캐릭터 extends 공격{
public intelligence = 10;
public attAct (){
return this.intelligence + this.attack
}
}
const char = new 캐릭터;
char.attAct() //10
private 제한자는 클래스 내부에서 접근 할 수 있지만 외부에서 접근이 불가능한 제한자이다.
class 공격 {
private attack = 0
}
class 캐릭터 extends 공격{
private intelligence = 10;
private attAct (){
return this.intelligence + this.attack //접근 가능
}
}
const char = new 캐릭터;
char.attAct() // 접근 불가
생상자의 매개변수에 접근 제한자를 추가하면 매개변수 속성이 되어 바로 멤버 변수가 되는 효과가 있다.
class Rectangle{
public x:number;
public y:number;
constructor(x:number,y:number){
this.x = x;
this.y = y;
}
}
원래라면 이런 식으로 선언 했지만 생성자의 매개변수에 제한자를 선언하여 더 간결하게 코드를 쓸 수 있다.
class Rectangle{
constructor(public x:number,public y:number){}
getArea(){
return this.x * this.y
}
}
이렇게 제한자를 생성자의 매개변수에 선언함으로 따로 내부 변수로 사용이 가능해진다.
protected는 외부 접근을 제한하지만 상속 관계에서 부모 클레스에 protected로 선언된 메서드나 변수의 접근은 허용합니다.
class 공격 {
protected attack = 0
}
class 캐릭터 extends 공격{
protected attAct (){
return this.intelligence + this.attack //접근 가능
}
}
const char = new 캐릭터;
char.attAct() // 접근 불가
상속 관계가 있을 때 자식클래스에서 부모 클래스의 메소드나 변수를 사용할 수 있는 방법은 super 메소드와 this를 사용하는 것입니다. super키워드는 부모 클래스의 공개 요소만 접근이 가능하고 this는 부모 클래스의 상속받은 내용과 현재 클래스 모두 접근할 수 있습니다.
class food {
constructor(public Ingredients:string);
private salt:number = 0;
set Salty(gram:number){this. salty += gram};
get Salty(){return this.salt}
protected getIngredients(){
return this.Ingredients;
}
}
class strowbarryCake extends food{
constructor(public Ingredients){
super(Ingredients);
}
getInfo(){
this.Ingredients = "strowbarry"
console.log(
super.getIngredients(), super.Ingredients //"strowbarry" undefined
this.getIngredients(), this.Ingredients // "strowbarry" "strowbarry"
super.Salty() // 0
)
}
}
strowbarryCake.getInfo()
위에서 볼 수 있듯이 super키워드를 통해서 부모 클래스의 생성자를 불러오지만, 부모 클래스의 멤버 변수를 직접 호출해 가져 올 수 없다. 부모 클래스의 멤버 변수를 가져오려면 get으로 선언된 getter 를 통해서 가져와야한다.
기본 접근 제한자란 접근 제한자 선언을 생략할 때 적용 된다. 기본 접근 제한자를 적용할 수 있는 대상은 클래스 멤버 변수, 메소드, get/set프로퍼티 , 생성자 매개변수 이다.
class food {
public salt:number = 0;
public set Salty(gram:number){this. salty += gram};
public get Salty(){return this.salt}
constructor(Ingredients:string ="딸기", protected name:string="딸기케이크" ,readonly price:number=6500 ){};
public getIngredients(){
return this.Ingredients; // 접근 불가
}
public getName(){ return [this.name,this.price] }
}
위에서 볼 수 있듯이 기본 접근 제한자는 public이며 생성자 매개변수의 경우 제한자가 생략되면 생성자 내부에서만 접근이 가능하다. 그러나 제한자나 readonly가 붙으면 멤버 변수가 된다. 이처럼 생성자 매개변수의 제한자를 생략하면 private의 기능을, readonly는 읽기 전용 속성으로 만들지만 public처럼 상속이나 외부 접근도 허용한다. 생성자 매개변수를 제외한 나머지 요소의 제한자를 생략하면 public으로 된다.
추상 클래스는 구현 메소드와 추상 메소드가 동시에 존재할 수 있다. 구현 메소드는 구현 내용이 있는 메소드이며, 추상 메소드는 선언만 된 메소드이다. 추상 클래스는 미완성된 추상 메소드를 포함하기 때문에 불완전한 클래스이며 따라서 추상 클래스 단독으로 객체를 생성할 수는 없다.
abstract class 추상클래스{
abstract 추상메소드();
abstract 추상멤버변수:타입;
public 구현메소드():void{
사용 로직;
this.추상메소드() //호출 가능
}
}
추상 클래스는 abstract 키워드를 사용해서 선언하며 static이나 private 키워드는 사용할 수 없다. public과 protected는 사용할 수 있다. 미구현 추상 메소드가 존재함으로 자식 클래스에서는 추상 메소드를 구현해야한다. 추상 멤버도 마찬가지로 자식 클래스에서 선언해줘야한다. 구현 메소드에서는 추상 멤버나 메소드를 호출할 수 있다.
따라서 abstract가 추가된 추상 메소드나 멤버변수는 자식 클래스에서 구현할 수 있게 public으로 선언해줘야한다.
추상 클래스에서 선언한 추상 메소드는 오버라이딩(자식 클래스에서 부모 클래스의 메소드를 같은 이름의 메소드를 선언)하여 자식 클래스에서 구현한다.
class 자식 클래스 extends 추상클래스{
public 추상멤버변수:타입;
public 구현메소드():void{
사용 로직;
}
}
이렇게 청사진 같은 추상 클래스는 선언하고 세부 기능은 구현 클래스에서 구현하는 패턴을 템플릿 메소드 패턴이라 한다.
인터페이스는 자바스크립트가 지원하지 않는 타입스크립트만의 특징이다. 인터페이스는 타입이며 컴파일 후에는 사라진다. 인터페이스는 선언만 존재하며, 멤버 변수와 메소드를 선언할 수 있지만, 접근 제한자는 선언할 수 없다.
interface car{
speed:number
}
이 인터페이스는 extends키워드를 통해서 부모 인터페이스를 상속받아 확장할 수 있습니다. 클래스와는 다르게 다중 상속을 받을 수 있다.
interface Attack{
power:number
}
interface Move{
way:string
}
interface char extends Attack,Move{
name: string
}
const warrior = <char>{}
다중 상속을 받을 때 같은 이름의 메소드를 상속받으면 상속 받는 인터페이스에서 같은 이름의 메소드를 모두 재정의해야한다.
interface car{
run():void
getspeed():{runningSpeed:number}
}
interface airplane{
fly():void
getspeed:{flySpeed:number}
}
interface carPlane extends car,airplane{
run():void;
flt():void;
getSpeed():{runningSpeed:number,flySpeed:number}{
return{runningSpeed:10,flySpeed:15}
}
}
자바스크립트의 객체는 구조를 고정할 수 없으며 쉽게 변화한다. 때문에 유지보수, 안전성을 위하여 고정할 필요가 있다. 인터페이스를 이용하면 객체의 구조를 고정할 수 있다.
interface Icar{name:string};
class Mycar{};
let mcar:Icar = {name:"bumblebee'}
인터페이스는 컴파일 시 사라진다. 인터페이스의 용도는 클래스와 객체의 타입 안전성을 확보하는 것 이기 때문에 컴파일시 검사가 끝나면 사라진다.
클래스는 객체 리터럴의 타입으로 사용할 수 있다. 배열의 요소들이 객체 리터럴 이라면 배열 타입을 선언 할 때 클래스를 사용할 수 있다.
배열의 요소로 객체가 올 수 있다. 이때 타입을 지정하지 않으면 배열의 요소로 사용될 객체의 구조가 임의의 형태로 변경될 수 있다.
배열의 요소가 객체 리터럴이라면 배열 타입을 선언할 때 배열의 요소의 타입을 객체 리터럴로 지정하여 타입 안전성을 강화할 수 있다.
let person:{name:string,city:string}[];
person = [
{name:"a",city:"r"}
{name:"q",city:"f"}
{name:"z",city:"b"}
]
배열의 타입을 선언함에 따라 배열 내부 요소의 타입을 고정하여 타입 안전성을 높이었다. 또한 요소 타입에 맞춰서 선언하기 힘들다면 타입 엘리어스를 사용한다.
type personInfo = {name:string,city:string}
let person:personInfo[];
person = [
{name:"a",city:"r"}
{name:"q",city:"f"}
{name:"z",city:"b"}
]
타입스크립트에서 선언된 타입은 컴파일시간까지 유요하다가 컴파일 후 사라진다.
배열 타입을 사용할 때 요소타입으로 기본 타입 뿐 아니라 클래스도 선언할 수 있다. 클래스로 타입을 선언할 시 클래스 구조와 동일한 객체를 배열의 요소로 받을 수 있다.
class Person{
public fullInfo;
constructor(public name:string, public city:string){
this.fullInfo = `${name} (${city})`
}
}
const person:Person = [
new Person("둘리","서울"),
new Person("도우너","어서오고")
]
인터 페이스는 객체 리터럴을 정의하는 타입으로 사용할 수 있다.
interface Person{
name:string;
city:string;
}
const person: Person ={name:"",city:""}
또한 배열의 요소가 객체 리터럴이라면 인터페이스를 이용하여 선언할 수 있다.
interface Person{
name:string;
city:string;
}
const person: Person[] =[{name:"",city:""}]
인터페이스는 클래스의 구조를 정의하기도 하면서 자바스크립트의 객체 구조를 정의하기 한다. 이처럼 인터페이스는 다양한 역할을 하는데 익명함수의 함수타입을 정의할 수도 있다.
interface format{
(data:string,toUpper?:boolean):string
}
const forms:format = function (data:string,toUpper:boolean){}
const forms2:format = function (info:string,isUpper:boolean){}
이때 인터페이스에서 선언한 매개변수의 이름과 함수에서 선언한 매개변수의 이름이 정확히 일치하지 않아도 된다.
오버라이딩이란 부모 클래스에 정의된 메소드를 자식 클래스에서 상속받아 새로 구현하는 것을 말한다. 부모 클래스에 오버라이딩 될 메소드를 오버라이든 메소드가 존재한다. 이를 자식 클래스에서 오버 라이딩 한 메소드를 오버 라이딩 메소드라고 한다. 이때 몇 가지 조건이 있다.
위의 조건에 따라 예시를 적어 보자.
class Bird{
flight (distance:number=0){} //오버라이든 메소드
}
class Eagle extends Bird{
flight(distance2:number){} // 오버라이딩 메소드, 매개변수명은 달라도 된다.
}
flight(distance:any) / flight(distance2:number) > 상위타입이라 가능
flight(distance:number) / flight(distance2:any) > 오버라이딩 메소드의 매개변수 타입이 any라 가능
flight(distance:any,speed:number) / flight(distance2:number) > 오버라이든 메소드의 매개변수가 같거나 많으면 가능
이번에 봤던 오버로딩이란 메서드 이름은 같지만 매개변수의 타입과 개수를 다르게 정의하는 방법을 말한다.
class Bird{
flight (distance:number=0){}
}
class Eagle extends Bird{
flight(distance2:number):void // 같은 타입을 포함해야한다.
flight(distance2:stirng):void
flight(distance2:any):void{
if(typeof distance2 ==="number"){}
else if(typeof distance2 ==="string"){}
else{}
}
}
위 코드에서 any는 모든 타입의 최상위 타입이라 다 받아 줄것 같지만 사실 string아니면 number밖에 못받는다. 때문에 위 코드를 유니언 타입으로 간결하게 바꿀 수 있다.
class Bird{
flight (distance:number=0){}
}
class Eagle extends Bird{
flight(distance2?:string|number):void{
//매개변수의 갯수가 달라질수있으니 선택으로 바꿈
if(typeof distance2 ==="number"){}
else if(typeof distance2 ==="string"){}
else{}
}
}
오버로딩 매소드 임으로 내부에서 매개변수의 타입을 검사해 처리를 해준다.
프로그램이에서 다형성이란 여러 타입을 받아 여러 형태를 갖는 것을 말한다. 타입스크립트에서는 크게 세 가지 다형성이 있다.
자식 클래스가 부모 클래스를 상속할 때 부모 클래스를 타입으로 갖는 객체 참조 변수에 자식 클래스가 할당 되는 경우
class Bird{
flight (distance:number=0){}
}
class Eagle extends Bird{
flight(distance2:number):void // 같은 타입을 포함해야한다.
flight(distance2:stirng):void
flight(distance2:any):void{
if(typeof distance2 ==="number"){}
else if(typeof distance2 ==="string"){}
else{}
}
}
const gugu:Bird = new Eagle()
이런 경우 할당은 자식 클래스를 받더라도 동작은 부모 클래스를 기준으로한다. 따라서 부모 클래스의 메소드에는 접근 가능하지만 자식 클래스에 선언된 멤버 변수에는 접근할 수 없다. 하지만 오버라이딩 된 메소드의 경우 부모 클래스의 메소드가 아닌 자식 클래스의 메소드가 호출된다. 이렇게 런타임에 호출이 결절되는 것을 런타임 다형성이라고 한다. 위와 같은 경우는 추상 클래스일 경우에도 적용된다.
abstract Bird{
flight (distance:number=0){}
}
class Eagle extends Bird{
flight(distance2:number):void{
if(typeof distance2 ==="number"){}
else if(typeof distance2 ==="string"){}
else{}
}
}
const gugu:Bird = new Eagle()
클래스가 인터페이스를 구현할 때 해당 인터페이스를 타입으로 갖는 객체 참조 변수가 구현클래스의 객체를 참조함으로 다형성을 띈다.
interface IPerson {
name:string;
getAge():number;
getCity : ()=>string
}
class Teacher implements Iperson{
name: number;
getAge():number{return 26}
getCity= ()=>"hanam"
getHobby() : string{return "fifa4"}
}
const umo : Iperson = new Teacher();
umo.getHobby() // 접근 불가
클래스의 다형성과 비슷하다. 할당은 Teacher로 되었지만 타입 선언이 IPerson으로 되었기 때문에 Teacher의 멤버 메소드에 접근할 수 없다.
매서드의 매개변수 타입을 유니언으로 이용하면서 객체가 다형성의 성질을 띄도록 만들 수 있다.
class Monitor{
display(data: string|number){}
}
display 메소드는 string이나 nubmer타입을 받아들인다. 때문에 내부에서 타입검사를 수행하는 타입 가드를 추가해야한다. 이와 비슷하게 매개변수의 타입을 클래스로 줄 때 유니온 타입으로 줌으로 instanceof로 타입 가드를 추가해야한다.
class Monitor{
display(data: Led|Oled){
if(Monitor instanceof Led){}
else if(Monitor instanceof Oled){}
}
}
이렇게 코드를 짜다 보면 유니언 타입에 클래스가 추가 될 때마다 타입 가드가 추가되는 번거로운 상황이 된다. 이를 개선하기 위해서 인터페이스를 이용하여 다형성을 구현해준다.
인터페이스는 해당 인터페이스를 상속받는 여러 클래스들의 타입을 받을 수 있다. 때문에 클래스를 타입으로 받지 않고 클래스가 사용하는 인터페이스를 타입으로 받으면서 타입 가드를 사용하지 않게 된다.
interface Mointor{
getName():string;
}
class Led implements Monitor{}
class Oled implements Monitor{}
class Qled implements Monitor{}
class MonitorDisplay{
display(monitor:Monitor){
const myMonitor:Monitor = monitor
return myMonitor.getName()
};
}
자바스크립트에서는 객체의 멤버에 접근하는 방법으로 ES6의 getter와 setter를 지원한다. getter를 접근자라 하며 setter는 설정자 라 한다. 타입스크립트에서는 클래스 내부의 get과 set메소드를 이용하여 getter와 setter를 선언할 수 있다.
class Student{
name : string;
birthDay : number
}
const element = new Student();
element.name="";
element.birthDat = 951012
// 값 설정
console.log(element.name, element.birthDat);
// 읽기
위의 자바스크립트 코드에서 값 설정이 setter, 읽기가 getter가 된다.
class Student{
private studentName:string;
get name():string{return this.studnetName}
set name(name:string):void{
if(name.indexOf("happy")!==0){
this.studentName=name;
}
}
}
위 타입스크립트에서 멤버 변수에 접근하기 위해서는 name이라는 getter함수를 호출해야하며 name을 설정하기 위해서는 name setter함수를 선언해서 할당해줘야한다. 멤버 변수를 외부에서 접근할 수 없도록 private 키워드를 사용했기 때문에 Get접근자와 Set접근자를 사용해야한다.
타입스크립트에서는 static키워드를 지원한다. static키워드는 클래스에 정적 멤버 변수나 정적 메서드를 선언할 때 사용할 수 있는데, 객체 생성없이 접근이 가능함으로 메모리의 절약 효과가 있다.
class Computer{
static cpu="4690"
}
Computer.cpu // 접근 가능
이런 정적 변수의 외부 접근 차단을 위해선 앞에 private 접근 제한자를 선언해줄 수 있다.
class Computer{
private static cpu="4690"
}
Computer.cpu // 접근 불가능
class Computer{
private state graphicCard : number= "150";
static cpuPower:number=50;
static isPowerOk(power:number):boolean{
if((this.graphicCard + this.cpu)*2 >power){
return true
}
return false
}
get cpu():number{
return Computer.cpu
}
static set cpu(power:number){
Computer.cpu=power;
}
}
위와 같이 정적 멤버 변수는 객체로 만들지 않고 사용이 가능하다.
readonly가 선언된 변수는 초기화되면 재할당이 불가능하다. const처럼 상수선언으로 사용할 수 있어 비슷한 것 같지만 차이점이 있다. readonly는 타입스크립트의 키워드이며 값의 재할당이 가능하다. 또 인터페이스의 멤버 변수나 클래스의 멤버 변수, 객체 리터럴의 속성 등에 사용할 수 있다.
interface Icount{
readonly count:number;
}
class TestReadOnly implements Icount{
readonly count : number
}
const info : {readonly name :string } = {name : "백승일"}
readonly의 동작을 알아보자.
interface Icount{
readonly count:number;
}
class TestReadOnly implements Icount{
readonly count : number
static readonly count1 : number
private readonly count4 : number
readonly count3 : number = 0;
getCount(){
this.count4 = 1 // 불가능 readonly는 재할당이 불가능하다.
}
}
const info : {readonly name :string } = {name : "백승일"}
컴파일 후
class TestReadOnly {
constructor(){
this.count3 = 0;
}
getCount(){
}
}
const info = {name : "백승일"};
컴파일 결과 타입과 인터페이스가 사라지며 readonly로 선언되었지만 초기화되지 않은 멤버 변수들도 같이 제거가 된다. 즉 readonly는 컴파일 때 까지만 유효하다.
readonly는 객체 리터럴 타입의 속성을 고정하는 데 사용할 수 있다.
const info : {readonly name :string } = {name : "백승일"}
info 변수에