[타입스크립트 (3)] OOP(Object Oriented Programming)

SeHoony·2021년 9월 24일
1

TypeScript

목록 보기
3/7
post-thumbnail

앞선 내용에서 타입스크립트의 Type에 대해 소개했다. 이번에 소개할 내용은 OOP(객체지향형 프로그래밍)이다. 이 내용을 토대로 타입스크립트 삼대장인 '컴파일시 오류 체크', 'Type', 'OOP' 개념들의 간을 모두 보게 된다.

1. OOP study flow

들어가기에 앞서, OOP 개념의 흐름을 구상해보았다. 이 챕터에서는 OOP 개념의 흐름을 개괄적으로 보고, 이후의 챕터에서 예시와 함께 다루도록 하겠다.

  1. 기초구상 ---> 2. 캡슐화(private, public) ---> 3. 추상화(abstract, interface) ---> 3-1. Interface : 기능 한정적 제시 기능, 접근 제한 기능 ---> 4. 상속(수직) ---> 5. 다형성 ---> 6. Composition(수평)

2. Basic Conception(기초 구상)

기초적인 구상에서는 만들고자 하는 class의 기본적인 부품들을 결정한다.
이번 예제에서는 Car 클래스를 구현해본다. field는 'oil, distance', method는 'goForward', 'makeCar'로 한다.

class Car {
  leftOil = 40				// 보유한 기름
  distanceUntilNow = 10			// 주행거리
  oilPerKilometer = 10			// 1km 당 소비되는 기름
  
  constructor(oil: number) {
    this.leftOil += oil
  }
  
  goForward(distance: number): void {
    if (this.leftOil < distance * this.oilPerKilometer) {
      throw new Error("oil shortage!!!")
    }
    this.leftOil -= distance * this.oilPerKilometer
    this.distanceUntilNow += distance
    console.log(`go ${distance}`)
  }
  
  makeCar(oil: number) {
    return new Car(oil)
  }
}

3. 캡슐화(Encapsulation)

캡슐화의 Key포인트는 '데이터'라고 생각한다. 데이터를 현실세계와 최대한 유사하도록 보장하는 것이 캡슐화라고 생각한다. 캡슐화에서 가장 중요한 행위는 데이터에 private, public, protected 등 접근제한자를 통해 정보를 은닉하는 것이다.

또한 데이터가 클래스에 종속적일 경우, static을 붙여준다. 하단에 makeCar의 경우, car class에 의해 생성되는 객체에 종속되지 않기 때문에 static을 붙여주었다.

마지막으로 getter & setter를 통해 데이터 무결성(getter) 및 값을 넣어줄 때(setter), 유효성 검사가 가능하도록 할 수 있다.

class Car {
  // 보유한 기름은 차 밖에서는 알 수 없으니까, private으로 하여 외부에서 접근할 수 없도록 한다.
  private leftOil = 40					  
  private distancUntilNow = 10
  private static oilPerKilometer = 10
  
  // getter & setter  
  // distanc의 데이터 무결성을 위해 getter 설정을 했고, setter의 경우 외부에서 distance 값을 넣어줄 경우는 없다고 판단하여 뺐다.
  
  get distance() {
    return this.distancUntilNow
  }
  
  // constructor
  private constructor(private oil: number) {
    this.leftOil += oil
  }
  
  // method
  goForward(distance: number): void {
    if (this.leftOil < distance * Car.oilPerKilometer) {
      throw new Error("oil shortage!!!")
    }
    this.leftOil -= distance * Car.oilPerKilometer
    this.distancUntilNow += distance
    console.log(`go ${distance}`)
  }
  static makeCar(oil: number) {
    return new Car(oil)
  }
}

4. 추상화(Abstract)

