코드스테이츠/코드스테이츠 @ 개발 복습

[코드 스테이츠] 41일차, "6주차 복습 (1) - 객체지향, 프로토타입, 재귀함수"

Je-chan 2021. 8. 28. 22:57


[ 오늘의 TODO ]

  1. 코드 스테이츠) 월~수 내용 복습
    // 객체지향
    // 재귀함수
  2. 패스트 캠퍼스) 인강 3개 이상 듣기 // optional
    // 요즘 복습해야할 게 너무 많아서 안 돼
  3. 스터디 그룹) 프로그래머스 문제 풀기 (~월)
    // 하노이의 탑
    // 기능 구현
  4. 생활) 물 1L 이상 마시기
  5. 생활) 수-토-일 운동 

 


[ 오늘의 복습 ]

1. 객체 지향

  1) Class, Instance

  클래스는 자동차 설계도, 인스턴스는 자동차 설계도로 만든 자동차라고 생각하면 좋을 것 같다. 

 

  자동차 설계도인 클래스를 만들 때는 일반적으로 함수 정의하듯이 만들 수 있다. 이때 키워드는 일반 함수 정의하듯이 function으로 적어줄 수 있으나, ES6 문법 이래 class 라는 키워드로 클래스를 생성할 수 있게 되었다. 함수의 매개변수로는 인스턴스를 만들 때, 인스턴스를 구성할 속성(요소)을 의미한다.

function Car (brand, style, color) {
  this.brand = brand
  this.name = name
  this.color = color 
 }

// ES6 이후
class Car {
  constructor (brand, style, color) {
    this.brand = brand
    this.name = name
    this.color = color 
  }
}

  이때 속성이란 차 브랜드, 차 바디의 모형, 색깔 등 정적인 내용이다. 하지만, 차는 운전, 기어 변속 등의 동적으로 작동되는 기능이 존재한다. 그런 것들을 여기에서는 메소드라고 한다. 메소드는 따로 위의 매개변수처럼 지정해주지 않고 바로 함수를 만든다. 이때, 클래스를 만들 때 class 키워드를 만들었을 때와 만들지 않았을 때의 차이가 존재한다

function Car (brand, style, color) {
  this.brand = brand
  this.name = name
  this.color = color 
  
  Car.prototype.drive = function () { 운전 기능 구현 }
  Car.prototype.changeGear = function () { 기어 변속 기능 구현}
 }

// ES6 이후
class Car {
  constructor (brand, style, color) {
    this.brand = brand
    this.name = name
    this.color = color 
  }
  drive () { 운전 기능 구현 }
  changeGear () { 기어 변속 기능 구현 }
}

  이제 클래스라는 자동차 설계도가 만들어졌다. 그러면 실제 인스턴스라는 자동차를 만들어 보자. 만들 때는 변수를 선언하고 할당 new 라는 키워드를 사용하여 클래스를 이용해 만든 객체를 할당해준다. 

let phantom = new Car('Rolls-Royce', 'sedan', 'white')
console.log(phantom.color) // 'white'
phantom.drive ( ) // 팬텀이 운전을 시작

let v60 = new Car('Volvo', 'wagon', 'gray')
v60.brand // 'Volvo'
v60.changeGear // v60이 기어를 변속함

  new 를 통해서 새로운 객체를 생성해주겠다는 의미고 new 다음에는 function을 사용하듯이 인자 값들을 부여 한다. 이렇게 class나 new라는 키워드는 새로운 객체를 생성하는데 이런 키워드를 생성자 함수라고 한다. 생성자 함수는 다른 프로그래밍 언어에서도 종종 등장한다. 

 

 

  2) 객체 지향

  객체 지향이 있기 전에는 절차 지향 언어들이 있었다. C나 포트란 등이 그런 언어인데 코드에 적힌 순서대로 명령을 실행하는 언어들이다. 그러나 객체 지향 언어는 순차적으로 작동하는 것이 아니다. 데이터에 대한 접근과 데이터 처리 과정을 한 번에 처리한다. 객체라는 그룹 내에서 데이터와 기능, 속성과 메소드가 존재한다. 

 

  그렇다면 왜 객체 지향을 사용하는가? 첫 번째 이유로는 절차 지향보다 객체 지향이 사람의 사고와 유사한 방식으로 이어지기 때문이다.

 만약에 누군가 "너 뭐하다 연락 받았어?" 라는 질문을 한다면

절차 지향은 "나는 처음에 밥을 먹기 위해 숟가락을 들었어. 숟가락으로 밥을 푸었어. 내 입에 가지러 가려고 했어. 그런데 너에게 연락이 왔어. 숟가락을 내렸어. 그리고 너의 연락을 받았어" 이렇게 말을 해야 한다.

