JS 객체지향 프로그래밍

Goody·2021년 1월 16일
0

자바스크립트

목록 보기
2/13

들어가며

OOP는 객체지향프로그래밍의 줄임말로, 객체간의 역할, 책임, 협력메시지를 통해 소프트웨어를 만드는 패러다임을 말한다.

  • 역할 : 프로그램을 동작하는 데 있어 객체 하나가 맡는 역할.

  • 책임 : 특정한 역할을 맡은 객체만이 해야하는 어떤 기능.

  • 협력 : 각기 다른 역할과 책임을 가진 객체들간의 협력.

    스타벅스의 역할 분담은 손님, 계산원, 바리스타로 나뉜다.

    손님은 메뉴 선택이라는 책임을, 계산원은 결제와 바리스타에게 메뉴 주문을, 바리스타는 주문을 받아 커피를 만드는 책임을 떠안는다.

    이 세 객체간의 협력으로 스타벅스 라는 프로그램이 돌아간다.

  • 메시지 : 객체간의 협력을 위한 통신 수단.



1. 오브젝트

오브젝트는 배열과 달리, 순서없는 프로퍼티를 갖는 데이터 컨테이너이다.

인덱스가 아닌 키를 통해 각 프로퍼티에 접근할 수 있다.

1.1 오브젝트의 나열

const SYM = Symbol();
const o = {a: 1, b: 2, c: 3, [SYM]: 4};

for(let prop in o) {
  if (!o.hasOwnProperty(prop)) continue;
  console.log((`${prop}: ${o[prop]}`));
}

// a: 1
// b: 2
// c: 3

hasOwnProperty는 상속된 프로퍼티가 for...in에 나타날 위험을 제거한다.

여기서는 생략해도 상관 없으나, 다른 타입 혹은 타인이 만든 객체를 나열할 때는 만약을 대비해 hasOwnProperty를 사용하는 습관을 들이자.



2. 클래스와 인스턴스

클래스는 자동차 처럼 추상적이고 범용적인 것이고, 인스턴스는 suv, 스포츠카, 준중형 등 구체적이고 한정적인 것이다.

여기서 추상적, 구체적이라는 표현은 어디까지나 상대적인 것이다.

자동차는 운송수단에 비해 구체적이고,

스포츠카는 람보르기니, 페라리 등에 비해 추상적이다.

다만 우리가 코드를 짤 때엔 적당히, 필요한 만큼의 추상화와 구체화를 한다.

페라리 -> 스포츠카 -> 자동차 -> 운송수단 -> 기계

이런식으로 추상화를 하면 끝도 없다.

클래스에는 내가 표현하려는 객체들이 공통적으로 갖고 있어야 할 속성, 또는 기능(메소드)를 추상화시켜 넣어놓는데, 코드를 보자.

class Car {	
    constructor(make, model) {
        this.make = make,		// this는 car1, 혹은 car2
        this.model = model,
        this.userGears = ['P','N','R','D'];
        this.userGear = this.userGears[0];
    }

    shift(gear) {
        if(this.userGears.indexOf(gear) < 0) {
            throw new Error(`Invalid gear: ${gear}`);
        }
        this.userGear = gear;
    }
}

const car1 = new Car("Toyota", "civic");
const car2 = new Car("Hyundai", "genesis");
const car3 = ...;
const car4 = ...
...

모든 차에는 제조사, 모델명, 기어 상태 라는 속성과 기어를 바꾸는 기능이 포함되어 있다.

위에서 자동차라면 당연히 가져야 할 속성이나 기능들을 class Car에 넣어 추상화하고,

모든 자동차의 이름이나 제조사, 기어 상태가 다를 수 있기 때문에 car1, car2 등의 인스턴스를 생성해 구체화했다.



3. 상속과 다형성

위에서 페라리를 추상화한 예를 다시 가져와보자.

페라리 -> 스포츠카 -> 자동차 -> 운송수단 -> 기계

페라리와 스포츠카, 자동차, 운송수단 사이에는 분명 공통 특성 혹은 기능이 존재한다.

예를 들면 좌석개수 , 현재위치 , 이동 등이 있을 수 있다.

운송수단이면 당연히 사람이 앉을 좌석, 현재 위치, 어딘가로 이동하는 기능이 있을 것이라고 쉽게 떠올릴 수 있다. 만약 우리가 운송수단이라는 클래스와 스포츠카라는 클래스를 만들었다고 가정해보자.

class Transportation {
	constructor(seat, position) {
		this.seat = seat,
        this.position = position
	}
    
	move(direction, distance) {
		return this.position += distance;
	}
}

class SportsCar {
	constructor(seat, position) {
		this.seat = seat,
        this.position = position,
        this.userGears = ['P','N','R','D'];
        this.userGear = this.userGears[0];
	}
    
	move(distance) {
		return this.position += distance;
	}
    shift(gear) {
        if(this.userGears.indexOf(gear) < 0) {
            throw new Error(`Invalid gear: ${gear}`);
        }
        this.userGear = gear;
    }
}