옛날부터 이 '추상화'라는 말이 참 어려웠다. 과장해서 설명하면, 진짜 나를 설명하기 위해서는 머리부터(머리카락 23452가닥, 이마 15cm, 눈 흰자 60%, 검은자 32%, 실필줄 3%, 오른쪽 눈 밑에 점 뺀 자국 등등) 발끝(두번째 발톱 없음 등등)까지 세세하게 설명하는 것이 맞다. 하지만 이렇게 설명하는 사람은 아무도 없다. 나의 인상착의 중 남과 다른 부분이나 특이점이 있는 부분을 골라서 "추상적으로" 설명하게 된다.

이처럼 객체지향 프로그래밍에서도 객체를 생성할 때, class의 전체 field나 method 중에서 꼭 필요한 부분들만 골라(추상화)서 나타낼 수 있는 방법을 고안해냈다. 이러한 방법의 두 축이 Abstract, Interface이다.

4-1. Interface

Interface의 기능으로 기능의 한정적 제시 기능, 접근 제한의 기능을 든다. 이는 이러한 용도로 사용한다는 것을 예로 든 것이니 외울 것은 아니다.

// LigthCar와 SportsCar 인터페이스를 따로 두어서, SportsCar 인터페이스에는 두 메서드를 더 추가했다.
interface LightCar {
  distance: number
  goForward: (dist: number) => void
}

interface SportsCar {
  distance: number
  makeNoise: () => void
  drift: () => void
  goForward: (dist: number) => void
}

class Car {
  private leftOil = 40
  private distancUntilNow = 10
  private static oilPerKilometer = 10
  get distance() {
    return this.distancUntilNow
  }
  // constructor
  private constructor(private oil: number) {
    this.leftOil += oil
  }
  // method

  makeNoise() {
    console.log("삐용삐용삐용")
  }

  stop() {
    console.log("stop")
  }

  drift() {
    if (this.leftOil < 50) {
      throw new Error("Oil Shortage for Drift!")
    }
    this.leftOil -= 50
    console.log("부와아아아앙")
  }

  goForward(distance: number): void {
    if (this.leftOil < distance * Car.oilPerKilometer) {
      throw new Error("oil shortage!!!")
    }
    this.leftOil -= distance * Car.oilPerKilometer
    this.distancUntilNow += distance
    console.log(`go ${distance}`)
  }

  static makeCar(oil: number) {
    return new Car(oil)
  }
}

const myCar: SportsCar = Car.makeCar(100)
const yourCar: LightCar = Car.makeCar(100)

// myCar는 drift, makeNoise 메서드에도 접근할 수 있지만, yourCar는 goForward 함수에만 접근할 수 있다. 이처럼 interface의 메서드 및 필드를 달리하여 interface 별로 한정적으로 기능을 제시할 수 있다.
// 또한 interface를 통해, 자격, 레벨에 따라 접근을 제한하도록 구현할 수도 있다.

class Amateur {
  constructor(private car: LightCar) {}
  race() {
    this.car.goForward(5)
  }
}
class Professional {
  constructor(private car: SportsCar) {}
  race() {
    this.car.makeNoise()
    this.car.goForward(5)
    this.car.drift()
  }
}

const sehoon = new Professional(myCar)
sehoon.race()
const kihoon = new Amateur(yourCar)
kihoon.race()

5. 상속(Inhabitance)

상속은 말그대로 부모가 자식에게 주는 것이다. 세대가 지날 수록 부가 축적되듯이, 자식이 부모보다 가진 것이 많다. 폭삭 망해서 자식이 가진 것이 없는 경우는 생각하지 않는 것으로 하자.

상속에서 주목할 것은 '수직적'인 성격을 가졌다는 것이다. 부모가 가진 필드와 메서드를 자식에서 수직적으로 넘겨준다. 이 때, 자식 class에서는 extends라는 키워드로 부모 class를 상속한다.

* TIP : 상속 시, 부모 class의 constructor는 private이면 안된다. 또한 자식 class에서 constructor에 부모 class의 parameter를 동일하게 받을 때는 super 키워드를 넣어준다. 하단을 확인해보자.

