객체와 자료구조

객체 vs 자료구조

자료구조는 단순히 데이터를 모아놓은 구조체 형태로, 내부 데이터를 노출한다. (JSON 같은 형태를 떠올려 보자.)

class Vihicle {
  public wheel: Wheel
  public speed: number
}
ts

위와 같은 클래스가 있을 때, Vihicle 인스턴스의 모든 데이터는 노출된다. 그러므로 자료구조라고 할 수 있다.

그렇다면 아래와 같이 getter/setter를 통해 다루게 한다면 자료구조라고 볼 수 있을까?

class Vihicle {
  public wheel: Wheel
  public speed: number

  public getSpeed() {}
  public setSpeed() {}
  public getWheel() {}
  public setWheel() {}
}
ts

이렇게 한다해도 구현을 외부로 노출하는 셈이 되므로 자료구조와 동일하다. 구현을 감추기 위해서는 추상화가 필요하다. 아래 코드를 보자.

class Vihicle {
  private wheel: Wheel
  private speed: number

  public accelerate(amount: number) {}
}
ts

이렇게 내부 구현을 감추고, 메서드를 통해 추상화를 한 경우에는 객체라고 부른다. 이렇게 자료구조와 객체는 서로 상반되는데, 코드 변경의 관점에서 자료구조와 객체의 차이점을 살펴보자.

class Car {
  public speed = 0
}

class Truck {
  public speed = 0
}

function accelerate(vehicle: Object) {
  if (vehicle instanceof Car) {
    vehicle.speed += 10
  } else if (vehicle instanceof Truck) {
    vehicle.speed += 5
  }
}
ts

위 코드는 매우 간단한 예시로, Car, Truck 이라는 자료구조가 있고, 동작은 외부 함수로 나타낸다.

이 때 속도를 줄이는 break라는 함수를 추가한다고 생각해보자. 아래와 같이 break 함수 하나만 추가해주면 된다.

function break(vihicle: Object) {
  if (vihicle instanceof Car) {
    vihicle.speed -= 10;
  }
  if (vihicle instanceof Truck) {
    vihicle.speed -= 5;
  }
}
ts

하지만 새로운 Tank라는 자료구조를 새로 추가한다고 하면, 모든 함수에 Tank를 처리하는 코드를 추가해줘야 한다.

자료구조를 이용한 절차지향적 코드는 새로운 함수를 추가할 때는 한 군데만 수정하면 되지만, 새로운 자료구조를 추가할 때는 여러 군데를 수정해야 한다.

function accelerate(vehicle: Object) {
  if (vehicle instanceof Car) {
    vehicle.speed += 10;
  }
  else if (vehicle instanceof Truck) {
    vehicle.speed += 5;
  }
  // 여기도 추가되고
  else if (vehicle instanceof Tank) {
    vehicle.speed += 3;
  }
}

function break(vihicle: Object) {
  if (vihicle instanceof Car) {
    vihicle.speed -= 10;
  }
  else if (vihicle instanceof Truck) {
    vihicle.speed -= 5;
  }
  // 여기도 추가돼야 한다
  else if (vehicle instanceof Tank) {
    vehicle.speed += 3;
  }
}
ts

위에서 자료구조로 작성한 예시를 다시 객체로 구성해보자. 공통 메서드를 인터페이스로 추상화할 수 있다.

interface Vihicle {
  accelerate(): void
}

class Car implements Vihicle {
  private speed = 0

  public accelerate() {
    this.speed += 10
  }
}

class Truck implements Vihicle {
  private speed = 0

  public accelerate() {
    this.speed += 5
  }
}
ts

인터페이스에 break 메서드를 추가한다고 하면, 인터페이스를 구현하는 모든 클래스를 수정해야 한다.

interface Vihicle {
  accelerate(): void
  break(): void
}

class Car implements Vihicle {
  private speed = 0

  public accelerate() {
    this.speed += 10
  }

  public break() {
    this.speed -= 10
  }
}

class Truck implements Vihicle {
  private speed = 0

  public accelerate() {
    this.speed += 5
  }

  public break() {
    this.speed -= 5
  }
}
ts

