[개발 도서] 리팩터링 2판 chapter7. 캡슐화

호박쿵야·2022년 2월 7일
0

7.1 레코드 캡슐화하기

레코드란?
컴퓨터 과학에서 레코드(record, struct)는 기본적인 자료 구조이다. DB나 스프레드시트의 레코드는 보통 로우(row)라고 부른다.
레코드는 각기 다른 자료형에 속할 수 있는 필드의 모임이며, 보통 고정 숫자나 시퀀스로 이루어져있다. OOP에서는 멤버(member)로도 부른다.

//c 언어에서의 레코드
	struct data {
    	int year;
        int month;
        int day;
    }

간단한 변수 캡슐화

Before

const organization = { name: "애크미 구스베리", country: "GB" };

Refactored

class Organization {
  constructor(data) {
    this._name = data.name;
    this._country = data.country;
  }
  get name() {
    return this._name;
  }
  set name(arg) {
    this._name = arg;
  }
  get country() {
    return this._country;
  }
  set country(arg) {
    this._country;
  }
}

레코드를 캡슐화하는 목적은 변수 자체는 물론 그 내용을 조작하는 방식도 통제하기 위해서이다. -> 레코드를 클래스로 바구고, 새 클래스의 인스턴스를 반환하는 함수를 새로 만든다.

중첩된 레코드 캡슐화하기

Before

const data = {
  1920: {
    name: "마틴 파울러",
    id: "1920",
    usages: {
      2016: {
        1: 50,
        2: 55,
        "...": "...",
        12: 41,
      },
      2015: {
        1: 50,
        2: 55,
        "...": "...",
        12: 41,
      },
    },
  },
  38673: {
    name: "닐 포드",
    id: "38673",
    usages: {
      2016: {
        1: 50,
        2: 55,
        "...": "...",
        12: 41,
      },
      2015: {
        1: 50,
        2: 55,
        "...": "...",
        12: 41,
      },
    },
  },
};

Refactored

class CustomerData {
  constructor(data) {
    this._data = data;
  }
  setUsage(customerID, year, month, amount) {
    this._data[customerID].usages[year][month] = amount;
  }
  usage(customerID, year, month) {
    return this._data[customerID].usages[year][month];
  }
  get rawData() {
    return _.cloneDeep(this._data);
  }
}

const customerData = new CustomerData(data);

function getCustomerData() {
  return customerData;
}

덩치 큰 데이터 구조를 다룰수록 쓰기(write) 부분에 집중한다. 캡슐화에서는 값을 수정하는 부분을 명확하게 드러내고 한 곳에 모아두는 일이 굉장히 중요하다.

깊은 복사는 lodash 라이브러리가 제공하는 cloneDeep() 으로 처리한다.

리팩터링 후 쓰기 부분

Before

 //write
data[customerID].usages[year][month] = amount;

//read
function compareUsage(customerID, lastYear, month) {
  const later = data[customerID].usages[lastYear][month];
  const earlier = data[customerID].usages[lastYear - 1][month];
  return { laterAmount: later, change: later - earlier };
}

Refactored

//read
function compareUsage_FirstWay(customerID, lastYear, month) {
  const later = getCustomerData().usage(customerID, lastYear, month);
  const earlier = getCustomerData().usage(customerID, lastYear - 1, month);
  return { laterAmount: later, change: later - earlier };
}

function compareUsage_SecondWay(customerID, lastYear, month) {
  const later = getCustomerData().rawData[customerID].usages[lastYear][month];
  const earlier =
    getCustomerData().rawData[customerID].usages[lastYear - 1][month];
  return { laterAmount: later, change: later - earlier };
}

첫번째 방법의 이점은 customerData의 모든 쓰임을 명시적인 API 로 제공한다는 것이다.
두번째 방법은 내부 데이터를 복제해서 제공하는데, 복제 비용이 성능에 영향을 줄 수 있고, 클라이언트가 원본을 수정한다고 착각할 수 있다.

7.2 컬렉션 캡슐화하기

가변 데이터는 모두 캡슐화 하는 편이다 -> 데이터 구조가 언제 어떻게 수정되는지 파악하기 쉬워서 필요한 시점에 데이터 구조를 변경하기도 쉬워지기 때문!

예를 들어, 컬렉션 변수로의 접근을 캡슐화하면서 getter가 컬렉션 자체를 반환하도록 한다면, 그 컬렉션을 감싼 클래스가 눈치채지 못하는 상태에서 컬렉션의 원소들이 바뀔 수 있다.

