[타입스크립트] 객체지향 알아보기

hoonie·2021년 8월 1일
0

타입스크립트

목록 보기
4/5
post-thumbnail

이번시간에는 매우 중요한 개념인 객체지향에 대해 알아보도록 하겠습니다!!

프로그래밍에는 절차지향적과 객체지향적이라는 개념이 존재합니다.

절차지향이란?

코드를 작성했을때 위에서 아래로 순차적으로 처리가 되는것입니다. 스크립트 내 메인 함수가 있는데, 그 메인 함수 안에서 다른 함수를 호출하고 또 그안에서 다른 함수들이 호출이 되며, 그 함수들 안에서는 전역변수들의 데이터에 접근을 할 수가 있습니다. 이런식으로 전체가 유기적으로 연결이 되어있는 프로그래밍 기법이 바로 절차지향언어입니다. (정의된 순서대로 함수가 하나씩 호출되는 기법)

장점

  1. 속도가 빠름

단점

  1. 함수와 데이터들이 여러가지가 얽혀있기때문에, 코드가 복잡해지면 복잡해질수록 전체적인 코드를 이해하기 어려움 -> 즉, 유지 보수 및 확장성이 매우 떨어짐

객체지향이란?

프로그램을 객체로 정의해서 객체들끼리 의사소통하게끔 코딩하는것을 말합니다. 즉, 서로 관련있는 데이터와 함수들을 오브젝트들로 만들어서 정의하고 프로그래밍하는 기법입니다.

장점

  1. 어느 한곳에서 문제가 생긴다면, 관련있는 객체만 수정하면 되기때문에 유지보수가 매우 편리함
  2. 반복해서 사용이 필요한 데이터가 필요할때 재사용성이 매우 우수함
  3. 다시 필요한 경우 다시 만들기도 편해서 확장면에서도 좋음

단점

  1. 처리속도가 절차지향보단 느린편
  2. 처음에 설계시 시간적인 리소스가 더 들어가는 편

객체지향에서 매우 중요한 4가지 개념

1. 캡슐화

절차지향에서는 관련되어있는 여러가지 함수와 데이터들이 흩어져 있는데, 이러한것들을 캡슐화 하는것임 ( 즉, 관련있는 함수와 데이터를 한곳에 담아놓고 외부에서 그 정보를 볼 수 없도록 캡슐화 하는것입니다. )

2. 추상화

내부의 복잡한 기능을 다 이해하지않고 외부에서 간단한 인터페이스로 쓸 수 있는것. 즉, 내부에서 얼마나 복잡한 코드로 구현되어있던 상관없이 외부에서 함수하나만 사용해서 객체를 쉽게 쓰도록 해주는 것

3. 상속성

관련된 클래스를 만들었을때 부모의 클래스로 부터 상속을 받아 그 기능을 사용 할 수 있음. 예를들어, 자동차를 만들어내는 클래스를 만들었고 자식 클래스로 내연기관 자동차와 전기자동차를 만들어내는 클래스를 만들었을시, 자동차 만드는 공통적인 메서드를 상속받아서 사용 할 수 있음.

4. 다형성

자동차 만드는 클래스의 함수를 이용할때 내연기관 자동차를 만드는것인지 전기 자동차를 만드는것인지 상관없이 그냥 공통적으로 쓸 수 있는 여러곳에서 사용 할 수 있는 공통된 함수에 접근 하는것


코드로 예시보기

간단하게 커피를 제조하는 함수로 절차지향적 방법과 객체지향적 방법을 비교해보겠습니다

1.절차지향적

	//커피의 1샷을 내릴때 필요한 커피콩의 용량
  const COFFE_BEAN_PER_ONE_SHOT:number = 7;
  
  // 현재 가지고 있는 커피콩 용량
  let coffebean:number = 0;
  
  //함수 리턴 타입 지정
  type Coffe = {
    shots:number,
    hasMilk:boolean
  }
  
  //커피 만드는 함수 생성
  function makeCoffee(shots:number):Coffe {
  //만약 현재 가지고 있는 커피콩 용량이 주문들어온 커피를 제조하기 위한 커피콩 용량보다 적을 경우 에러 리턴
    if(coffebean < shots * COFFE_BEAN_PER_ONE_SHOT) {
      throw new Error("커피콩이 부족해요!!")
    }
    
    //커피 제조가 되면 그만큼 현재 가지고 있는 커피콩 용량 차감
    coffebean -= shots * COFFE_BEAN_PER_ONE_SHOT;
    
    //리턴 타입 만든대로 리턴
    return {
      shots,
      hasMilk:false
    }
  }

	//3개의 샷을 내릴 수 있는 커피콩 용량 생성
  coffebean += 3*COFFE_BEAN_PER_ONE_SHOT
  
  console.log(makeCoffee(2))
  console.log(makeCoffee(1))
  
  //위 함수에서 현재 가지고 있는 커피콩을 다 소진했기에 마지막 함수에선 에러 리턴
  console.log(makeCoffee(1))