하지만 새로운 Tank 클래스를 추가하는 경우, 클래스 파일 하나만 추가하면 된다.

class Tank implements Vihicle {
  private speed = 0

  public accelerate() {
    this.speed += 3
  }

  public break() {
    this.speed -= 3
  }
}
ts

정리해보자.

자료구조/객체를 추가할 때함수/메서드를 추가할 때
자료구조여러 곳을 수정한 곳만 수정
객체한 곳만 수정여러 곳을 수정

디미터 법칙

디미터 법칙. 또는 데메테르 법칙이라고 부른다. 객체의 조회 함수를 통해 내부 구조를 노출시키면 안된다는 법칙이다.

디미터 법칙은 자신과 관련이 적은 객체에 접근하는 것을 피해서 결합도를 낮추고, 결과적으로 변경하기 쉬운 코드를 작성하기 위한 법칙이다. 내부 구조가 공개된 자료구조에 대해서는 적용되지 않는다.

디미터 법칙의 좀 더 정확한 표현은 아래와 같다.

"클래스 C의 메서드 f는 다음과 같은 객체의 메서드만 호출해야 한다."

  • 클래스 C
  • f가 생성한 객체
  • f 인수로 넘어온 객체
  • C 인스턴스로 변수에 저장된 객체

하지만 법칙을 어렵게 외울 필요는 없다. 간단하게 생각해서 "."을 하나만 쓰려고 노력해보자.

예시로 아래 코드를 보자.

function applyDiscount(customer, orderId, discount) {
  const totals = customer.orders.find(orderId).getTotals()

  totals.grandTotal = totals.grandTotal - discount
  totals.discount = discount
}
ts

applyDiscount는 고객의 특정 주문에 할인을 적용하는 함수다. 이 함수는 고객의 주문목록(orders), 찾은 특정 주문의 합계(totals), 주문 총액(grandTotal), 할인액(discount) 등 전부를 알고 갱신해야 한다.

지금 상태는 변경에 취약한데, 할인을 적용하기 위해 조회한 객체 중 하나라도 변경되거나, 할인 정책이 변경되면 applyDiscount함수 또한 변경되어야 하기 때문이다.

그렇다면 어떻게 변경해서 디미터 법칙을 만족하는 코드로 바꿀 수 있을까?

Tell, Don't Ask(묻지 말고 시켜라)라는 행동원칙이 있다. 객체의 상태를 물어보게 되면 내부 캡슐화의 장점은 사라지고 내부 구현에 대한 지식이 여기저기 퍼지게 된다. 그래서 객체에게 묻지 말고 처리를 위임하는 원칙이다.

먼저, 할인 처리를 totals객체에게 위임하자.

function applyDiscount(customer, orderId, discount) {
  customer.orders.find(orderId).getTotals().applyDiscount(discount)
}
ts

마찬가지로, 고객의 orders 컬렉션을 묻지말고 바로 주문 객체를 얻어오도록 할 수 있다.

function applyDiscount(customer, orderId, discount) {
  customer.findOrder(orderId).getTotals().applyDiscount(discount)
}
ts

totals 또한 묻지 않고 주문 객체에게 바로 할인을 시킬 수 있다.

function applyDiscount(customer, orderId, discount) {
  customer.findOrder(orderId).applyDiscount(discount)
}
ts

마무리로 customer 객체에 applyDiscountToOrder 메서드를 추가할 수도 있지만, 이 정도도 괜찮은 것 같다.

DTO, Active Record

DTO

자료 구조를 실제로 사용하는 대표적인 형태는 공개 변수만 있고 함수가 없는 클래스다. 이런 자료 구조체를 때로는 DTO(Data Tansfer Object)라고 한다. DTO는 주로 데이터베이스, 클라이언트와 통신할 때 사용된다.

Bean

자료구조의 좀 더 일반적인 형태로 Bean구조가 있다. 빈은 비공개 필드를 getter/setter로 조작하여 캡슐화하는 패턴이다.

Active Record

DTO의 특수한 형태로 활성 레코드(Active Record)가 있다. 비공개 변수에 조회/설정 함수가 있는 자료 구조에 CRUD에 대한 함수도 제공한다. 주로 데이터베이스를 조작하기 위해 사용한다.