코드를 자세히 보면, 운송수단과 스포츠카 사이에 똑같은 코드를 발견할 수 있다.

constructor 부분에 this.seat, this.positionmove 메소드가 겹친다.

위에서 이야기했듯, 운송수단과 스포츠카 사이에 좌석개수 , 현재위치 , 이동 이라는 공통 특성 혹은 기능들이 있기 때문이다.

이처럼 클래스간에 공통적인 프로퍼티가 있을 때는 하위 클래스가 상위 클래스의 기능을 상속하게 만들 수 있다.

class Transportation {
	constructor(seat, position) {
		this.seat = seat,
        this.position = position;
	}
    
	move(distance) {
		return this.position += distance;
	}
}

class SportsCar extends Transportation {
	constructor(seat, position) {
		super(seat, position);
        this.userGears = ['P','N','R','D'],
        this.userGear = this.userGears[0]
	}
    
    shift(gear) {
        if(this.userGears.indexOf(gear) < 0) {
            throw new Error(`Invalid gear: ${gear}`);
        }
        this.userGear = gear;
    }
}
const car1 = new SportsCar(2, 0, 2, 5);

console.log(car1.seat);		// 2
console.log(car1.move(10));	// 10

TransportationSportsCar 가 상속하면서, 중복되는 생성자와 메소드를 코딩하지 않고도 SportsCar는 moveseat , position 프로퍼티를 갖게 되었다.

위에서는 car1으로 인스턴스를 1 대만 만들었지만, 이외에도 car2, car3 등 다양한 인스턴스를 생성할 수 있고, 각기 다른 인스턴스들은 다른 좌석 개수, 위치, 움직임 등을 가져갈 수 있다.

이를 다형성 이라고 한다.



4. this와 super

  • this : 메소드 안에서의 this가 어떤 객체를 가리킬지는 this가 호출된 곳의 context에 따라 다르다.
  • super : 부모클래스가 갖고있는 코드를 실행하고, 부모 클래스가 하지 못하는 일은 자식 클래스만 할 수 있게 함.
    • super() : 부모클래스의 생성자.
    • super.메소드이름 : super === 부모클래스
차이점
this그 클래스의 다른 생성자를 호출
super슈퍼 클래스의 생성자를 호출



5. 객체 비교

좌석 수, 위치, 기능이 완전히 똑같은 두 운송수단이 있다면, 우리는 이 둘이 같다고 말할 수 있을까?

class Transportation {
	constructor(seat, position) {
		this.seat = seat,
        this.position = position;
	}
    
	move(distance) {
		return this.position += distance;
	}
}

const vehicle1 = new Transportation(4, 0);
const vehicle2 = new Transportation(4, 0);

console.log(vehicle1 === vehicle2); // false;

vehicle1과 vehicle2 는 분명 같은 부모 클래스를 갖고, 인자도 같은데 왜 같지 않을까?

객체는 원시형 값과 달리, 값 비교가 아닌 레퍼런스 참조를 비교하기 때문이다.

이에 객체를 비교하는 방법은 크게 두 가지가 있다.

5.1 얕은 비교 (Shallow equality)

function shallowEqual(object1, object2) {
  const keys1 = Object.keys(object1);
  const keys2 = Object.keys(object2);

  if (keys1.length !== keys2.length) {
    return false;
  }

  for (let key of keys1) {
    if (object1[key] !== object2[key]) {
      return false;
    }
  }

  return true;
}

위 코드에서, 우리는 두 오브젝트의 key의 길이를 먼저 비교하고,

for 루프를 돌면서 key 안에 담긴 value를 비교한다.

이 두 과정을 거치고도 false를 반환하지 않는다면, 두 객체는 같은 것으로 간주하는 함수를 만들었다.

얕은 비교는 객체 내 프로퍼티의 값들이 원시형 값들이라면 유용하게 쓰일 수 있지만,

객체 내 프로퍼티에 객체를 담았을 때는 제대로 동작하지 않는다.

5.2 깊은 비교 (Deep equality)

깊은 비교는 얕은 비교와 비슷하지만, 한가지 다른 점이 있다.

비교되는 프로퍼티가 오브젝트라면, 얕은 비교를 재귀적으로 수행한다는 점이다.

function deepEqual(object1, object2) {
  const keys1 = Object.keys(object1);
  const keys2 = Object.keys(object2);

  if (keys1.length !== keys2.length) {
    return false;
  }

  for (const key of keys1) {
    const val1 = object1[key];
    const val2 = object2[key];
    const areObjects = isObject(val1) && isObject(val2);
    if (
      areObjects && !deepEqual(val1, val2) ||
      !areObjects && val1 !== val2
    ) {
      return false;
    }
  }

  return true;
}

function isObject(object) {
  return object != null && typeof object === 'object';
}



참고문헌

0개의 댓글