JIGGAG

캡슐화된 객체를 식별하기

2021년 8월 24일

작은 객체

캡슐화는 작게 유지하자

하나의 클래스에 많은 것을 담으려고 하다보면 클래스의 목적이 흐려지고 복잡성이 높아져 유지보수가 어려워진다.

클래스 내부에 캡슐화된 모든 객체들이 객체의 식별자를 구성하는 요소이다.

class Cash {
	private dollars?: number;
  private cents?: number;
  private currency?: string;
}

Cash클래스는 3개의 객체를 캡슐화하였고 이들이 Cash를 식별 가능하도록 해준다. 객체는 단순히 이런 상태를 저장할 수 있는 껍질이라고 생각할 수 있고, 각각의 상태(dollars, cents, currency)가 동일하더라도 생성된 객체마다 식별자는 다르기 때문이다.

다양한 상태를 표현하기 위해 더 많은 상태를 캡슐화하여 사용하기도 한다.

객체가 모여 더 큰 객체를 나타낼 수 있는데 하나의 객체에 모든 것을 담아둘 필요는 없기 때문에 커다란 객체를 다시 그룹화하여 작은 객체로 분리한다면 하나의 클래스가 갖고 있는 객체의 수가 작아지도록 한다.


식별자?

계속 나오고 있는 상태와 식별자의 차이가 뭘까?

Cash 객체에 캡슐화된 dollars, cents, currency를 상태라고 생각했다. 각각 어떤 값을 가지고 있고 그 값을 상태라고 볼 수 있다고 생각했기 때문이다.

그럼 식별자는 무엇일까?

객체를 식별한다라는 말에서 받는 느낌으로는 무언가 유니크한 값인 것 같다. 다른 객체와 구분할 수 있는 것으로 객체의 상태가 변경되어도 식별자를 통해 동일한 객체인지 확인할 수 있어야한다.

값이라는 것은 식별자가 없기 때문에 값이 가지고 있는 상태를 통해 동등한 값인지 확인해야한다. 그러나 객체는 상태가 변경될 수 있기 때문에 식별자를 통해 비교해야한다.

- 객체는 상태를 가지고 있으나 상태는 변경될 수 있다.
- 객체의 행동을 통해 상태를 변경시킨다.
- 객체는 어떤 상태에 있더라도 유일하게 식별할 수 있다.

(엘리스 책도 재밋게 봤었는데... 재밋게만 보고 기억하지 못해 또 이렇게 매일 새롭다)

참고: 객체지향의 사실과 오해 - 객체의 특징 (상태, 행동, 식별자)


그렇다면

그렇다면 왜 적은 캡슐화에서 식별자 이야기가 나왔을까?

객체는 객체들의 집합체이다.

객체에는 상태가 존재하고 집합체에서 그 상태를 이루는게 또 다른 객체이다.

그럼 집합체를 구성하는 객체가 식별자이고, 그들이 집합을 이룬 상위 객체도 식별자이다. 이런 흐름 속에서 상위 객체의 상태는 하위 객체이고, 하위 객체는 식별자니깐 상위 객체의 상태는 식별자라는 조건이 성립한다🙈

내부에 캡슐화된 모든 객체들이 식별자를 구성하는 요소가 된다면 식별자가 많은 것이 좋은게 아닌가?

물론 유니크한 객체를 위해서는 식별자가 많은게 좋다. 하지만 캡슐화한 객체들을 그룹화할 수 있고 작은 컴포넌트로 만드는 것이 유지보수에도 좋고, 각각의 객체가 말하고자 하는 목적이 명확해지기 때문에 최대한 그룹화하여 캡슐화되는 대상을 줄여보자

적은 캡슐화로 클래스가 이야기하고자 했던 목적을 명확하게 유지하도록 하자


그래서

이번 챕터 초반에 상태와 식별자이야기가 나오고 나서 적은 캡슐화와 식별자를 무조건 연관지어서 생각하려고 했다. 그렇게 도달한 질문은 만약 많이 캡슐화된 객체를 사용한다면 과연 그렇게 생성된 인스턴스는 식별하기 쉬울까?였다.

그리고 지금 돌아보니 저 질문은 식별의 포인트를 식별자를 통한 인스턴스 자체의 식별이 아니라 상태를 비교하는 잘못된 개념으로 시작된 것 같다. 상태와 식별자를 명확하게 구분하지 못해 생긴 문제이지 않을까😭

처음에 내가 생각했던 객체의 식별객체 상태 동등성 비교였다. 진정한 객체의 식별은 동일한 상태를 갖고 있는 객체를 식별할 수 있도록 동일성 비교를 해야한다. (내가 생각했던 식별의 개념(상태값 비교) !== 객체가 하고자 하는 식별(주소값 비교))

그럼 동일성 비교는 어떻게 이뤄지는 것일까?

객체가 갖고 있는 상태가 아니라 주소값을 비교하여 정말 동일한 객체인지 확인하는 것이다. 같은 상태를 갖고 있을뿐 동일한 객체는 아닐 수 있고, 다른 상태를 갖고 있지만 동일한 객체일 수 있다.

