코드스테이츠_S2U2_5W_수,목

윤뿔소·2022년 9월 21일
0

CodeStates

목록 보기
18/47

오늘은 객체 지향 프로그래밍(OOP, Object-oriented programming)에 대해 배워보겠다.

객체 지향

  • 순차적인 명령의 조합의 절차 지향 프로그래밍(C, 포트란 등)과 다르게 데이터와 기능을 한번에 묶어 처리
  • 현대의 언어들 대부분이 가지고 있음(Java, C++, C# 등), JS는 객체 지향 언어는 아니지만 패턴을 가지고 있어 작성 가능
  • 예 : "이번에 만들 자동차는 빨간색의 최고 속력은 200km/h를 내도록 만들어보자!" => 속성: 빨강, 최고속력 200km/h, 기능: 색을 지정될, 최고속력을 내는 함수들

객체 지향의 특징

  • OOP는 객체로 그룹화가 되고, 4가지 대표적인 개념을 통해 재사용 가능!
    • Encapsulation (캡슐화)
    • Inheritance (상속)
    • Abstraction (추상화)
    • Polymorphism (다형성)

Encapsulation (캡슐화)

  • 데이터와 기능을 하나의 단위로 묶는 것
  • 은닉(hiding): 구현은 숨기고, 동작은 노출시킴
    • 내부 데이터나 내부 구현이 외부로 노출되지 않도록 만드는 것
    • 즉! 디테일(구현, 데이터 등)을 숨기고, 객체 외부에서 필요한 동작(메소드)만 노출시켜 유지•보수에 용이하게끔 만듦(메소드 구현만 수정해 코드 흐름을 유지, setter-getter를 지정해 더 엄격하게)
    • JS는 솔직히 잘 안쓰고 ⭐️클로저 모듈 패턴을 많이 씀
  • 느슨한 결합(Loose Coupling)에 유리: 언제든 구현을 수정할 수 있음
    • 코드 실행 순서에 따라 절차적으로 코드를 작성하는 것이 아니라, 코드가 상징하는 실제 모습과 닮게 코드를 모아 결합하는 것을 의미
    • 마우스로 예를 들면 스위치 누르고, 전기 신호가고...의 절차가 아니라, 마우스의 속성(상태)를 정하고 클릭, 이동 등을 메소드(기능)로 설정해 인스턴스의 기능을 상상하게끔 만듦
  • 결론 : 코드가 복잡하지 않게 만들고, 재사용성 높임

Inheritance (상속)

  • 부모 클래스의 특징을 자식 클래스가 물려받는 것 (다른 말로 기본(base)클래스와 파생(derived)클래스)
  • 예 : 사람과 직업 특성 중 학생
    • 사람은 기본적으로 이름, 성, 나이 등을 속성으로 갖고, 자거나 먹는 등의 행위를 함, 여기서 학생을 업으로 띄는 사람이 있다면 속성에 학년, 과목 등과 배우는 행위를 추가할 수도 있음
    • 추가적인 클래스(학생)이 발생했으므로 처음부터 구현하기엔 비효율적, 그래서 OOP를 써 상속받게 해 추가에 용이
  • 결론 : 역시 불필요한 코드를 줄여 재사용성 높임

Abstraction (추상화)

  • 내부 구현은 아주 복잡한데, 실제로 노출되는 부분은 단순하게 만드는 개념
    • 인터페이스 단순화 : 전화기를 사용할 때 버튼만 눌러도 기능을 사용가능하게!
    • 차이와 용이함 : 캡슐화의 은닉화는 본질이 숨기는 것에 있는 반면, 추상화는 인터페이스를 단순화하여 사용자가 쓰는 기능을 최소화해 예기치 못한 사용상 변화를 예방
  • 결론 : 캡슐화와 마찬가지로 코드가 복잡하지 않게 만들고, 단순화된 사용으로 변화에 대한 영향을 최소화

Polymorphism (다형성)

  • 객체 지향 패턴은 '다양한 형태'를 가질 수 있음, 다른 방식으로 구현
    • 즉! 하나의 객체가 여러 가지 타입을 가질 수 있는 것
    • 객체의 메소드들이 다양한 타입을 가질 수 있고 절차라면 모든 타입 분기마다 조건을 걸어줘야하지만 OOP의 메소드라면 특성에 맞게 구현 가능하다?!
  • 결론 : 다형성으로 인해 동일한 메소드라도 조건 대신 객체의 특성에 맞게 구현 가능

객체 지향의 한계점

+ 보강책으로 TS도 나왔으니 함 보기!

  • 은닉화의 private 키워드 : 클래스 내부에만 쓰이는 속성, 메소드를 구별짓기 위해 사용되는 키워드
    • JS에선 지원되는 브라우저가 적어 잘 안쓰임 - TS, Java에는 있어 TS로 보강, 아니면 클로저
      // TypeScript 문법
      class Animal {
      private name: string; // JS에선 #이 있지만 지원 브라우저 적음
      constructor(theName: string) {
        this.name = theName;
      }
      }
      new Animal("Cat").name; // 사용 불가
      // Property 'name' is private and only accessible within class 'Animal'. 
  • 추상화의 interface 키워드 : 인터페이스 키워드를 따로 써 인터페이스만의 클래스 구축을 용이하게 만드는 키워드
    - "이 클래스는 메서드 이름이 의도한 바대로 작동할 것이다"
    - 실질적인 구현 방법을 공개하지 않고 사용법을 노출시키기에도 유리, API처럼 외부 공개용 모듈을 만들 때 빛을 발함
    • JS에선 존재하지 않는 키워드 - TS로 보강
      // TypeScript 문법
      interface ClockInterface {
      currentTime: Date;
      setTime(d: Date): void;
      }
      class Clock implements ClockInterface {
      currentTime: Date = new Date();
      setTime(d: Date) {
       this.currentTime = d;
      }
      constructor(h: number, m: number) {}
      }

클로저 모듈 패턴

단순 객체를 사용한 클로저 모듈은 이렇다.
⭐️주의 : 메소드 호출 방식을 쓸 땐 화살표 함수 금지 - this, 인수, new 등을 쓸땐 쓰면 안됨

let counter1 = {
  value: 0,
  increase: function() {
    this.value++ // 메서드 호출을 할 경우, this는 counter1을 가리킵니다
  },
  decrease: function() {
    this.value--
  },
  getValue: function() {
    return this.value
  }
}

counter1.increase()
counter1.decrease()
counter1.getValue() // 0

문제점은 이제 똑같은 기능을 쓰기위함과 다른 결괏값을 지정하고 싶을 때 counter2라는 게 생길 수도 있다. 그러면 또 2에 같은 기능을 또 써줘야한다. 여기서 객체 지향이 나온다.

function makeCounter() {
  let value = 0;
  return {
    increase: function() {
      value++;
    },
    decrease: function() {
      value--;
    },
    getValue: function() {
      return value;
    }
  }
}
let counter1 = makeCounter()
counter1.increase()
counter1.getValue() // 1
let counter2 = makeCounter()
counter2.decrease()
counter2.decrease()
counter2.getValue() // -2

그 전 코드와 다르게 makeCounter라는 클로저가 담긴 모듈 패턴 함수를 써서 변수 선언 및 할당만 해주면 쓸 수 있는 간편함이 생겼다!

클래스 문법

위의 사진에서 청사진(원형 객체, original form)은 클래스, 만들어낸 객체를 인스턴스라고 부른다. 이제 클래스 문법에 대해 공부해보겠다!파스칼 케이스 : 일반 명사로 대문자를 이용해 만듦
참고 : ES5까지는 class 문법이 따로 없고 함수 선언식을 통해 OOP를 했음, ES6부터 클래스, 생성자, new 등 직접적인 문법이 개발됐음

  • class 키워드 : class를 통해 기본적인 '생성자 함수'를 만듦, 참고로 함수는 리턴값이 없음
  • this.property : 함수가 실행될 때, 해당 스코프마다 생성되는 고유한 실행(execution) context
  • constructor : '생성자 함수'가 실행될 때의 초깃값
class Car {
  // 기본 생성자 함수, 필수임, 없어도 나중에 추가하면 컴파일러가 추가 해줌
  constructor(brand, name, color) {
    this.brand = brand;
    this.name = name;
    this.color = color;
  }
  // 메소드 정의
  refuel(~) { 
    // 부족한 연료 채워 넣는 기능 
  }
  drive(~) { 
    // 운전 시작하는 기능 
  }
}

인스턴스

  • new 키워드 : class의 사례임, class(함수)를 만들고 변수에 할당해 인스턴스를 만드는 과정에서 new를 써서 새 인스턴스를 만듦
  • 실행 컨텍스트가 없을 때 new로 인스턴스를 생성했다면 그게 this값이 됨
let avante = new Car("hyundai", "avante", "black");
avante.color; // 'black'
avante.drive(); // (메소드) 아반떼가 운전 시작

let mini = new Car("BMW", "mini", "white");
mini.brand; // 'BMW'
mini.refuel(); // 미니에 연료를 공급

참고 : 배열

사실 우리는 배열을 하며 객체 지향 개념을 마스터했다!

// 배열 정의 - let arr = ['rhino', 'yoon', 'zzang'] : Array의 인스턴스 창조와 동일
let arr = new Array('rhino', 'yoon', 'zzang')
arr.length; // 3
// 새 엘리멘트 추가 : push-메소드의 정의 앞에 Array.prototype이 붙어있음, push는 Array의 객체라면 쓸 수 있음
arr.push('baby') 

+ 정리

ES5 문법

function Car(brand, name, color) { // class, constructor 無
  this.brand = brand;
  this.name = name;
  this.color = color;
}
// prototype을 통해 원형 객체를 만들어 메소드 정의
Car.prototype.refuel = function() {
  // 연료 공급을 구현하는 코드
}
Car.prototype.drive = function() {
  // 운전을 구현하는 코드
}
// 인스턴스에도 new 無

실습 : 클로저 모듈 패턴 때의 예제를 가져옴

class Counter {
  // 생성자 호출을 할 경우, this는 new 키워드로 생성한 Counter의 인스턴스임
  constructor() {
    this.value = 0; // let value = 0;
  }
  increase() {
    this.value++
  }
  decrease() {
    this.value--
  }
  getValue() {
    return this.value
  }
}
let counter1 = new Counter() // 생성자 호출
counter1.increase()
counter1.getValue() // 1

Prototype

코딩애플, 코딩앙마

  • 프로토타입 : '원형 객체', 걍 다 떼놓고 '유전자'임, prototype이 써져있으면 같은 유전자의 객체 복제가 된다는 뜻, 상속의 느낌
    • 인간 A를 복제하면 똑같은 복제인간 A, B, ... 등을 만듦
    • 인간 A는 복제인간들의 '프로토타입', 복제인간들은 '인스턴스'라고 할 수 있음
    • 물론 복제인간을 복제한 복제복제인간이 있으면 복제인간은 복제복제인간의 '프로토타입'임, 즉! 모든 객체는 프로토타입.
  • JS는 사실 클래스라는 개념이 없어 기존의 객체를 복사해 다른 객체를 생성하는 프로토타입 기반 언어임
    • 즉! 서로의 주소를 .__proto__(prototype Link)라는 메소드와 .prototype 메소드로 참조 후 복사
    • .__proto__는 조상의 유전자를 찾아 떠나는 link를 발견하게함, 부모 속성을 조회
      -.prototype는 복제된 자신과 후손을 만드는 느낌? 아 이거 좀 헷갈려서 영상으로 개념 잡고 가자

프로토타입의 클래스

  • 실습
class Human {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  sleep() {
    console.log(`${this.name}은 잠에 들었습니다`);
  }
}

let kimcoding = new Human('김코딩', 30);
// 실습 : .__proto__와 .prototype의 구조
Human.prototype.constructor === Human; // 1. true
Human.prototype === kimcoding.__proto__; // 2. true
Human.prototype.sleep === kimcoding.sleep; // 3. true
  1. Human 클래스의 생성자 함수는 Human
  2. Human 클래스의 프로토타입은 Human 클래스의 인스턴스인 kimcoding의 __proto__
  3. Human 클래스의 sleep 메서드는 프로토타입에 있으며, Human 클래스의 인스턴스인 kimcoding에서 kimcoding.sleep으로 사용할 수 있음
  • 과정: 무조건 기억
  • 배열도 마찬가지로 클래스임, 메소드

프로토타입의 체인

  • JS에서 상속을 구현할 때 프로토타입 체인을 이용함, 특정 객체의 프로퍼티나 메소드에 접근시 객체 자신의 것뿐 아니라 __proto__가 가리키는 링크를 따라서 자신의 부모 역할을 하는 프로토타입 객체의 프로퍼티나 메소드를 접근
  • 즉! 체인은 __proto__로 검색하여 부모 역할의 프로토타입 객체의 속성, 메소드를 나타낸 역사를 뜻함!
  • 모든 프로토타입 체이닝의 종점은 Object.prototype
  • 그래서 이 체인이 존재함을 알고 __proto__으로 구조를 알고, 그 부모의 자식을 만들고 싶다면 extends을 써서 복사, super 로 호출(메소드, 속성 불러오기)을 쓰는 것이다!

실습

배웠던 DOM 구조에도 프로토타입이 존재함!

  • let div = document.createElement('div');의 구조를 살펴보자
  • 위의 사진을 보면 HTMLDivElement - ... - Node - EventTarget으로 나온다!(이벤트 수신기, 이벤트를 받아주는 그릇이라는 뜻, 이벤트 핸들러, 리스너 비슷)
  • 즉! div를 만든 DOM인 EventTarget부터 HTMLDivElement까지 조상들은 Object이고 __proto__는 그 조상들을 찾을 수 있다!

과제

Beesbeesbees라는 페어 과제를 수행

  • src 디렉토리 안에는 클래스가 존재합니다.
    • constructor, super, extends, class 키워드를 이용하여 구현
    • 상속과 관련된 키워드 super, extends는 공식문서를 읽어본 후 적용
  • 각각의 클래스마다 여러 개의 테스트가 작성되어 있지만, 따라서 작성된 순서대로 테스트를 진행, 현재 테스트가 통과해야 그다음 테스트를 진행
  • Grub.js 파일부터 작성을 시작, Grub은 다른 모든 Bee의 기반
├── Grub
│   └── Bee
│       ├── HoneyMakerBee
│       └── ForagerBee

인 구조로 되어있음! 즉, 체인을 경험해보고, 각각을 캡슐화하고 상속받아 그 다음 벌에게 주고 기능을 또 추가하는 그런 과정을 그림

코드

class Grub {
  //  constructor() {
  //    this.age = 0;
  //    this.color = "pink";
  //    this.food = "jelly";
  // 디폴트 파라미터로 테스트 + 클래스 문법을 다 지킬 수 있음(원래 생성자에 파라미터 없고 age에 할당된 모습이어서 이상했음)
  constructor(age = 0, color = "pink", food = "jelly") {
    this.age = age;
    this.color = color;
    this.food = food;
  }
  eat() {
    return `Mmmmmmmmm ${this.food}`;
  }
}
class Bee extends Grub {
  constructor(age = 5, color = "yellow", food, job = "Keep on growing") {
    super();
    this.age = age;
    this.color = color;
    this.job = job;
  }
}
class ForagerBee extends Bee {
  constructor(age = 10, job = "find pollen", canFly = true, treasureChest) {
    super();
    this.age = age;
    this.job = job;
    this.canFly = canFly;
    this.treasureChest = [];
  }
  forage(treasure) {
    return this.treasureChest.push(treasure);
  }
} // Test에 foragerBee.forage('pollen') ... 등으로 인수를 줌

알게된 것

  • extends는 아들을 만들었으니 프로토타입 기반의 키워드구나! 그래서 복사된 객체의 메소드를 다 가져온다!
  • extendsBeeGrub의 아들로 만듦 : 아하! 이제 ForagerBee.__proto__를 붙이면 부모인 Bee가 나오겠구나!
  • 그다음 super()로 요소를 데려오는 구나, 거기에 재할당(Overriding)도 가능하구나!
  • super()는 자식의 클래스에서 쓰이고, 그 말은 즉슨 extends로 불러왔으면 super()는 필수구나! 그래서 super() 이전의 this를 쓰면 생성자 함수를 호출할 수 없구나!@
  • ⭐️중요! super() 인자를 넣으면 순서에 맞춰 인자들을 배치해줘야한다. 그런데 상속을 많이 하게 되면 중첩되는 프로토타입들이 많아져 순서를 맞추는 게 힘들 수도 있다. super()로 일단 다 불러 온 다음에 덮어 쓰는 번거로움이 있어도 super() 안에 필요한 것만 쏙쏙 골라서 this로 배정해주자! 오히려 더 순서 맞추는 게 일이니까 일단 가져온 다음에 원하는 값은 다시 this 이용해서 덮어 씌워라!
  • this는 인스턴스를 만들 때 그 값을 불러오고 덮어쓸려고 쓰는 경우구나!

참고: 구조

class Human {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  sleep() {
    return `${this.name}이(가) 잠을 잡니다.`;
  }
}
class Student extends Human {
  constructor(name, age, grade) {
    super(name, age);
    this.grade = grade;
  }
  study(num) {
    this.grade = this.grade + num;
    return `${this.name}의 성적이 ${num}만큼 올라 ${this.grade}이 되었습니다.`;
  }
}
class ForeignStudent extends Student {
  constructor(name, age, grade, country) {
    super(name, age, grade);
    this.country = country;
  }
  speak() {
    return `${this.country} 언어로 수업을 진행합니다.`;
  }
}

let americanStudent = new ForeignStudent("jake", 23, 80, "미국");
americanStudent.study(20) // 'jake의 성적이 20만큼 올라 100이 되었습니다.'
americanStudent.sleep() // 'jake이(가) 잠을 잡니다.'
americanStudent.speak() // '미국 언어로 수업을 진행합니다.'

(Object.prototype) - Human(name, age, sleep()) - Student(super(name, age), grade, study(num)) - ForeignStudent(super(name, age, grade), country, speak())구조로 Class가 상속 후 americanStudent가 인스턴스로 발행!

잡설

엄청 어렵다. 객체지향이 이래서 핫하구나,, 가히 프론트 언어의 꽃이라봐도 무방하다. 객체 지향을 이용한 모듈화가 핵심인 거 같으니 공부 열심히 해서 마스터 해야겠다. 특히 나중에 프레임워크도 배울 텐데 알아놔야 그때 잘 할 수 있을 거 같다.

profile
코뿔소처럼 저돌적으로

0개의 댓글