JIGGAG

객체 + 객체 = 객체

2021년 9월 25일

절차적 vs 객체지향

책임을 지는 주체가 무엇인지가 절차적 프로그래밍과 OOP의 차이점이다.

절차적 프로그래밍에서는 문장, 연산자, 명령문으로 구성된 코드가 책임을 지는 주체데이터를 제어한다. 데이터는 코드가 호출해주고 수정하는 등 수동적인 역할로 존재하게 된다.

반면에 객체지향 프로그래밍에서는 객체책임을 지는 주체로 데이터를 대체하는 역할을 위임받아 동작한다. 객체지향에서는 클래스와 인스턴스만을 포함할 뿐 문장, 연산자, 명령문이 존재하지 않기 때문이다. 작은 객체들을 모아서 커다란 객체, 애플리케이션으로 조합한 후 이 커다란 객체가 작업을 수행할 수 있도록 역할을 위임하는 것이다.

호출해주기를 기다리는 형태인 수동적인 절차적 프로그래밍에서 역할을 위임받아 능동적으로 수행할 수 있는 객체지향 프로그래밍으로 흘러가면서 좀 더 작은 객체를 구성하고자 한다.


다시 한번 작은 객체를 위해서

작은 객체를 만들기 위해 최소한의 public 메서드를 유지하는 것이 중요하다.

작은 객체를 사용해야 유지보수가 수월하고 응집도를 높일 수 있다.

public 메서드(private을 제외한 public, protected 모두 해당한다)가 많아지는 것은 클래스는 커지고 외부에서 접근할 수 있는 진입점이 많아지는 것을 의미한다. 이런 경우 모든 진입 케이스를 대응해야하기 때문에 응집도가 낮아질 수 있다.

클래스가 작을수록 프로퍼티와 메서드가 가까이 위치하여 모든 메서드에서 프로퍼티를 사용하도록 응집도가 높일 수 있다. 메서드가 많아질수록 특정 프로퍼티만을 사용하고 서로 연관되지 않아 사용하지 않는 프로퍼티/메서드가 존재하게 된다.

서로 독립적인 존재의 프로퍼티와 메서드가 함께 위치하기 보다는 분리해서 좀 더 응집도를 높이고 테스트나 케이스를 쉽게 확인 할 수 있도록 하자.


다시 새로운 객체 생성

스터디하면서 가장 충격적?으로 다가왔던 내용 중 하나로 정적메서드, 퍼블릭상수, 유틸함수 대신 같은 역할을 하는 새로운 객체를 만들어서 사용하는 것이다.

정적메서드, 퍼블릭상수, 유틸함수를 새로운 객체로 만들어서 작은 객체를 조합해 큰 객체가 되도록 사용하는 것이 대부분 이해된다. 하지만 정적메서드, 퍼블릭상수, 유틸함수의 장점이라고 생각하던 간단하고 빠르고 명확하게 사용할 수 있는 직관적인 부분을 대신하여 매번 새로운 객체를 생성하는 것은 오히려 과한 생성이지 않을까 생각이 들었다.

그러나 실행을 명령하는 정적메서드, 퍼블릭상수, 유틸함수는 객체지향의 능동적으로 동작하는 객체 형태가 아니기 때문이다.


정적메서드 대신 새로운 객체를 사용하자

컴퓨터는 우리가 결정한 명령어를 순차적으로 위에서 아래로 실행한다. 그러나 순차적인 흐름으로 실행하는 것은 동일한 동작을 다시 실행하기 위해 중복된 내용을 계속 다시 작성해주어야하는 한계에 도달하였다. 이런 중복을 해결하게 위해 중복된 역할의 함수를 분리하고 필요에 따라 호출하도록 위임하였다.

함수를 분리하게 되면 유틸리티 클래스의 정적 메서드 형태로 구현하는 것이 습관화 되어있었다. 하지만 이번 챕터의 주제인 정적메서드 대신 객체를 사용하기 위해서 아래에 예시를 적어보았다.

우선 최대값을 구하는 로직이 중복되어 이를 정적메서드로 분리하였다. (가장 일반적인, 의식의 흐름대로 작성되는 정적메서드이다...)