2. 객체지향적


type Coffe = {
    shots:number,
    hasMilk:boolean
  }

  class CoffeMaker {
    //class 레벨 -> 클레스마다 연결이 되어있기때문에 오브젝트 생성때마다 생기지 않음. 클래스 자체에 만든 변수
      static COFFE_BEAN_PER_ONE_SHOT:number = 7;
      //instance 레벨 변수
      coffebean = 0;

      //생성자만들때 들어오는 매개변수를 인스턴스 변수에 할당
      constructor(coffebean:number) {
        this.coffebean = coffebean
      }
      
      makeCoffe(shots:number):Coffe {
        if(this.coffebean < shots*CoffeMaker.COFFE_BEAN_PER_ONE_SHOT) {
          throw new Error('커피 부족!!!')
        }
        this.coffebean -= CoffeMaker.COFFE_BEAN_PER_ONE_SHOT * shots
        return {
          shots,
          hasMilk:false
        }
      }
  }

  const coffe1 =new CoffeMaker(21);
  console.log(coffe1.makeCoffe(3))
    console.log(coffe1.makeCoffe(1))

CoffeMaker 클래스 안에 static을 선언해서 만든 변수가 있는데, COFFE_BEAN_PER_ONE_SHOT이라는 변수는 무조건 고정되어야하는 변수라 새로운 생성자를 호출하여 인스턴스를 만들고 변수를 생성할때 차지하는 메모리를 줄이기 위해서 클래스 레벨로 만든것입니다. static으로 만든 class 레벨 변수에 접근을 하기 위해선 this. 으로 접근하는 것이 아닌 클래스명.변수명 으로 접근해야합니다!

추가적으로, 객체지향에서는 은닉화 개념이 매우 중요합니다.
때문에 외부에서 접근하는것을 막고 중요한 데이터를 변경하지 못하게 하는 과정이 필요합니다. 위 객체지향식에서 은닉화를 추가한 코드를 살펴보겠습니다.


type Coffe = {
    shots:number,
    hasMilk:boolean
  }

  class CoffeMaker {
    //class 레벨 -> 클레스마다 연결이 되어있기때문에 오브젝트 생성때마다 생기지 않음. 클래스 자체에 만든 변수
      private static COFFE_BEAN_PER_ONE_SHOT:number = 7;
      //instance 레벨
      private coffebean = 0;

      //생성자만들때 들어오는 매개변수를 인스턴스 변수에 할당
      private constructor(coffebean:number) {
        if(coffebean<0) {
          throw new Error('커피콩 갯수는 양수로 입력되어야합니다.')
        }
        this.coffebean = coffebean
      }

      static makeMachine(coffebean:number) {
        return new CoffeMaker(coffebean)
      }
      
      makeCoffe(shots:number):Coffe {
        if(this.coffebean < shots*CoffeMaker.COFFE_BEAN_PER_ONE_SHOT) {
          throw new Error('커피 부족!!!')
        }
        this.coffebean -= CoffeMaker.COFFE_BEAN_PER_ONE_SHOT * shots
        return {
          shots,
          hasMilk:false
        }
      }
  }

//은닉화를 위해 기본 생성자 constructor를 private으로 만들고 makeMachine이라는 statice 선언한 함수에 return 생성자를 하였음

const coffe1 = CoffeMaker.makeMachine(25)
console.log(coffe1.makeCoffe(3))

객체지향의 접근제한자 종류로 public, private, protected라는 것이 있습니다! public은 누구다 더 접근이 가능하며 default값이 public이기 때문에 아무것도 적지 않으면 public인것입니다.
private와 protected로 설정하게되면 인스턴스명.변수명 이런식으로 접근하려고 할때 자동완성이 뜨질 않습니다. 때문에 외부에서 접근이 불가능하죠.

private vs protected

  • private는 상속을 받은 자식클래스에서도 접근을 못함, 하지만 protected로 만들시 자식클래스에서는 접근이 가능

또 여기서 추상화라는 개념도 추가 할 수 있습니다.

