클래스 사용에 대한 고찰

1. 개요

이번 파트는 우아한테크코스 3주 차 미션 이후 코치분들에게 받은 공통 피드백을 바탕으로 정리한 클래스 사용에 대한 내용이다.

아래의 내용을 클래스를 다룰 때 적용해야 하는 원칙으로 생각하며 하나하나 정리해보자.


2. 원칙 1 - 객체의 상태 접근을 제한한다

클래스의 필드는 외부에서 접근할 수 없도록 private class 필드로 구현하는 것을 권장한다.

class Rectangle {
  #width;
  #height;

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

클래스의 기본에서 살펴본 것을 바탕으로, private class 필드를 구현하기 위해선 #을 변수 앞에 추가하면 된다. 이를 통해 인스턴스가 만들어졌다 해도 인스턴스에서 직접적으로 클래스 필드에 접근할 수 없다.

// 추가 작성
const rectangle = new Rectangle(3, 4);

rectangle.#width;

외부에서 클래스 필드에 직접 접근을 한다면 오류가 발생한다.


2-1. 생각을 해보자...

(위의 예시) 사각형의 너비와 높이를 외부에서 사용하면 편리할 것 같은데 왜 private로 만들어 접근 자체를 막는 것일까?

여러 기능을 만들다 보면 분명 필요한 상황이 찾아올 텐데, 이를 막아 놓으면 코드 구현은 어떻게 해야 할까?

그러면 클래스 필드를 가져오기 위해 getter를 사용해야 하는 것일까?

아니면 메서드를 만들어 this.#width를 반환해야 하는 것일까?


2-2. 정보 은닉

클래스에서의 정보 은닉이란 모든 객체지향 언어적 요소를 활용하여 객체(클래스)에 대한 구체적인 정보를 노출시키지 않도록 하는 기법이다. 즉 위의 예시에서 사각형의 너비와 높이에 대한 정보는 노출되지 않았다는 것이 바로 정보 은닉에 해당된다.(정보 은닉 !== 캡슐화)

소프트웨어의 유연성을 확보하는 단 한가지 방법만 있다면 그것은 "객체(또는 클래스) 간에 서로를 모르게 하는 것"이다. 두 객체(또는 클래스)가 서로를 모른다는 것은 서로의 코드에 상대 객체나 클래스에 대한 코드가 단 한 줄도 없다는 의미이다.

언뜻 이해가 되지만 구체적인 코드는 떠오르지 않는다. A와 B클래스가 있다고 생각하자. A클래스의 필드를 변경하는 것은 B클래스가 아니라 오직 A클래스에서만 가능하다. 단, 공개된 메서드를 통해서 B클래스에서 A클래스로 메시지를 보내는 것은 가능하다.

최대한 정보를 은닉하고 꼭 필요한 메서드만 외부에 공개해야 한다. 즉, 객체(또는 클래스)의 상태(필드)를 외부로 노출하지 않는다. 객체(또는 클래스)가 외부에 노출하는 것은 행동(메서드)뿐이며, 외부에서 객체에 접근할 수 있는 방법 역시 행동(메서드)뿐이다.


2-3. 객체의 상태 접근을 제한함으로써 생기는 이점

  1. 객체의 자율성을 높인다.

  2. 자율적인 객체는 스스로 판단하고 스스로 결정한다.

  3. 자율성이 높아질수록 지능도 높아지고 지능이 높아질수록 협력은 유연하고 간결해진다.


2-4. 나의 결론

오른손이 하는 일을 왼손이 모르게 하라.

오른손과 왼손은 모두 개별적인 존재이다. 누가 오른손을 움직이기 위해 왼손을 사용하는가? 개별적으로 존재함으로써 행동의 자율성이 높아지고 이로 인해 많은 일들을 할 수 있다.

프로그래밍에서의 객체(클래스)도 마찬가지이다. 최소한의 행동만 외부에 노출함으로써 서로간의 의존성을 최대한 낮춰야 한다. 그래야 똑똑한 프로그램이 탄생된다.

객체의 상태 접근을 제한한다라는 원칙을 이해하기 위한 가장 좋은 방법은 직접 의존성이 빵빵한 프로그래밍을 하는 것이라고 생각한다. 언제 한 번 기회가 된다면 정말 유지보수가 어렵고 서로가 서로를 의존하는 코드를 작성해보자. 그래야 객체의 상태 접근 제한한다.라는 의미를 더욱 알 수 있지 않을까?


3. 원칙 2 - 객체는 객체스럽게 사용한다

객체스러움이 무엇일까? 먼저 객체스러움이 아닌 경우를 살펴보자.


3-1. 객체스럽지 못한 객체

class Rectangle {
  #width;
  #height;

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

  getWidth() {
    return this.#width;
  }

