✏️7.1 레코드 캡슐화하기 (Encapsulate Record)

리팩터링 전

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

리팩터링 후

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

입력 데이터 레코드와의 연결을 끊어준다는 이점이 생긴다. 이 레코드를 참조하여 캡슐화를 깰 우려가 있는 코드가 많을 때가 좋다.

🧷 배경

📍레코드: 연관된 여러 데이터를 직관적인 방식으로 묶을 수 있어서 각각을 따로 취급할 때보다 훨씬 의미 있는 단위로 전달할 수 있게 해준다.

📍레코드의 단점: 계산해서 얻을 수 있는 값과 그렇지 않은 값을 명확히 구분해 저장해야 하는 점이 번거롭다.

{start: 1, end: 5} {start: 1, length: 5} 

와 같이 어떤 식으로 저장하든 시작과 끝과 길이를 알 수 있어야 한다.

→ 가변 데이터를 저장하는 용도로는 레코드보다 객체를 선호한다.

레코드 구조

  • 필드 이름을 노출하는 형태

  • (필드를 외부로 숨겨서) 내가 원하는 이름을 쓸 수 있는 형태

🧷 절차

  1. 레코드를 담은 변수를 캡슐화한다.

  2. 레코드를 감싼 단순한 클래스로 해당 변수의 내용을 교체한다. 이 클래스에 원본 레코드를 반환하는 접근자도 정의하고, 변수를 캡슐화하는 함수들이 이 접근자를 사용하도록 수정한다.

  3. 테스트한다.

  4. 원본 레코드 대신 새로 정의한 클래스 타입의 객체를 반환하는 함수들을 새로 만든다.

  5. 레코드를 반환하는 예전 함수를 사용하는 코드를 4에서 만든 새 함수를 사용하도록 바꾼다. 필드에 접근할 때는 객체의 접근자를 사용한다. 적절한 접근자가 없다면 추가한다. 한 부분을 바꿀 때마다 테스트한다.

  6. 클래스에서 원본 데이터를 반환하는 접근자와 원본 레코드를 반환하는 함수들을 제거한다.

  7. 테스트한다.

  8. 레코드의 필드도 데이터 구조인 중첩 구조라면 레코드 캡슐화하기와 컬렉션 캡슐화하기를 재귀적으로 적용한다.

🧷 리팩터링 전

중첩된 레코드 캡슐화하기

"1920":{
  name: "마틴 파울러",
  id: "1920",
  usages: {
    "2016": {
      "1": 50,
      "2": 55,
      // 나머지 달은 생략.
    },
    "2015":{
      "1": 70,
      "2": 63,
      // 나머지 달은 생략.
    }
  }
},
"38673":{
  name: "닐 포드",
  id: "38673",
  // 다른 고객 정보도 같은 형식으로 저장된다.
}

중첩 정도가 심할수록 읽거나 쓸 때 데이터 구조 안으로 더 깊숙히 들어가야 한다.

// 쓰기 예...
customerData[customerID].usages[year][month] = amount;

// 읽기 예...
function compareUsage (customerID, laterYear, month) {
  const later = customerData[customerID].usages[laterYear][month];
  const earlier = customerData[customerID].usages[laterYear-1][month];
  return {laterAmount: later, change: later - earlier};
}
  1. 변수캡슐화부터 시작한다.

function getRawDataOfCustomer() {return customerData;}
function setRawDataOfCustomer(arg) {customerData = arg;}

// 쓰기 예...
getRawDataOfCustomers()[customerID].usages[year][month] = amount;

// 읽기 예...
function compareUsage(customerID, laterYear, month) {
  const later = getRawDataCustomers()[customerID].usages[laterYear][month];
  const earlier = getRawDataOfCustomers()[customerID].usages[laterYear-1][month];
  return {laterAmount: later, change: later - earlier};
}
  1. 전체 데이터 구조를 표현하는 클래스를 CustomerData와 같이 정의하고 반환하는 함수로 새로 만든다.

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

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

