본문 바로가기

General

[클린 코드] 6장 객체 vs 데이터구조

자료 추상화

자료를 세세하게 공개하기 보다는, 추상적인 개념으로 표현하는 편이 좋다. 인터페이스나 조회/설정 함수만으로는 추상화가 이루어지지 않는다. 개발자는 객체가 포함하는 자료를 표현할 가장 좋은 방법을 심각하게 고민해야 한다. 아무 생각 없이 조회/설정 함수를 추가하는 방법이 가장 나쁘다.

// ❌
public class Point {
  public x: number;
  public y: number;
}

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

추상 인터페이스를 제공해, 사용자가 구현을 모른 채 자료의 핵심을 조작할 수 있어야 진정한 의미의 클래스다. 구현을 감추려면 추상화가 필요하다. 중간에 함수라는 계층을 넣는다고 구현이 저절로 감춰지지는 않는다.  

// ❌
public interface Vehicle {
  getFuelTankCapacityInGallons(): number;
  getGasolineInGallons(): number;
}

// ✅
public interface Vehicle {
  getPercentFuelRemaing(): number;
}

 

데이터/객체 비대칭

절차지향과 객체지향은 상호 보완적이다. 절차지향 코드에서 어려운 변경은, 객체지향 코드에서 쉬우며, vice versa이다.

데이터 구조(Data Structure) 데이터를 그대로 공개하며 별다른 함수를 제공하지는 않는다.

(데이터구조를 사용하는) 절차적인 코드는 기존 데이터 구조를 변경하지 않으면서 새 함수를 추가하기 쉽다. 반면 객체지향 코드는 기존 함수를 변경하지 않으면서 새 클래스를 추가하기 쉽다. 즉, 새로운 데이터 타입이 아니라 새로운 함수가 필요한 경우에는 절차지향적인(Procedual) 코드와 자료구조가 좀 더 적합하다. 

다음 절차지향적 클래스이다. area메서드 외에 새로운 함수를 추가하기가 용이하다. 하지만 새 도형 Oval 을 추가하고 싶다면 area메서드 등 Geometry 클래스에 속한 함수를 모두 고쳐야 한다.

// 절차지향적 클래스

class Sqaure {
  topLeft: Point;
  side: number;
}
class Rectangle {
  topLeft: Point;
  width: number;
  height: number;
}
class Circle {
  center: Point;
  radius: number;
}

class Geometry {
  PI = 3.141592;
  
  area(shape: Object) {
    if (shape instanceof Square) {
      return shape.side * shape.side
    }
    else if (shape instanceof Rectangle) {
      return shape.width * shape.height
    }
    else if (shape instanceof Circle) {
      return PI * shape.radius * shape.radius
    }
    else {
      throw new Error('no such shape');
    }
  }
}

 

반면, 객체(Object)는 추상화 뒤로 데이터를 숨긴 채, 데이터를 다루는 함수만 공개한다. 클래스와 객체지향(Object-Oriented) 기법은 새로운 함수가 아니라 새로운 데이터 타입이 필요한 경우에 더 적합하다.

객체지향적으로 작성하면 다음과 같다. 여기서 area메서드는 다형(polymorphic) 메서드이다. 새 도형을 Oval 추가해도 기존 클래스에 아무런 영향을 미치지 않는다. 반면 새 함수를 추가하고 싶다면 도형 클래스 전부를 고쳐야 한다.

// 객체지향적 클래스

class Sqaure implements Shape {
  topLeft: Point;
  side: number;
  
  area() {
    return this.side * this.side
  }
}

class Rectangle implements Shape {
  topLeft: Point;
  width: number;
  height: number;
  
  area() {
    return this.width * this.height
  }
}
class Circle implements Shape {
  PI = 3.141592;
  center: Point;
  radius: number;
  
  area() {
    return PI * this.radius * this.radius
  }
}

 

 

디미터 법칙 The Law of Demeter

a module should not know about the innards of the objects it manipulates.

- Each unit should have only limited knowledge about other units: only units "closely" related to the current unit.
- Each unit should only talk to its friends; don't talk to strangers.
- Only talk to your immediate friends.

 