이와 같은 문제를 예방하기 위해 컬렉션을 감싼 클래스에 add-(), remove-()라는 이름의 컬렉션 변경자 메서드를 만든다. 이런식으로 컬렉션을 소유한 클래스를 통해서만 원소를 변경하도록 하면 프로그램을 개선하면서 컬렉션 변경 방식도 원하는 대로 수정할 수 있다.

또한 컬렉션 getter가 원본 컬렉션을 반환하지 않게 만들어 실수로 컬렉션을 변경할 가능성을 차단하는 것이 낫다.
-> 컬렉션에 접근하려면 컬렉션이 소속된 클래스의 적절한 메서드를 반드시 거치도록 하는 것
: aCustomer.orders.size() => aCustomer.numberOfOrders()
위와 같은 방식이 있지만 최신 언어는 다양한 컬렌션 클래스들을 표준화된 인터페이스로 제공하며, 컬렉션 파이프라인과 같은 패턴을 적용하여 다채롭게 조합할 수 있다.
-> 컬렉션을 읽기 전용으로 제공하는 것
: '컬렉션 getter를 제공하되, 내부 컬렉션의 복제본을 반환하는 것'
컬렉션이 상당히 크다면 성능 문제가 발생할 수 있다. 하지만 성능에 지장을 줄만큼 컬렉션이 큰 경우는 별로 없으니 성능에 대한 일반 규칙을 따르자!

Before

class Person {
  get courses() {
    return this._courses;
  }
  set courses(aList) {
    this._courses = aList;
  }
}

Refactored

class Person {
  get courses() {
    return this._courses.slice();
  }
  addCourse(aCourse) {}
  removeCourse(aCourse) {}
}

7.3 기본형을 객체로 바꾸기

단순한 출력 이상의 기능이 필요해지는 순간 숫자나 문자열 같은 데이터 항목을 표현하는 전용 클래스를 정의한다. 시작은 기본형 데이터를 단순히 감싼 것과 큰 차이가 없지만 나증에 특별한 동작이 필요해지면 이 클래스에 추가하면 되니 프로그램이 커질수록 점점 유용한 도구가 된다.

Before

orders.filter((o) => "high" === o.priority || "rush" === o.priority);

Refactored

orders.filter((o) => o.priority.higherThan(new Priority("normal")));

7.4 임시 변수를 질의 함수로 바꾸기

함수 안에서 어떤 코드의 결과값을 뒤에서 다시 참조할 목적으로 임시 변수를 쓰기도 한다. 임시 변수를 사용하면 값을 계산하는 코드가 반복되는 걸 줄이고 값의 의미를 설명할 수도 있어서 유용하다. 그런데 더 나아가 아예 함수로 만들어 사용하는 편이 더 나을 때가 많다.

해당 리팩터링은 클래스 안에서 적용할 때 효과가 가장 크다. 클래스는 추출할 메서드들에 공유 컨텍스트를 제공하기 때문이다. 클래스 바깥의 최상위 함수로 추출하면 매개변수가 너무 많아져서 함수를 사용하는 장점이 줄어든다.

자고로 변수는 값을 한번만 계산하고 그 뒤로는 읽기만 해야한다. 만약 변수에 값을 한번 대입한 뒤 더 복잡한 코드 덩어리에서 여러 차례 다시 대입해야하는 경우는 모두 질의 함수로 추출해야 한다.

Before

const basePrice = this._quantity * this._itemPrice;

if (basePrice > 1000) return basePrice * 0.95;
else return basePrice * 0.98;

Refactored

get basePrice(){return this._quantity * this._itemPrice}

if(this.basePrice > 1000) return this.basePrice *0.95
else return this.basePrice *0.98

7.5 클래스 추출하기

실무에서는 몇 가지 연산을 추가하고 데이터를 보강하면서 클래스가 점점 비대해지곤 한다. 기존 클래스를 굳이 쪼갤 필요까지는 없다고 생각하여 새로운 역할을 덧씌우기 쉬운데, 역할이 갈수록 많아지고 새끼를 치면서 클래스가 굉장히 복잡해진다.

메서드와 데이터가 너무 많은 클래스는 이해하기 쉽지 않으니 잘 살펴보고 적절히 분리하는 것이 좋다.

Before

