# 03-19. 내부자 거래


# ✋ Intro

  • 모듈 사이의 데이터 거래가 많아 결합도가 높아지는 것은 지양해야하지만 서비스를 구동하려면 어느 정도 내부적인 데이터 거래는 존재할 수 밖에 없다.

    • 하지만 그 양을 최소로 줄이고 모두 투명하게 처리해야 한다.
  • 만약 은밀하게 데이터를 주고받는 모듈이 있다면 함수 옮기기필드 옮기기 기법으로 떼어놓고 사적으로 처리하는 부분을 줄인다.

  • 같은 관심사를 공유하는 부분이 많다면 제3의 모듈을 새로 만들거나 위임 숨기기를 이용하여 다른 모듈이 중간자 역할을 하게 만든다.

  • 상속 구조에서는 부모 - 자식 사이에 결탁이 생길 때가 있다.

    • 부모 - 자식 사이 에서는 자식 클래스는 항상 부모 클래스가 공개하고 싶은 것 이상으로 부모에 대해 알려고 하는 경우가 있다.
    • 그러다가 부모 품을 떠나야 할 때가 온다면 서브클래스를 위임으로 바꾸기슈퍼클래스를 위임으로 바꾸기 기법을 활용하면 된다.


# (1) 서브클래스를 위임으로 바꾸기

  • 상속 구조의 가장 명확한 단점은 한 번만 쓸 수 있는 카드라는 것이다.

    • 무언가가 달라져야 하는 이유가 여러 개여도 상속에서는 그중 단 하나의 이유만 선택해 기준으로 삼을 수밖에 없다.
  • 또 다른 문제로, 상속은 클래스들의 관계를 아주 긴밀하게 결합한다.

    • 부모를 수정하면 이미 존재하는 자식들의 기능을 해치기가 쉽기 때문에 각별히 주의해야 한다.
    • 그래서 자식들이 슈퍼클래스를 어떻게 상속해 쓰는지를 이해해야 한다.
  • 위임(delegate)은 이상의 두 문제를 모두 해결해준다.

    • 위임은 객체 사이의 일반적인 관계이므로 상호작용에 필요한 인터페이스를 명확히 정의할 수 있다.
    • 즉, 상속보다 결합도가 훨씬 약하다.
  • 조심해야할 부분은 반드시 상속이 나쁘다는 것은 아니다!

    • 상속을 과용하는 데 따른 반작용이 있을 수 있으므로 적절하게 상속과 위임을 사용하라는 의미이다.

# (2) 슈퍼클래스를 위임으로 바꾸기

  • 객체 지향 프로그래밍에서 상속은 기존 기능을 재활용하는 강력하고 손쉬운 수단이다.
    • 기존 클래스를 상속하여 입맛에 맞게 오버라이드하거나 새 기능을 추가하면 된다.
    • 하지만 상속이 혼란과 복잡도를 키우는 방식으로 이뤄지기도 한다.
  • 슈퍼클래스의 기능들이 서브클래스에는 어울리지 않는다면 그 기능들을 상속을 통해 이용하면 안 된다는 신호다.
  • 제대로 된 상속이라면 서브클래스가 슈퍼클래스의 모든 기능을 사용함은 물론, 서브클래스의 인스턴스를 슈퍼클래스의 인스턴스로도 취급할 수 있어야 한다.
  • 위임을 이용하면 기능 일부만 빌려올 뿐인, 서로 별개인 개념이 명확해진다.
  • 슈퍼/서브클래스는 강하게 결합된 관계라서 슈퍼클래스를 수정하면 서브클래스가 망가지기 쉽다.
  • 단, 무조건 상속을 절대 사용하지 마라는 의미는 아니다.
    • 상위 타입의 모든 메서드가 하위 타입에도 적용되고, 하위 타입의 모든 인스턴스가 상위 타입의 인스턴스도 되는 등, 의미상 적합한 조건이라면 상속은 간단하고 효과적인 메커니즘이다.
    • 상속을 먼저 적용하고 나중에 문제가 생기면 슈퍼클래스를 위임으로 바꾸는 것도 좋다.

  • 리팩토링 적용 예시
// before

class CatalogItem {
  constructor(id, title, tags) {
    this._id = id;
    this._title = title;
    this._tags = tags;
  }
    
  get id() {
    return this._id;
  }
    
  get title() {
    return this._title;
  }
    
  hasTag(arg) {
    return this._tags.includes(arg);
  }
}

class Scroll extends CatalogItem {
  constructor(id, title, tags, dataLastCleaned) {
    super(id, title, tags);
    this._lastCleaned = dataLastCleaned;
  }
    
  needsCleaning(targetDate) {
    const threshold = this.hasTag('revered') ? 700 : 1500;
    return this.daysSinceLastCleaning(targetDate) > threshold;
  }
    
  daysSinceLastCleaning(targetDate) {
    return this._lastCleaned.until(targetDate, ChronoUnit.DAYS);
  }
}
  • 위 코드에서 물리적인 스크롤과 논리적인 카탈로그 아이템에는 차이가 있다.
    • 예컨대 석화병 치료법을 적어 놓은 스크롤은 사본이 여러 개임에도 카탈로그 아이템은 하나뿐이었다.
  • 제목과 태그로 사본을 구별할 수 있다.
    • 데이터가 절대 변하지 않는다면 이런 대표성이 문제를 해결해줄 것이다.
    • 하지만 사본 중 하나의 내용을 수정해야 한다면 같은 카테고리 항목의 다른 사본들 모두가 올바르게 수정되는지를 주의해서 확인해야 한다.
  • 카탈로그 아이템을 스크롤의 슈퍼클래스로 사용한다면 미래에 이 코드를 읽는 개발자에게 혼란을 줄 수 있어 좋은 모델은 아니다.
// after

class Scroll {
  constructor(id, title, tags, dataLastCleaned) {
    this._catalogItem = new CatalogItem(id, title, tags);
    this._lastCleaned = dataLastCleaned;
  }
    
  get id() {
    return this._catalogItem.id;
  }
    
  get title() {
    return this._catalogItem.title;
  }
    
  hasTag(aString) {
    return this._catalogItem.hasTag(aString);
  }
    
  needsCleaning(targetDate) {
    const threshold = this.hasTag('revered') ? 700 : 1500;
    return this.daysSinceLastCleaning(targetDate) > threshold;
  }
    
  daysSinceLastCleaning(targetDate) {
    return this._lastCleaned.until(targetDate, ChronoUnit.DAYS);
  }
}
// after2 (값을 참조로 바꾸기)

class Scroll {
  constructor(id, dataLastCleaned, catalogID, catalog) {
    this._id = id;
    this._catalogItem = catalog.get(catalogID);
    this._lastCleaned = dataLastCleaned;
  }
    
  get id() {
    return this._id;
  }
    
  get title() {
    return this._catalogItem.title;
  }
    
  hasTag(aString) {
    return this._catalogItem.hasTag(aString);
  }
    
  needsCleaning(targetDate) {
    const threshold = this.hasTag('revered') ? 700 : 1500;
    return this.daysSinceLastCleaning(targetDate) > threshold;
  }
    
  daysSinceLastCleaning(targetDate) {
    return this._lastCleaned.until(targetDate, ChronoUnit.DAYS);
  }
}