디미터 법칙은 '모듈은 자신이 조작하는 객체의 속사정을 몰라야 한다.'는 법칙이다. (이름은 1987년 노스이스턴 대학의 디미터 프로젝트에서 기원했다.)

객체는 자료를 숨기고 함수를 공개한다. 

  • 클래스 MyClass 내부의 메서드 func가 호출할 수 있는 메서드
    • MyClass 클래스의 메서드
    • func가 생성한 객체의 메서드
    • func의 인수로 넘어온 객체의 메서드
    • MyClass 인스턴스 변수에 저장된 객체의 메서드
  • 클래스 MyClass 내부의 메서드 func가 호출할 수 없는 메서드
    • 위의 허용된 메서드가 반환하는 객체의 메서드
// ❌ 기차충돌 train wreck
const outputDir: string = ctxt.getOptions().getScratchDir().getAbsolutePath();

// 이렇게 나누는 편이 좋다.
const opts: Options = ctxt.getOptions();
const scratchDir: File = opts.getScratchDir();
const outputDir: string = scratchDir.getAbsolutePath();

 위 예제들이 디미터 법칙을 위반하는지 여부는 ctxt, Options, ScratchDir이 객체인지 데이터 구조인지에 달렸다. 객체라면 내부 구조를 숨겨야하므로 디미터 법칙을 위반한다. 단순 데이터 구조라면 당연히 내부 구조를 노출하므로 디미터 법칙이 적용되지 않는다. 

그런데 위 함수에서는 조회함수를 사용해서 혼란이 생긴다. 데이터 구조는 무조건 함수없이 공개 변수만 포함하고, 객체는 비공개 변수와 공개함수를 포함한다면, 문제가 간단해진다. 다음과 같이 구현했다면 디미터 법칙을 거론할 필요가 없어진다. 

// ✅
const outputDir: string = ctxt.options.scratchDir.absolutePath;

 

그러나 단순한 데이터 구조에도 조회함수와 설정함수를 정의하라 요구하는 프레임워크와 표준이 존재한다. 이런 혼란으로 반은 데이터 구조, 나머지 반은 객체인 잡종 구조가 나오기도 한다. 잡종 구조는 새로운 함수도, 새로운 데이터 타입도 추가하기 어려워 피하는 편이 좋다. 

ctxt 등이 진짜 객체라면, 이들에게 뭔가를 하라고 해야지, 속을 드러내라고 말하면 안된다. 여기서 ctxt는 내부 구조를 드러내지 않고, 모듈에서 해당 함수는 자신이 몰라야하는 여러 객체를 탐색할 필요가 없다.

const bos = ctxt.createScratchFilestream(classFileName);

 

데이터 전달 객체 DTO(Data Transfer Object)

DTO는 데이터베이스와 통신하거나 소켓에서 받은 가공되지 않은 정보를 애플리케이션 코드에서 사용할 객체로 변환하는 가장 첫 단계에서 사용하는 구조체를 말한다. DTO는 공개 변수만 있고 함수가 없다. 

공개변수가 있거나 비공개 변수에 조회 설정 함수가 있는 특수한 형태의 DTO를 활성레코드라고 부른다. save, find와 같은 탐색함수도 제공한다. 활성 레코드에 비즈니스 규칙 메서드를 추가해 데이터 구조가 아닌 객체로 다루는 개발자가 흔하다. 하지만 이 또한 잡종 구조이기 때문에 바람직하지 않다. 활성 레코드는 데이터 구조로 취급하는 것이 좋다.

 

 

출처: 도서 클린코드 애자일 소프트웨어 장인 정신, 로버트 C.마틴 지음 | 박재호, 이해영

 

'General' 카테고리의 다른 글

[클린 코드] 8장 경계  (0) 2023.05.28
[클린 코드] 7장 오류 처리  (0) 2023.05.25
[클린 코드] 5장 포맷 맞추기  (1) 2023.05.18
[클린 코드] 4장 주석  (0) 2023.05.06
[클린 코드] 3장 함수  (0) 2023.04.23