class Person {
  get officeAreaCode() {
    return this._officeAreaCode;
  }
  get officeNumber() {
    return this._officeNumber;
  }
}

Refactored

class Person {
  get officeAreaCode() {
    return this._telephoneNumber.areaCode;
  }
  get officeNumber() {
    return this._telephoneNumber.number;
  }
}
class TelephoneNumber {
  get areaCode() {
    return this._areaCode;
  }
  get number() {
    return this._number;
  }
}

7.6 클래스 인라인하기

클래스 인라인하기는 클래스 추출하기를 거꾸로 돌리는 리팩터링이다. 더 이상 제 역할을 못해서 그대로 두면 안 되는 클래스는 인라인해버린다. 두 클래스의 기능을 지금과 다르게 배분하고 싶을 때도 클래스를 인라인한다. 클래스를 인라인해서 하나로 합친 다음 새로운 클래스를 추출하는 게 쉬울 수도 있기 때문이다.

Before

class Person {
  get officeAreaCode() {
    return this._telephoneNumber._officeAreaCode;
  }
  get officeNumber() {
    return this._telephoneNumber._officeNumber;
  }
}
class TelephoneNumber {
  get areaCode() {
    return this._areaCode;
  }
  get number() {
    return this._number;
  }
}

Refactored

class Person {
  get officeAreaCode() {
    return this._officeAreaCode;
  }
  get officeNumber() {
    return this._officeNumber;
  }
}

7.7 위임 숨기기

모듈화 설계를 제대로 하는 핵심은 캡슐화다. 캡슐화는 모듈들이 시스템의 다른 부분에 대해 알아야 할 내용을 줄여준다. 캡슐화가 잘 되어 있으면 무언가를 변경해야 할 때 함께 고려해야 할 모듈 수가 적어져서 코드를 변경하기 훨씬 쉬워진다.

위임 객체의 인터페이스가 바뀌면 이 인터페이스를 사용하는 모든 클라이언트가 코드를 수정해야 한다. 이러한 의존성을 없애려면 위임 메서드를 만들어서 위임 객체의 존재를 숨기면 된다.

Before

const manager = aPerson.department.manager

Refactored

const manager = aPerson.manager

Class Person{
    get manager(){return this._department.manager}
}

7.8 중개자 제거하기

클라이언트가 위임 객체의 또 다른 기능을 사용하고 싶을 때마다 서버에 위임 메서드를 추가해야 하는데, 이렇게 기능을 추가하다 보면 단순히 전달만 하는 위임 메서드들이 성가신다. 차라리 클라이언트가 위임 객체를 직접 호출하는 게 나을 수 있다.

어느 정도까지 숨겨야 적절한지를 판단하기란 쉽지 않지만, 필요하면 언제든 균형점을 옮길 수 있으니 괜찮다. ( 시스템이 바뀌면 '적절하다'의 기준도 바뀌기 마련이다 )

Before

manager = aPerson.manager

class Person{
    get manager(){return this.department.manager}
}

Refactored

manager = aPerson.department.manager

class Person{
    get department(){return this._department}
}

7.9 알고리즘 교체하기

리팩터링하면 복잡한 대상을 단순한 단위로 나눌 수 있지만, 때로는 알고리즘 전체를 걷어내고 훨씬 간결한 알고리즘으로 바꿔야 할 때가 있다.
문제를 더 확실히 이해하고 훨씬 쉽게 해결하는 방법을 발견했을 때 이렇게 한다. 내 코드와 똑같은 기능을 제공하는 라이브러리를 찾았을 때도 마찬가지다.

이 작업에 착수하려면 반드시 메서드를 가능한 한 잘게 나눴는지 확인해야 한다. 알고리즘을 간소화하는 작업부터 해야 교체가 쉬워진다.

Before

function foundPerson(people) {
  for (let i = 0; i < people.length; i++) {
    if (people[i] === "Don") {
      return "Don";
    }
    if (people[i] === "John") {
      return "John";
    }
    if (people[i] === "Amma") {
      return "Amma";
    }
  }
  return "";
}

Refactored

function foundPerson(people) {
  const candinates = ["Don", "John", "Amma"];
  return people.find((p) => candinates.includes(p) || "");
}

+++ 추가로 리팩터링 내용으로 참고하기 좋았던 영상 자료
https://www.youtube.com/watch?v=edWbHp_k_9Y

0개의 댓글