추상화란 복잡한 내부적으로 복잡한 코드가 구성되어있어도 그걸 알지 못하게하고 간단하게 식하나로 복잡한 내부 식을 다 사용하는 것이라 보면 됩니다.

코드를 살펴보겠습니다


type Coffe = {
    shots:number,
    hasMilk:boolean
  }

  class CoffeMaker {
    //class 레벨 -> 클레스마다 연결이 되어있기때문에 오브젝트 생성때마다 생기지 않음. 클래스 자체에 만든 변수
      private static COFFE_BEAN_PER_ONE_SHOT:number = 7;
      //instance 레벨
      private coffebean = 0;

      //생성자만들때 들어오는 매개변수를 인스턴스 변수에 할당
      private constructor(coffebean:number) {
        if(coffebean<0) {
          throw new Error('커피콩 갯수는 양수로 입력되어야합니다.')
        }
        this.coffebean = coffebean
      }

      static makeMachine(coffebean:number) {
        return new CoffeMaker(coffebean)
      }
      
      private grindBeans(shots:number) {
        console.log(`${shots*CoffeMaker.COFFE_BEAN_PER_ONE_SHOT} 만큼 커피콩 가는중`)
        if(this.coffebean < shots*CoffeMaker.COFFE_BEAN_PER_ONE_SHOT) {
                  throw new Error('커피 부족!!!')
                }
      }
      private preHeat() {
        console.log("데우는중")
      }
      private extract(shots:number):Coffe {
        console.log("추출완료 ")
        this.coffebean -= CoffeMaker.COFFE_BEAN_PER_ONE_SHOT * shots
        return {
                  shots,
                  hasMilk:false
                }
      }

      makeCoffe(shots:number):Coffe {
        this.grindBeans(shots);
        this.preHeat();
        this.extract(shots);
        return this.extract(shots)
      }
  }

const coffe1 = CoffeMaker.makeMachine(25)
console.log(coffe1.makeCoffe(3))

기존에 되어있던 makeCoffe 함수안에 복잡한 식으로 되어있던것을 세분화하여 3개의 함수로 쪼갰습니다. 그 3개의 함수는 은닉화가 되어있으며 사용자입장에서는 저 함수의 존재를 알지 못합니다. 오로지 makeCoffe 라는것과 그 결과물만 알 수 있죠. 이것이 바로 추상화입니다. 콩을갈고 데우고 추출하는 과정을 몰라도 되는거죠. 오로지 그냥 결과만 알면됩니다. 이러한 개념이 바로 추상화입니다.


그다음은 상속을 만들어보겠습니다.

상속은 부모의 클래스의 기능을 물려받는 것을 말합니다

예를들어 라떼를 생성하는 커피메이커를 만들어보겠습니다.


type Coffe = {
    shots:number,
    hasMilk:boolean
  }

  class CoffeMaker {
    //class 레벨 -> 클레스마다 연결이 되어있기때문에 오브젝트 생성때마다 생기지 않음. 클래스 자체에 만든 변수
      private static COFFE_BEAN_PER_ONE_SHOT:number = 7;
      //instance 레벨
      private coffebean = 0;

      //생성자만들때 들어오는 매개변수를 인스턴스 변수에 할당
      constructor(coffebean:number) {
        if(coffebean<0) {
          throw new Error('커피콩 갯수는 양수로 입력되어야합니다.')
        }
        this.coffebean = coffebean
      }

      static makeMachine(coffebean:number) {
        return new CoffeMaker(coffebean)
      }
      
      private grindBeans(shots:number) {
        console.log(`${shots*CoffeMaker.COFFE_BEAN_PER_ONE_SHOT} 만큼 커피콩 가는중`)
        if(this.coffebean < shots*CoffeMaker.COFFE_BEAN_PER_ONE_SHOT) {
                  throw new Error('커피 부족!!!')
                }
      }
      private preHeat() {
        console.log("데우는중")
      }
      private extract(shots:number):Coffe {
        console.log("추출완료 ")
        this.coffebean -= CoffeMaker.COFFE_BEAN_PER_ONE_SHOT * shots
        return {
                  shots,
                  hasMilk:false
                }
      }

      makeCoffe(shots:number):Coffe {
        this.grindBeans(shots);
        this.preHeat();
        this.extract(shots);
        return this.extract(shots)
      }

      clean() {
        console.log("기계 청소중")
      }
  }


//상속받은 라떼메이커
class LatteMaker extends CoffeMaker {
  steam():void {
    console.log("스팀중")
  }

  makeCoffe(shots:number):Coffe {
    const coffe = super.makeCoffe(shots)
    this.steam();
    return {
      ...coffe,
      hasMilk:true
    }
  }
}

