6장 - 객체와 자료구조

  1. 객체와 자료구조의 차이를 이해하고, 특성에 따라 활용한다.
  2. 무조건 객체만! 고집할 것은 없다.
  3. 객체를 사용한다면 객체의 특성에 맞게 사용해야 한다.

자료 추상화

구체적인 Point 클래스:

class Point {
    x: number;
    y: number;
}

구체적인 Point 클래스는 어떤 좌표계를 사용하는지 명확하다.
그리고 개별적으로 좌표값을 읽고, 설정하도록 강제한다.
해당 구현을 노출한다. 변수가 private로 선언되더라도 Getter, Setter가 있다면 구현을 외부로 노출하는 것과 같다.

추상적인 Point 클래스:

interface Point {
    getX(): number;
    getY(): number;
    setCartesian(x: number, y: number): void;
    getR(): number;
    getTheta(): number;
    setPolar(r: number, theta: number): void;
}

추상적인 Point 클래스는 x, y가 어떤 좌표계를 사용하는지 알 수 없다.
그리고 정의된 메서드로 변수에 대한 접근을 강제한다. (읽어올 땐 하나씩, 설정할 땐 한번에)

구현을 감추기 위해서는 추상화가 필요하다.
추상 인터페이스를 제공해 사용자가 구현을 모른 채 자료의 핵심을 조작할 수 있어야 한다.

객체는 자료를 세세하게 공개하기보다 추상적인 개념으로 표현하는 편이 좋다.
포함하는 자료를 표현할 가장 좋은 방법에 대해 고민해야 한다.

자료/객체 비대칭

객체는 추상화 뒤로 자료를 숨긴 채, 자료를 다루는 함수만 공개한다.
자료구조는 자료를 그대로 공개하며 별다른 함수는 제공하지 않는다.

절차적인 도형:

class Square {
    topLeft: Point;
    side: number;

    // ...
}

class Rectangle {
    topLeft: Point;
    height: number;
    width: number;

    // ...
}

class Geometry {
    area(shape: Object) {
        if (shape instanceof Square) {
            return shape.side * shape.side;
        }
        if (shape instanceof Rectangle) {
            return shape.height * shape.width;
        }
        throw new Error("No Such Shape");
    }
}

Square, Rectangle는 도형 클래스로 간단한 자료구조이다. (메서드 정의 없음)
Geometry는 각 도형 클래스를 다뤄 도형이 동작하는 방식을 구현한다.

Geometry에 둘레 길이를 구하는 perimeter() 함수를 추가하고 싶다면,
도형 클래스는 아무 영향을 받지 않는다.
반면에 새로운 도형을 추가하고 싶다면 Geometry에 정의된 메서드를 모두 고쳐야 한다.

새로운 도형을 추가할 때 변화
class Circle {
    center: Point;
    radius: number;

    // ...
}

class Geometry {
    readonly PI = 3.14159265;

    area(shape: Object) {
        if (shape instanceof Square) {
            return shape.side * shape.side;
        }
        if (shape instanceof Rectangle) {
            return shape.height * shape.width;
        }
        if (shape instanceof Circle) {
            return PI * shape.radius * shape.radius;
        }
        throw new Error("No Such Shape");
    }
}

위와 같이 Geometry에 조건문이 추가된다.


다형적인 도형:

interface Shape {
    area(): number;
}

class Square implements Shape {
    #topLeft: Point;
    #side: number;

    area(): number {
        return this.#side * this.#side;
    }
}

class Rectangle implements Shape {
    #topLeft: Point;
    #height: number;
    #width: number;

    area(): number {
        return this.#height * this.#width;
    }
}

객체 지향적인 도형 클래스이다.
Geometry 클래스는 필요없다. 새 도형을 추가해도 기존 함수에 영향이 없다.
반면 새 함수를 추가하고 싶다면 도형 클래스 전부를 고쳐야 한다.

새 함수를 추가할 때 변화
class Square implements Shape {
    #topLeft: Point;
    #side: number;

    area(): number {
        return this.#side * this.#side;
    }

    perimeter(): number {
        return this.#side * 4;
    }
}

class Rectangle implements Shape {
    #topLeft: Point;
    #height: number;
    #width: number;

    area(): number {
        return this.#height * this.#width;
    }

    perimeter(): number {
        return this.#height * 2 + this.#width * 2;
    }
}

위와 같이 각 도형 클래스에 함수를 따로 구현해준다.