위의 코드 밑에 하단의 코드를 추가한다.

class SuperCar extends Car {
  constructor(oil: number) {
    super(oil)
  }

  boost() {
    console.log("부아아아아앙아아아아앙 야아아아아아")
  }

// override
  goForward(distance: number): void {
    super.goForward(distance)
    this.boost()
  }
}

6. 다형성(Polymorphism)

다형성은 자식 Class에서 상속받은 메서드들을 필요에 따라 변화시킬 수 있는 것을 의미한다.

자동차는 다양하지만, 주유소에서 따꿍열고 기름 넣는 것은 모두 똑같다.
이처럼 다형성은 여러 다른 클래스들을 하나의 공통적인 행위를 통해, 동시에 기능할 수 있도록 하는 것이 주목적이라고 생각한다.

const Cars: Car[] = [new Car(55), new SuperCar(33), new Car(55), new SuperCar(123)]
Cars.forEach((car) => {
  car.goForward(1)
})

7. Composition(수평적)

Inheritance(수직적)를 보완하는 개념이다.
Car가 있다면, supercar, tank 등등 여러 자동차 객체를 상속으로만 구현하는데는 무리가 있다. 이것을 보완하기 위해 Composition이 있다.

// Car class의 constructor 수정
constructor(private oil: number, private engine: Engine, private wheel: Wheel) {
    this.leftOil += oil
  }
  
//.... ....

// interface
interface Engine {
  power: number
  boostUp: () => void
}

interface Wheel {
  size: number
}

// class
class CheapEngine implements Engine {
  power = 1

  boostUp() {
    console.log(`more fast*${this.power}`)
  }
}

class NormalEngine implements Engine {
  power = 5

  boostUp() {
    console.log(`more fast*${this.power}`)
  }
}

class LuxuryEngine implements Engine {
  power = 50

  boostUp() {
    console.log(`more fast*${this.power}`)
  }
}

class CheapWheel implements Wheel {
  size = 5
}
class NormalWheel implements Wheel {
  size = 10
}
class ExpensiveWheel implements Wheel {
  size = 20
}

// Engine Objects
const cheapEngine = new CheapEngine()
const normalEngine = new NormalEngine()
const luxuryEngine = new LuxuryEngine()

// Wheel Objects
const cheapWheel = new CheapWheel()
const normalWheel = new NormalWheel()
const expensiveWheel = new ExpensiveWheel()

// car Objects
const cheapCar = new Car(5, cheapEngine, cheapWheel)
const noramalCar = new Car(33, normalEngine, normalWheel)
const luxuryCar = new Car(44, luxuryEngine, expensiveWheel)

위의 예시처럼 car 부품으로써 engine, wheel interface를 만들고, interface를 토대로 여러 engine class및 wheel class를 만든다. 그리고 만들고자 하는 car 객체에 맞는 engine 객체, wheel 객체를 쓰면 된다.

8. Conclusion!

Car class를 통해서 OOP에 대해 개괄적으로 살펴보았다.
OOP를 공부하면서, 프로그래밍의 지향점은 현실을 완벽히 복사하는 것이 아닐까라는 생각을 했다. 현실의 여러 객체들을 컴퓨터 프로그래밍으로 표현하기 위한 OOP이기 때문이다. 따라서 공부의 방식도 현실에 존재하는 객체를 객체지향형 프로그래밍으로 구현해보는 것이 많이 도움될 거 같다.
또한 예제로 든 Car class는 너무 허접하기 떄문에, 기회가 된다면 꼭 더 나은 예제를 만들어보고 싶다.

profile
두 발로 매일 정진하는 두발자, 강세훈입니다. 저는 '두 발'이라는 이 단어를 참 좋아합니다. 이 말이 주는 건강, 정직 그리고 성실의 느낌이 제가 주는 분위기가 되었으면 좋겠습니다.

0개의 댓글