Thief of Wealth

캡슐화

모듈을 분리하는 가장 중요한 기준은 아마도 시스템에서 각 모듈이 자신을 제외한 다른 부분에 드러내지 않아야할 비밀을 얼마나 잘 숨기느냐에 있다.

이러한 비밀 중 대표적인 형태인 데이터 구조는 레코드 캡슐화하기컬렉션 캡슐화하기로 캡슐화해서 숨길 수 있다.

심지어 기본형 데이터도 기본형을 객체로 바꾸기로 캡슐화할 수 있다.
리팩터링할 때 임시 변수가 자주 걸리적 거리는데, 정확한 순서대로 계산해야하고 리팩터링 후에도 그 값을 사용하는 코드에서 접근할 수 있어야 하기 때문이다. <= 이때는 임시 변수를 질의함수로 바꾸기가 사용된다. (특히 길이가 너무 긴 함수를 쪼갤 때 유용)

클래스는 원래 정보를 숨기는 용도로 설계되었다.
그 뿐만 아니라 클래스는 내부 정보클래스 사이의 연결 관계를 숨기는 데에도 유용하다.
그래도 너무 많이 숨기면 인퍼페이스가 비대해질 수 있으니 유의하자. (중개자 제거하기 기법이 사용됨)

레코드 캡슐화하기

배경

거의 모든 프로그래밍 언어는 데이터를 레코드 형태로 제공한다.
레코드가 연관된 여러 데이터를 묶어서 의미있는 단위로 전달할 수 있게 해주기 때문이다.
그런데 단순 레코드는 단점이 있다.
=> 계산해서 얻을 수 있는 값과, 그렇지 않은 값을 명확히 구분해서 저장해야하는 것이 번거롭다.

레코드는 2가지로 구분된다.

  1. 필드 이름을 노출하는 레코드
  2. 필드 이름을 외부로 부터 숨겨서 내가 원하는 이름을 쓸 수 있게하는 레코드
    => 해시, 맵, 해시맵, 딕셔너리, 연관 배열 등

많은 프로그래밍 언어가 해시맵을 쉽게 만드는 문법을 제공하고, 작업에도 유용할 수 있으나, 필드를 명확히 알려주지 않는다는것이 단점이 될 수 있다.

리팩터링 절차

  1. 레코드를 담을 변수를 캡슐화 한다.
  2. 레코드를 감싼 단순한 클래스로 해당 변수의 내용을 교체
  3. 테스트
  4. 원본 레코드 대신에 새로 정의한 클래스 타입의 객체를 반환하는 함수를 새로 생성
  5. 클래스에서 원본데이터를 반환하는 접근자와 원본 레코드를 반환하는 함수 제거
간단한 예시
const organization = {name: "하루", country: "KO"}

위 상수는 프로그램 곳곳에서 레코드 구조로 사용되는 js 객체이고,
코드 베이스 상에서 다음과 같이 reac/write된다고 가정하자.

result += `<h1>${organization.name}</h1>`; // read
organization.name = newName; // write
  1. 레코드를 담을 변수를 캡슐화 한다.
function getRawDataOfOrganization(){return organization._data;}

그러면 read/write 코드는 다음과 같이 바뀐다.

result += `<h1>${getRawDataOfOrganization().name}</h1>`; // read
getRawDataOfOrganization().name = newName; // write
  1. 레코드를 클래스로 바꾼다.
class Organization {
  constructor(data){
    this._data = data;
  }
}
  1. 어디선가 테스팅

  2. 새 클래스의 인스턴스를 반환하는 함수를 만든다.

const organization = new Organization({
  name: "도비",
  country: "US"
});
function getRawDataOfOrganization(){return organization._data;}
function getOrganization(){return organization;}
  1. 레코드를 사용하던 코드를 살펴보고, 레코드를 갱신하던 코드를 모두 setter를 사용하도록 교체하고, 레코드를 읽는 코드는 getter를 사용하도록 교체한다.