식별자는 주소값에 해당하고 상태를 얼마를 갖고 있는지와는 영향이 없다. 식별자로써는 동일한 상태의 객체를 비교할 수 없기 때문에 동일성 비교를 위해서는 상태를 각각 확인해야한다.

기존에 ===, ==로는 동일성 비교가 되지 않아 equals라는 함수를 추가해보았다.

class Rectangle {
  private height?: number;

  private width?: number;

  constructor(width: number, height: number) {
    this.height = height;
    this.width = width;
  }

  equals(rect: Rectangle) {
    return this.height === rect.height && this.width === rect.width;
  }
}

const a = new Rectangle(1, 1);
const b = new Rectangle(1, 1);
console.log(a === b); // false
console.log(a == b); // false
console.log(a.equals(b)); // true

같은 상태를 가지는 객체 비교를 위해 equals에서는 모든 캡슐화된 프로퍼티를 각각 비교해주고 있는데, 만약 캡슐화가 큰 사이즈로 되어 있다면 이 조건이 점점 방대해질 것이다.

근데 이게 이번 챕터에서 하고자 했던 이야기가 아닌 것 같다.......! 이게 바로 한번의 스터디에서 여러가지 주제를 담으려 하다보니 목적이 흐려지는 커다란 캡슐화의 문제점이다


최소 1개는 캡슐화해야한다

앞에서 적은 캡슐화에서 알아본 캡슐화된 객체 = 식별자라는 것에 의하면 아무것도 캡슐화 하지 않은 것은 식별할 수 있는 무언가가 없기 때문에 생성된 객체는 동일하다.

class Year {
	read() {
    return new Date().toLocaleString();
  }
}

아무런 상태도 들어있지 않은 단순한 껍질을 인스턴스화 한 것 뿐이라 비어있는 껍질은 사용하지 않는 것이 좋다. 프로퍼티가 없는 클래스는 정적 메서드와 동일해지는데, 이는 아무런 상태와 식별자도 가지 않고 있기에 오직 행동만을 하게 된다. 위의 Year클래스를 인스턴스화 하지 않아도 Year.read()으로 호출해버리면 되기 때문이다😭 (인스턴스를 생성과 실행을 분리하는 것이 의미가 없어진다)

정적 메서드로 사용되는 부분을 생성자를 통해 전달하도록 하여 인스턴스 생성과 실행을 분리하였다.

class Year {
  private date?: string;

  constructor(date: string) {
    this.date = date;
  }

	read() {
    return this.date;
  }
}

캡슐화된 상태는 객체를 고유하게 식별할 수 있지만 아무것도 캡슐화하지 않은 객체는 식별할 수 있는 대상이 없다

자기 자신을 식별할 수 있도록 다른 객체들을 캡슐화하여야 한다


인터페이스를 사용하자

각각 객체가 어떤 역할을 하고 있는지 알려줄 수 있기 때문에 객체와 객체를 결합하는 것이 유용하게 사용된다. 그러나 이런 결합도가 높아질수록 서로 연결된 객체를 수정하는 것은 조심스럽고 어떤 사이드이펙트가 있는지 알기 어렵기 때문에 유지보수에 어려움이 있다.

따라서 상호작용하는 객체를 수정하지 않고도 해당 객체를 수정할 수 있도록 분리 decouple 해야 한다.

방금까지 결합이 유용하게 사용된다고 했는데 다시 분리해야한다고?

객체가 객체왁 결합하면 서로 변경하기 어려운 강한 결합이 생긴다. 우선 각각의 객체가 따로 사용될 수 있고 결합된 형태로 사용될 수 있기 때문에 한쪽에서 구현이 변경되어야 한다면 강한 결합으로 의도하지 않은 동작을 하게 되는 어려움 발생한다. 객체가 인터페이스와 먼저 결합된다면 변경사항으로 인해 누락되거나 의도와 다른 동작을 맞게 되는 일을 방지할 수 있다.

interface Year {
  read(): string;
}

class A implements Year {
  read(): string {
    throw new Error("Method not implemented.");
  }
}

클래스가 존재한다는 것은 다른 어디선가 이 클래스에 대한 객체를 사용하려고 하는 것이고, 클래스 자체가 누군가에게는 인터페이스 역할을 하고 있다는 것이다.

각각의 객체가 어떤 형태를 가지고 있고 어떤 역할을 하고 있는지 인터페이스를 통해 알 수 있다.

인터페이스에 구현된 모든 public 메서드를 클래스에서 구현해야만 하기 때문에 안정성을 보장할 수 있는 이유이다.

인터페이스를 참고하여 다른 객체에서 해당 클래스에 대한 로직을 구현하는데, 만약 인터페이스에 구현된 퍼블릭 메서드를 누락하게 된다면 존재할 것이라 생각했던 상태나 동작이 의도와 다르게 문제가 발생할 수 있기 때문이다.

인터페이스를 구현하는 클래스를 변경하고 싶다면 해당 인터페이스를 구현하고 있는 모든 클래스에서 변경이 되어야한다. 이것이 결합도를 높이는 단점으로 보여지지만 인터페이스를 사용해서 어디선가 구현이 변경되어 의도와 다르게 동작하는 문제를 방지할 수 있는 장점으로 여겨진다.