class Max {
	static max(a: number, b: number) {
		return a > b ? a : b;
	}
}

// 최댓값을 구하기 위해 유틸리티 클래스의 정적메서드를 호출하였다
console.log(Max.max(1, 2));

최대값을 구하기 위해 Max.max 정적메서드를 호출하고 그와 동시에 계산하여 반환하는 모양이다.

그러나 객체를 사용하면 명령하는 것이 아니라 동작을 정의만 해두는 것으로 필요할 때 스스로 상호작용하도록 위임할 수 있다.

class Max {
  private a: number;

  private b: number;

  constructor(a: number, b: number) {
    this.a = a;
    this.b = b;
  }

  get() {
    return this.a > this.b ? this.a : this.b;
  }
}

// 최댓값 객체를 생성만 하였다
const maxClass = new Max(1, 2);

new Max 객체가 어떤일을 해야하는지 명령하지 않고 오직 최대값 객체를 생성할뿐이다. 이 객체는 최대값 계산을 아직 실행을 하지 않았고 객체로써 생성만 되어있는 상태이다.


명령 대신 선언

정적메서드를 사용하는 경우와 객체를 만들어서 사용한 경우, 언제 계산 로직이 실행되느냐의 차이점이 있었다.

명령형에서는 문장을 통해 계산 방식을 서술해야하지만 선언형에서는 계산 로직을 표현하기만 할 뿐이다. (정적메서드가 명령형이라면, 객체는 선언형이다)

명령이나 선언이나 결국 구하고자 하는 계산 로직은 구현이 되어있어야한다. 최댓값을 구하는 a > b ? a : b는 어디선가 처리되고 있는데, 어떤 클래스, 객체, 메서드가 사용하는지에 따라 언제 계산이 되는지 차이가 존재한다.

명령형 정적메서드에서는 필요한 연산을 호출한 즉시 결과값을 반환하게 된다. 명령했기 때문에 바로 반환하는 것이다.

선언형에서는 객체를 생성하기만 했을뿐 아직 결과를 반환하지 않았다. 아직 계산을 시작하지 않았기 때문에 이 객체가 최댓값을 가져올 것이다라고 정의만 했을뿐 최댓값을 계산하는 시점은 maxClass.get()을 사용할때이다.

선언형이 더 좋은 이유 3가지

첫번째, 계산 자체를 할 필요가 없는 조건에 성능 최적화를 제어할 수 있다는 점에서 선언형이 더 빠르다.

명령형이 호출과 동시에 바로 결과를 가져온다는 점에서 선언형보다 빨라 보이지만, 실제로 그 계산된 값을 사용하지 않는다면 성능 최적화 부분에서 호출과 동시에 계산을 해버리는 명령형은 비효율적이다. (사용하지도 않을 값을 계산하고 있는 것은 불필요하다)

실제로 계산된 값이 필요한 시점과 위치를 결정하도록 위임하여 요청이 있을때에만 객체가 계산을 하도록 최적화 할 수 있다. (이것은 생성자에서 초기화만 진행하고 계산로직은 분리하는 것과 같은 이유라고 생각한다.)

두번째, 코드 사이의 의존성을 분리할 수 있다.

주생성자메서드 안에서 new 연산자를 사용하지 않아야한다. 그렇다면 서로 의존적인 코드를 어떻게 해결할 수 있을까?

부생성자에서 새로운 객체를 생성하거나 외부에서 주입하는 방법이 있다. 객체가 스스로 상호작용하여 동작하기 위해 객체와 객체의 의존을 완전히 분리하고 독립적이여야만 좀 더 나은 유지보수를 이끌어낼 수 있다. (계속 이어지는 유지보수의 중요성)

세번째, 선언형에서는 결과를 표현하지만 명령형에서는 수행하려는 무언가를 이야기한다.

명령형애서는 얻고자 하는 결과를 가져오기 위해 미리 예상한 코드를 실행해야만 한다. Max.max()를 통해 최대값을 가져오라고 명령하는 것은 이미 수행하려는 무언가를 예상하고 호출하는 방식이다. (컴퓨터가 실행하는 일을 동일하게 예상해야만 한다)