const latte = new LatteMaker(21)
console.log(latte.makeCoffe(3))

위 코드에서 class LatteMaker를 추가하여 CoffeMaker의 기능을 상속받았습니다.

상속받은 기능을 쓰기 위해 부모의 클래스에 접근하기 위해선 super. 라는 접근자로 접근을 하셔야합니다. 또한 상속을 받기 위해선 부모의 생성자가 private로 되면 안되기때문에 public이나 procted로 바까줘야합니다.

steam() 이라는 함수도 추가하여 라떼 클래스만의 함수도 구현이 가능합니다.


마지막으로, 다형성이라는 개념을 입힌 코드를 작성해보겠습니다.

다형성을 입혀서 내부적으로 구현된 다양한 클래스들이 한가지의 인터페이스를 구현하거나 동일한 부모 클래스를 상속했을때 동일한 함수를 어떤 클래스인지 구분하지않고 공통된 api를 호출하여 사용 할 수 있다는 것입니다.

바로 코드를 살펴보겠습니다


type Coffe = {
    shots:number,
    hasMilk:boolean,
    hasSugar?:boolean
  }

  interface ICoffe {
    makeCoffe(shots:number):Coffe
  }

  class CoffeMaker implements ICoffe {
    //class 레벨 -> 클레스마다 연결이 되어있기때문에 오브젝트 생성때마다 생기지 않음. 클래스 자체에 만든 변수
      private static COFFE_BEAN_PER_ONE_SHOT:number = 7;
      //instance 레벨
      private coffebean = 0;

      //생성자만들때 들어오는 매개변수를 인스턴스 변수에 할당
      constructor(coffebean:number) {
        if(coffebean<0) {
          throw new Error('커피콩 갯수는 양수로 입력되어야합니다.')
        }
        this.coffebean = coffebean
      }

      static makeMachine(coffebean:number) {
        return new CoffeMaker(coffebean)
      }
      
      private grindBeans(shots:number) {
        console.log(`${shots*CoffeMaker.COFFE_BEAN_PER_ONE_SHOT} 만큼 커피콩 가는중`)
        if(this.coffebean < shots*CoffeMaker.COFFE_BEAN_PER_ONE_SHOT) {
                  throw new Error('커피 부족!!!')
                }
      }
      private preHeat() {
        console.log("데우는중")
      }
      private extract(shots:number):Coffe {
        console.log("추출완료 ")
        this.coffebean -= CoffeMaker.COFFE_BEAN_PER_ONE_SHOT * shots
        return {
                  shots,
                  hasMilk:false
                }
      }

      makeCoffe(shots:number):Coffe {
        this.grindBeans(shots);
        this.preHeat();
        this.extract(shots);
        return this.extract(shots)
      }

      clean() {
        console.log("기계 청소중")
      }
  }

class LatteMaker extends CoffeMaker {
  constructor(beans:number,public serialNumber:string) {
    super(beans)
  }
  steam():void {
    console.log("스팀중")
  }

  makeCoffe(shots:number):Coffe {
    const coffe = super.makeCoffe(shots)
    this.steam();
    return {
      ...coffe,
      hasMilk:true
    }
  }
}

class SweetCoffeeMaker extends CoffeMaker{
  makeCoffe(shots:number) {
    const coffe = super.makeCoffe(shots)
    return {
      ...coffe,
      hasSugar:true
    }
  }

}

const machines:ICoffe[] = [
  new CoffeMaker(21),
  new LatteMaker(34,'234'),
  new SweetCoffeeMaker(20)
]

machines.forEach((machine)=>{
  console.log("-----------------")
  machine.makeCoffe(3)
})

위코드처럼 내부적으로 관련된 일반 커피를 만드는 클래스, 라떼를 만드는 클래스, 달콤한 커피를 만드는 클래스를 구현된 인터페이스 배열로 받아서 반복문을 돌려 공통된 방식을 다양성 있게 만드는 것입니다.


타입스크립트를 공부하면서 객체지향에 대해 좀 더 깊게 공부하게 되었는데, 개념들이 아직 명확하게 세워지지 않아 헷갈리는 면도 많은 것 같습니다.
구글링을 하여 사전적 정의를 봐도 이해가 안가는것이 많았으며, 직접 코딩을 해가면서 천천히 익혀가기 위한 노력을 하였는데, 아직 숙달이 되지 않아 좀 어려운 개념인것 같습니다. 지속적인 연습을 통해 제것으로 만들어야겠습니다

감사합니다

0개의 댓글