하지만 객체 지향이라면 "나 지금 밥 먹고 있었어" 라고 한 번에 말하면 된다. 그 말 안에 저 절차적인 내용을 가시적으로 정확하게 확인할 수는 없지만 저런 절차가 진행됐음을 유추할 수 있게 된다. 

  두 번째 이유로는 수정을 할 때 객체 지향은 직관적으로 어느 함수에서 수정해야할 지 파악할 수 있기 때문이다. 

 

  물론, 절차지향이 더 좋을 때도 존재한다. 절차 지향은 메모리 사용이 상대적으로 적고 처리 속도도 상대적으로 빠르기 때문이다. 다만, 객체 지향은 재활용성, 코드 이해, 디버깅 의 면에서 절차 지향보다 높은 성능을 지닌다.

 

  그렇다면 객체 지향의 특징은 어떻게 되는지 살펴보자.

 

 

    1) 캡슐화 (Encapsulation)

  데이터(속성)과 기능(메소드)를 따로 정의하는 것이 아니라 하나의 객체 안에 묶는 것을 의미한다. 느슨하게 결합된다는 것은, 코드 실행 순서에 따라 절차적으로 코드를 작성하는 것이 아니라, 코드가 상징하는 실제 모습과 닮게 코드를 모아 결합한다는 것을 의미한다.

 

  예를 들어 키보드가 작동되는 원리를 코드로 작성해보자.

  자판이 눌린다 ⇒ 신호가 생긴다 ⇒ 전선을 타고 흐른다

  이렇게 나열하는 것이 절차 지향이다. 하지만 위의 모든 과정을 묶어 키보드의 상태를 prototype 으로 정하고 자판이 눌리는 걸 method로 정해서 코드만 보고도 이 객체의 기능을 상상할 수 있게 작성하는 것이 느슨한 결합을 추구하는 코드 작성법이다.

 

  이렇게 작성하면 은닉(hiding)(구현은 숨기지만 동작되는 결과는 보일 수 있게 하는 것) 이 가능하다. 은닉의 좋은 점은 내부 데이터나 내부 구현이 외부로 노출되지 않도록 할 수 있다는 점이다. 

    2) 상속 (Inheritance)

  부모 클래스의 특징을 자식 클래스가 물려 받는 것을 의미한다.

 

  예를 들어서 클래스를 만들 때 사람이라는 클래스가 있다고 하자. 사람이라는 클래스에는 기본적으로 이름, 나이, 성별 이라는 속성과 말하다, 먹다 라는 메소드가 부여될 것이다. 이 사람이라는 클래스를 통해서 나라고 하는 사람의 클래스를 생성한다고 하면 저 사람이라는 클래스의 모든 속성들을 가져온 후, 다른 사람과 구별되는 고유의 속성과 메소드들이 추가하면 된다.

 

  만약 상속이 없다면 같은 내용임에도 불구하고 일일이 다 작성해야하는 불편함이 존재한다. 그러나 상속이라는 특성이 있기에 사람이라는 클래스의 속성과 메소드들을 나라는 클래스가 상속으로 모두 받아오면 된다.

    3) 추상화 (Abstraction)

실제로 노출되는 부분을 단순하게 만든다는 의미다. 우리가 보는 스마트폰은 단순한 직사각형의 모양이지만 그 내부는 매우 복잡하게 구성돼서 우리가 스마트폰 사용할 때 사용하는 기능들을 구현한다. 하지만, 실제 우리가 스마트폰을 받아들이는 방식처럼 그냥 스마트폰 화면을 손가락으로 이용해 누르는 것으로 기능들을 활용하는 방향으로 인터페이스를 단순화한다. 인터페이스란 클래스를 정의할 때 메소드와 속성만 정의한 것을 의미한다. 이 인터페이스가 추상화의 본질이다. 너무 많은 기능이 노출되지 않았을 때는 예기치 못한 사용상의 변화가 일어나지 않도록 막을 수 있다. 이 특징때문에 캡슐화와 추상화를 헷갈려하는 경우가 많다. 그러나 각 특징의 포커스는 명확히 다르다

 

캡슐화는 코드나 데이터의 은닉에 포커스가 맞춰진다 (어떻게 보면 개발자의 관점)