여기서 중요한 부분은 데이터를 쓰는 코드다. getRawDataOfCustomers()를 호출한 후에 데이터를 변경할 때도 주의를 해야한다. 3. 고객 객체에는 세터가 없으니 데이터 구조 깊이 까지 들어가서 값을 바꾸는데 이를 보완하기 위해 아래와 같이 데이터 구조 안으로 들어가는 코드를 세터로 뽑아내는 작업을 해준다.

// 쓰기 예....
setUsage(customerID, year, month, amount);

// 최상위....
function setUsage(customerID, year, month, amonth){
  getRawDataOfCustomer()[customerID].usges[year][month] = amount;
}
  1. 이제 이 함수를 고객 데이터 클래스로 옮긴다.

// 쓰기 예...
getCustomerData().setUsage(customerID, year, month, amount);

// CustomerData 클래스...
setUsage(customerID, year, month, amount){
  this._data[customerID].usages[year][month] = amount;
}

캡슐화에서는 값을 수정하는 부분을 명확하게 드러내고 한 곳에 모아두는 일이 굉장히 중요하다.

  1. 우선 getRawDataOfCustomers()에서 데이터를 깊은 복사해서 반환하여 확인하는 방법이 있다.

// 최상위...
function getCustomerData() {return customerData;}
function getRawDataOfCustomer() {return customerData.rawData;}
function setRawDataOfCustomer(arg) {customerData = new CustomerData(arg);}


// CustomerData 클래스...
get rawData() {
  return _.cloneDeep(this._data)
}
  1. 깊은 복사는 lodash라이브러리의 cloneDeep()로 처리한다.

읽기는 어떻게 처리해야 할까? 첫번째로 세터 때와 같은 방법을 적용할 수 있다. 읽는 코드를 모두 독립함수로 추출한 다음 고객 데이터 클래스로 옮기는 것이다.

// CustomerData클래스
usage(customerID, year, month){
  return this._data[customerID].usages[year][month]
}
  
// 최상위...
function compareUsage (customerID, laterYear, month){
  const later = getCustomerData().usage(customerID, laterYear, month);
  const earlier = getCustomerData().usage(customerID, laterYear - 1, month);
  return {laterAmount: later, change: later - earlier};
}

이 방법의 장점은 customerData의 모든 쓰임을 명시적인 API로 제공한다는 것이다. 이 클래스만 보면 데이터 사용방법을 모두 파악할 수 있다. 그리고 다른 방법으로는 실제 데이터를 제공할 수도 있다.

하지만 클라이언트가 데이터를 직접 수정하지 못하게 막을 방법이 없어서 모든 쓰기를 함수 안에서 처리한다

  1. 가장 간단한 방법은 앞에서 작성한 rawData() 메서드를 사용하여 내부 데이터를 복제해서 제공하는 것이다.

// CusomerData 클래스
get rawData() {
  return _.cloneDeep(this._data);
}

// 최상위...
function compareUsage (customerID, laterYear, month){
  const later = getCustomerData().rawData[customerID].usages[lastYear][month];
  const earlier = getCustomerData().rawData[customerID].usages[lastYear - 1][month];
  return {laterAmount: later, change: later - earlier};
}

두번째 방법은 레코드 캡슐화를 재귀적으로 하는 것으로써 가장 확실하게 제어할 수 있다. 이방법을 적용하려면 고객 정보 레코드를 클래스로 바꾸고, 바로 뒤에 나올 컬렉션 캡슐화하기로 레코드를 다루는 코드를 리팩터링해서 고객 정보를 다루는 클래스를 생성한다.

때로는 새로만든 클래스와 게터를 잘 혼합해서, 게터는 데이터 구조를 깊이 탐색하게 만들고, 원본 데이터를 그대로 반환하지 말고 객체로 감싸서 반환하는게 효과적일 수 있다.

Last updated