  getHeight() {
    return this.#height;
  }
}

const rectangle = new Rectangle(3, 4);

const width = rectangle.getWidth();
const height = rectangle.getHeight();

const area = width * height;

Rectangle 클래스에서는 가로와 높이를 가져오는 메서드가 있다. 생각해보자. private class 변수로 만들었기 때문에 외부에서 접근을 하지 못한다. 하지만 getWidth() 메서드, getHeight() 메서드를 통해 해당 변수를 가져올 수 있다. 이로 인해 외부에서 도형의 넓이를 구하는 로직을 작성할 수 있다.

또한 private class 변수를 만들었지만 private한 기능을 충실히 이행하고 있지 않다. 즉, 객체의 상태 접근을 제한한다.라는 원칙도 위배하고 있다.

어떤가? 객체가 자기가 해야 할 역할을 충실히 하고 있는가? 전혀 그렇지 않다. 위의 예시에서의 객체는 행동을 갖고 있지도 않고 외부와의 메시지를 주고 받는 형태도 아니다.

즉, 객체스럽지 못한 객체가 된다.


3-3. 객체를 객체스럽게 사용하기

위의 예시를 아래와 같이 리펙토링 해보자.

class Rectangle {
  #width;
  #height;

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

  calArea() {
    return this.#width * this.#height;
  }
}

const rectangle = new Rectangle(3, 4);

const area = rectangle.calArea();

클래스의 외부에서 클래스에 메시지를 전달하고 있다. 메시지의 내용은 calArea 즉, "넓이를 계산해줘"이다. 즉, 메서드가 클래스에게 전달하는 하나의 메시지라고 생각을 할 수 있다. 메서드를 통해 클래스 내부의 변수에 접근을 할 수 있을 뿐 외부에서는 접근을 하지 않고 있다.

드디어, 객체가 로직을 구현함으로써 제대로된 역할을 할 수 있게 되었다.

추가적으로 setter, getter 메서드는 사용하지 않도록 하자.


3-3. 객체스럼움을 위한 리팩터링 원칙

  1. 상태를 가지는 객체를 추가했다면 객체가 제대로 된 역할을 하도록 구현해야 한다.

  2. 객체가 로직을 구현하도록 해야한다.

  3. 상태 데이터를 꺼내 로직을 처리하도록 구현하지 말고 객체에 메시지를 보내 일을 하도록 리팩토링한다.


3-4. 나의 결론

고양이의 밥은 누가 주는가?

야생의 고양이는 스스로 밥을 찾아 다니지만 가정에서 애완으로 키우는 고양이의 경우 밥은 집사가 차려준다.

이때 고양이는 집사의 손을 물리적이든 정신적이든 조종하지 않고 단지 배고프다는 울음소리(메시지)를 보낼 뿐이다.

고양이의 밥을 제공해주는 것은 집사의 몫이다. 집사가 집사다운 행동을 한 것이다. 이러한 행동이 없으면 과연 진정한 집사라고 할 수 있는가?

객체도 객체다운 행동을 해야한다. 함부로 상태 데이터를 외부로 뿌리고 외부에서 작업을 해서는 안된다. 앞으론 객체가 스스로 일하는 살아있는 객체를 만들어보자.


4. 원칙 3 - 필드의 수를 줄이기 위해 노력한다

클래스 필드의 수가 많아지면 많아질 수록 복잡도는 높아지고 버그를 발생시킬 가능성이 생긴다. 어떤 필드로 부터 다양한 결과를 도출할 수 있으면 굳이 필드를 추가하기 않도록 하자.

필요한 필드만 만들어 놓자.


4-1. 음... 이건

이번 원칙은 다른 원칙보다 직관적으로 이해가 된다. 필드가 무작정 많다고 많은 데이터를 가공 할 수 있고 다양한 역할을 하는 클래스가 만들어지는 것이 아니다. 오히려 필드 수가 많고 많은 역할을 한다면 클래스 분리에 대해 고민할 필요가 있다.


4-2. 실제 프리코스의 클래스

3주 차 미션인 로또 미션에서 사용한 클래스를 예시로 든다.

복수의 Lotto 인스턴스를 관리하는 Lottos 클래스이다.

class Lottos {
  constructor(money) {
    this.validate(money);
    this.count = money / MONEY.UNIT;
    this.list = [];
    this.publish();
  }

Lottos 클래스의 필드는 countlist이다. countlist의 길이라고 할 수 있다. 즉, count 필드는 필요가 없다.

이렇게 굳이 필드로 만들지 않아도 되는 것은 리팩토링을 통해 줄이도록 하자.


4-4. 나의 결론

프로그래밍의 세계에선 복잡함은 질색이다. 복잡함은 버그로 이어질 수 있으므로 딱 필요한 내용만 존재하도록 하자.


5. Conclusion

프리코스 3주 차 공통 피드백에 있는 내용 중 클래스에 대한 내용을 나름 정리해보았다. 새로운 개념을 배웠다기 보다는 어떻게 객체(또는 클래스)를 사용해야 하는지에 대한 방법에 대해 배웠다고 생각한다. 지금 당장 구현을 뮈한 코드도 중요하지만 미래의 나에 대한 배려 그리고 동료를 배려하는 마음으로 클린하게 객체를 사용하자. 학생들에게 난 항상 말한다. "너희만 사는 곳이 아니다. 내 행동이 다른 사람들에게 피해를 주지 않도록 서로 배려를 해야 한다. 보이지 않는 선을 지키도록 하자." 나부터 올바른 객체(클래스)를 사용함으로써 지키도록 하자.


참고

객체 지향의 사실과 오해 - 객체, 그리고 소프트웨어 나라 객체지향의 올바른 이해 : 5. 정보 은닉(information hiding) 객체를 객체스럽게 사용하도록 리팩토링해라.


📅 2022-11-20

Last updated