추상화는 사용하는 사람이 필요하지 않은 메소드를 노출시키지 않고, 기능들을 단순한 이름으로 정의하는 것에 포커스가 맞춰져있다. (어떻게 보면 사용자들의 관점) 

  4) 다형성 (Polymorphism)

  아까 전에 사람이라는 클래스로 말하다라는 메소드를 일반 개개인의 사람들이 상속 받았다. 하지만, 사람들이 말을 할 때 100% 동일한 목소리를 내지 않는다. 말소리를 낸다는 메소드의 본질적인 기능은 구현하지만 사람마다 각기 다른 목소리를 낸다. 이것이 다형성이다. 객체도 마찬가지로 같은 메소드라 해도 다른 방식으로 구현될 수 있다.

 

 

  3. prototype

  자바 스크립트를 프로토타입 기반 언어라고 부른다. 객체와 메소드가 속성을 상속 받기 위해서 프로토타입 객체를 가진다. 객체는 생성될 때 prototype이 자동 생성된다. 그리고 메소드들은 우리가 위에서 일반 function으로 클래스를 만들어줬을 때 처럼 prototype 안에 메소드들이 저장된다. 이 prototype 내용들은 chain 이 가능하고 이를 통해 상속이 이뤄질 수 있게 만든다. 참조하면 좋은 자료 링크는 모던 JS 의 내용이다.

 

https://ko.javascript.info/class-inheritance공식 MDN 문서를 참조하면 다음과 같은 클래스가 있다. 

 

클래스 상속

 

ko.javascript.info

 

  위와는 별개로 공식 MDN 문서를 참조하면 다음과 같은 클래스가 있다. 

class Person {
  constructor(first, last, age, gender, interests) {
    this.name = {
      first,
      last
    };
    this.age = age;
    this.gender = gender;
    this.interests = interests;
  }

  greeting() {
    console.log(`Hi! I'm ${this.name.first}`);
  };

  farewell() {
    console.log(`${this.name.first} has left the building. Bye for now!`);
  };
}

  이 클래스를 기반으로 인스턴스를 만들어낸다.

let snape = new Person('Severus', 'Snape', 58, 'male', ['Potions']);
snape.greeting(); // Hi! I'm Severus.

  그런데, 스네이프는 해리포터에서 교수직을 맡고 있다. 그래서 단순히 Person이 아닌 Person 중에서도 교수(Person의 내용은 모두 갖고 교수라는 또 다른 특징을 갖게 하는 것)라는 카테고리를 붙이고 싶다. 그러면 Professor 클래스를 만들 떄 Person의 내용을 상속시키면 된다. 상속시키는 방법은 다음과 같다.

class Professor extends Person {
  constructor(first, last, age, gender, interests, subject, grade) {
    super(first, last, age, gender, interests);

    // subject and grade are specific to Teacher
    this.subject = subject;
    this.grade = grade;
  }
}

 

 

 

2. 재귀 함수

  1) 재귀 함수 사용 시기  

  첫 번째로는 주어진 문제를 비슷한 구조로 더 쪼갤 수 있을 때다. 모든 재귀 함수는 반복문으로도 표현이 가능하다. 다만, 재귀를 적용할 때 코드가 더 간결해진다는 장점이 있다. 어쨌든, 반복문으로 사용가능하다는 건 말 그대로 반복되는 내용이 있다는 것이다. 반복이 되는 그 지점을 쪼갤 수 있을 때 재귀 함수를 사용할 수 있다.

 

  두 번째로 중첩된 반복문이 많거나 중첩 횟수를 예측하기 어려울 때 사용한다. 반복문 안에 반복문을 사용하는데, 그 반복문 사용이 만약에 원래 사용하던 반복문과 똑같은 기능을 구현하는 것일 때 재귀 함수를 사용하면 더 편하다. 예를들어 고차원의 배열이 있다고 하자 그 때 배열의 원소로 true를 찾아내려고 할 때 재귀를 사용하면 더 편하다

 

  2) 재귀 함수로 풀기

  1. 재귀 함수의 입력 값과 출력값을 정의한다. 

  2. 문제를 쪼갠 후 경우의 수를 나눈다. 

  3. 재귀 함수의 탈출 조건 (Base Case)를 설정한다. 재귀 함수를 언제 멈출지를 정의한다.

  4. 재귀 함수의 내용을 작성한다. 어떤 기능을 구현할지 작성한다 

 

 

function recursive(a, b, c, ...) {
  // Base Case : 문제를 더 이상 쪼갤 수 없는 경우, 재귀 함수 탈출 조건
  if (문제를 더 이상 쪼갤 수 없을 경우) {
    return 단순한 문제의 해답;
  }
  // recursive Case
  // 문제 해결을 위해 인자들에 변화를 줌 
  // a = ~~
  // b = ~~
  // c = ~~
  
  return 더 작은 문제로 새롭게 정의된 문제
  // 예1. something + recursive(a, b, c, ...)
}

 

 

재귀 함수는 개념을 백날 공부해도 문제 앞에만 서면 무력해진다. 재귀 함수와 관련된 문제를 많이 푸는 것만이 답이다.