객체와 자료 구조는 근본적으로 양분된다.
객체 지향 코드에서 어려운 변경은 절차적인 코드에서 쉽고,
절차적인 코드에서 어려운 변경은 객체 지향 코드에서 쉽다.

객체로 구현하는 것이 모든 해답은 아니다.
때에 따라 단순한 자료구조와 절차적인 코드가 가장 적합한 상황도 있다.

디미터 법칙

디미터의 법칙은 다른 객체가 어떠한 자료를 갖고 있는지 속사정을 몰라야 한다는 것을 의미한다.
캡슐화를 높혀 객체의 자율성과 응집도를 높일 수 있다.

기차 충돌 (문제)

outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();

위와 같은 코드를 기차 충돌이라고 말한다.
각각 객체를 반환하는 함수가 연결되고 연결된 상황이다.
함수 하나가 아는 지식이 굉장히 많다. 함수는 많은 객체를 탐색할 수 있어야 한다는 것이다.

디미터 법칙을 위반하는지 여부는 ctxt, options, scratchDir이 객체인지 자료구조인지에 달렸다.
객체라면 내부 구조를 숨겨야하므로 디미터 법칙을 위반, 자료구조는 디미터 법칙을 적용하지 않는다.

자료구조와 같이 사용했다면 Getter를 사용하는 것이 아니라, ctxt.options.scratchDir.absolutePath와 같이 사용하는 것이 좋겠다.

자료 구조는 무조건 함수 없이 공개 변수만 포함하고, 객체는 비공개 변수와 공개 함수만 포함한다면 문제 없다.
하지만 단순한 자료구조에도 Getter, Setter를 정의하는 프레임워크와 표준이 존재한다.

잡종 구조 (안티 패턴)

이러한 혼란은 절반은 객체, 절반은 자료구조인 잡종 구조를 야기한다.
중요한 기능을 수행하는 함수도 표함하고, 공개 변수나 Getter, Setter도 있다.

이러한 모습은 객체와 자료구조의 단점만 모아 놓은 구조이기 때문에 피하는 편이 좋다.

구조체 감추기 (해결)

ctxt, Options, ScratchDir가 객체라면?
앞선 코드와 같이 모두 엮어서는 안된다. (객체는 내부 구조를 감춰야 하기 때문에)

앞서 설명한 내용과 같이 private으로 변수를 감추더라도 Getter로 가져올 수 있다면 내부 구조를 감춘 것이 아니다.

ctxt.getAbsolutePathOfScratchDriectoryOption(); // 1번

ctxt.getScratchDirectoryOption().getAbsolutePath(); // 2번

1번의 경우 ctxt에 공개해야 하는 메서드가 너무 많아진다.
2번의 경우 getScratchDirectoryOption() 메서드가 객체가 아닌 자료구조를 반환한다고 가정한다.
두 방법 모두 좋은 것은 아닌 것 같다.

ctxt가 객체라면 내부구조를 보여라라고 하는 것이 아니라 뭔가를 하라고 해야 한다.
ctxt 객체가 절대 경로를 얻어야 하는 이유는 임시 파일을 생성하기 위한 목적이 있다.
그렇다면 위와 같은 로직이 아니라 임시파일을 생성하는 코드를 작성한다면?
이러한 코드를 구현하면 모듈에서 몰라야 하는 여러 객체를 탐색할 필요가 없고, 디미터 법칙을 위반하지 않는다.

자료 전달 객체

자료 전달 객체Data Transfer Object, DTO는 공개 변수만 있고 함수가 없는 클래스이다.

좀 더 일반적인 형태는 빈bean구조이다. 빈은 비공개 변수를 Getter, Setter로 조작한다.

빈(Bean) 예시:

class Address {
    #street: string;
    #city: string;
    #state: string;

    constructor(street: string, city: string, state: string) {
        this.#street = street;
        this.#city = city;
        this.#state = state;
    }

    getStreet(): string {
        return this.#street;
    }

    getCity(): string {
        return this.#city;
    }

    getState(): string {
        return this.#state;
    }
}

결론

객체는 동작을 공개하고 자료를 숨긴다.
자료구조는 별다른 동작 없이 자료를 노출한다.

시스템을 구현할 때, 새로운 자료 타입을 추가하는 유연성이 필요하다면 객체가 적합하다.
새로운 동작을 추가하는 유연성이 필요하다면 자료구조가 적합하다.