set name(aString) {this._data.name = aString;}
getOrganization().name = newName;

get name() {return this._data.name;}
result += `<h1>${getOrganization().name}</h1>`;
  1. 다 바꿨으면 아까 이상한 이름으로 지었던 임시함수 getRawDataOfOrganization를 제거한다.

완성본은 다음과 같다.

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

  get name() {return this._name;}
  set name(aString) {this._name = aString;}

  get country() {return this._country;}
  set country(aCountryCode) {this._country = aCountryCode;}
}
복잡한 예시 (중첩된 레코드 캡슐화 하기)

다음과 같은 레코드가 있다고 가정해보자

"1920": {
  name: "심바",
  id: "1920",
  usages: {
    "2016": {
      "1": 50,
      "2": 55,
      // 나머지 month 생략
    },
    "2015": {
      "1": 70,
      "2": 63,
      // 나머지 month 생략
    }
  }
},
"38673": {
  name: "다른 사람 이름",
  id: "38673",
  ... // 다른 고객 정보도 똑같이 작성됨.
}

겪어 본 사람들은 알겠지만, 중첩 정도가 심할수록, 읽거나 쓸 때 데이터 구조 안으로 더 깊숙히 들어가야 합니다.

이렇게요

// read
function compareUsage(customerID, laterYear, month){
  const later = customerData[customerID].usages[laterYear][month];
  const earlier = customerData[cusromerID].usages[laterYear - 1][month];

  return {
    laterAmount: later,
    chage: later - earlier
  };
}

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

크으.. 굉장히 사용하기 더럽습니다.

이런 중첩된 레코드도 앞에서와 마찬가지로 변수 캡슐화부터 해줍니다. (이름은 대충 지어도됨 어짜피 없앨거라;)

function getRawDataOfCustomers() {return customerData;}
function setRawDataOfCustomers(arg) {customerData = arg;}

이렇게 바꾸고

아까 read/write 하던 부분을 바꿔줍니다.

// ! cusromerData가 getRawDataOfCustomers()으로 바뀜

// read
function compareUsage(customerID, laterYear, month){
  const later = getRawDataOfCustomers()[customerID].usages[laterYear][month];
  const earlier = getRawDataOfCustomers()[cusromerID].usages[laterYear - 1][month];

  return {
    laterAmount: later,
    chage: later - earlier
  };
}

// write
getRawDataOfCustomers()[customerID].usages[year][month] = amount;

이제 전체 데이터 구조를 표현하는 클래스를 정의하고 그것을 반환하는 함수를 새로 만듭니다.

class CustomerData {
  constructor(data){
    this._data = data;
  }
}

function getCustomerData() {return customerData;}
function getRawDataOfCustomers() {return customerData._data;}
function setRawDataOfCustomers(arg) {customerData = new CustomerData(arg);}

function setUsage(customerID, year, month, amount){
  setRawDataOfCustomers()[customerID].usages[year][month] = amount;
}

이제 위 함수들을 클래스로 옮긴다. (함수 옮기기 기법 사용)

class CustomerData{
  constructor(data){
    this._data = data;
  }

  get rawData(){
    return _.cloneDeep(this._data);
  }

  setUsage(customerID, year, month, amount){
        this._data[customerID].usages[year][month] = amount;
  }
}

위 처럼, 읽기는 깊은 복사를 통해 처리해도 괜찮다.(단, 데이터 구조가 클수록 복제 비용이 커져서 성능이 느려질 수 있다. 한번 더 말하지만 성능은 측정해보고 그 때 판단해도 늦지 않다.)

getter로 객체를 반환하니까, 클라이언트가 원본을 수정하는 것 같은 착각이 들 수도 있는데 이런건, 읽기전욕 proxy를 만들어주던가 freeze를 해주어 예외를 발생시키는 것이 현명할 것이다.

profile on loading

Loading...