선언형에서는 new Max()라는 객체에서 max.get() 최대값을 가져오는 것을 사용했을 뿐이다. 실제 구현부는 예상하지 않고 객체에게 온전히 위임한 형태이다.

알고리즘을 생각하고 실행하는 명령형이 아니라 객체의 행동을 생각하는 선언형이었다.


사실 한가지가 더 있었는데, 응집도 관련한 이야기이다. 선언형 객체를 사용하면 응집도를 높일 수 있는데, 이것은 객체지향을 따르면 되는 이야기이기도 하고 정적메서드를 활용했을때에도 유틸리티 클래스에 응집도를 높일 수 있지 않을까 하는 생각이 남아있었다.

명령형과 선언형을 혼용하는 것은 계속 명령형에 남게 되는 이유이다

유틸리티 클래스에서도 응집도 높게 적절히 분리하면 된다고 생각하고 있었는데, 결국 내가 명령형을 벗어나지 못하는 이유이기도 하다. (선언형을 쓰고 있지도 못한 것 같지만)

대안으로는 명령형을 감싸서 선언형으로 가리는 것인데, 결국 언젠가는 걷어내야만하는 덩어리인 것이다...


싱글톤 패턴

하나의 정적 메서드를 사용해 객체 인스턴스를 하나만 생성하고자 사용하는 디자인 패턴이다.

class MathSingleton {
  private static instance: MathSingleton = new MathSingleton();

  constructor() {

  }

  static getInstance() {
    return MathSingleton.instance;
  }

  max(a: number, b: number) {
    return a > b ? a : b;
  }
}

private으로 생성된 인스턴스 하나를 계속 사용하고자 public으로 인스턴스를 가져오는 메서드 getInstance를 만들었다.

정적메서드를 사용하는 것이 유틸리티 클래스와 동일하지만, 유틸리티 클래스는 내부에서 인스턴스를 만들지 않지만 싱글톤은 인스턴스를 만들고 setInstance 메서드를 추가하여 인스턴스를 변경할 수 있다는 차이점이 있다.

하지만 인스턴스를 하나만 만들어서 사용한다는 것은 전역변수와 동일한 개념이고 객체지향에 어긋나는 내용이다🙈

그렇다면 싱글톤처럼 전역에서 필요한 정보를 어떻게 객체로 정리할 수 있을까?

전역에서 필요한 정보를 모든 클래스 내부에 캡슐화하는 것이다.

이를 모든 생성자에서 제공하면 클래스가 너무 방대해져서 오히려 응집도가 떨어지게 되지 않을까?? 싱글톤이라는 객체지향 안티패턴을 위해 모든 객체가 방대해지는 것, 어떤 방향을 선택하는 것이 좋을까


데코레이터

데코레이터는 객체를 감싸서 기존의 객체를 커스텀하는 형태이다. 객체 안에 객체 안에 객체 안에 객체 형태로 기존의 초기 객체 형태는 그대로 갖고 있지만 약간의 상태를 변경하는 것이다.

new Sorted(new Unique(new File()))File객체를 반환하는데 단지 Unique + SortedFile이 된 것이다. 동일한 작업을 new File().unique().sorted() 처럼 함수를 통해 할 수 있다.

그냥 객체로만 이루어진 코드 형태를 유지하기 위한 객체 전체를 캡슐화하고 객체를 계속 조합할 수 있도록 유지하는 것이 데코레이터의 목적으로 보인다.


객체 합성: 객체 + 객체 = 객체

객체지향 프로그래밍은 작은 객체들을 조합해 큰 객체를 만들고 다시 조합해 더 큰 객체를 만들어가는 것이다.

물론 정적메서드를 감싸는 방식으로 새로운 객체를 만들어서 사용할 수 있지만 일반적으로 정적메서드를 이용하면 다른 객체와 조합이 불가능하다. (정적 메서드를 호출하는 순간 결과값이 나와버리기 때문에 객체와 조합할 수 없다)

조합할 수 있는 객체를 만들자