All Articles

레코드 캡슐화하기

레코드란 단어라 자체가 굉장히 생소하게 느껴진다. 컴퓨터 공학적인 단어로 표현해서 낯설지만, 사실 Array, Object등을 떠올리면 쉽다.

컴퓨터 과학에서 레코드(record, struct)는 기본적인 자료 구조이다. 데이터베이스나 스프레드시트의 레코드는 보통 로우(row)라고 부른다.

JS에서는 보통 아래와 같은 것 떠올리면 쉽다.

const organization = { name: "김요한", country: "KR" };

레코드의 단점이라면 계산해서 얻을 수 있는 값과, 그렇지 않고 직접 얻을 수 있는 값을 명확히 구분해서 저장해야 한다는 것이다.

값의 범위를 표현하는 레코드가 있다고 하자. 시작과 끝, 그리고 길이를 표현하려고 한다.

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

위와 같이 각기 다른 세가지 형식으로 레코드를 표현할 수 있는데, 어찌되었든 시작과 끝의 길이를 알 수 있어야 한다.

객체 사용

객체를 사용한다면 위 세가지 값(start, end, length)을 모두 메소드로 제공 가능하다.
사용자가 사용할 때 어떤 값이 저장된 형태로 가져오는 건지, 혹은 계산해서 가져오는 건지 알 필요가 없다.
또한 필드 이름이 바뀐다고 해도 각각의 메소드로 제공이 가능하다.

👉👉🏿👉🏽 즉, 레코드를 다루는 일이 많다면 이를 캡슐화 하는 것이 좋다.


레코드 값이 바뀔 일이 없다면? (레코드 값이 불변일 경우)

레코드 값이 불변이라면 위의 경우 단순히 시작과 끝의 길이를 모두 구해서 레코드에 저장한다.

const myRecord = { start: 1, end: 5, length: 5};


단순한 레코드 캡슐화하기

const organization = { name: "김요한", country: "KR" };

위와 같은 레코드가 있다고 하면 아래와 같이 사용할 것이다.

result += `<h1>${organization.name}</h1>`;
organization.name = newName;

이렇게 쓰는 경우가 많지만 이럴 경우 레코드가 바뀔 경우 수정하기가 어렵다. 또한 실수로 잘못 조작할 경우 원본 레코드 값이 수정될 위험이 항상 존재한다.

1단계 우선 상수를 캡슐화시킨다.

데이터는 참조하는 모든 부분을 한번에 바꿔야 코드가 제대로 작동한다.
따라서 데이터에 접근하려고 할 때에는 그 데이터로 접근을 독점하는 함수를 만드는 식으로 캡슐화하는 방법이 제일 좋다.

마틴 파울러의 경우 유효범위가 함수 하나보다 넓은 가변 데이터는 모두 캡슐화해서 그 함수를 통해서만 접근하게 변경한다고 말한다.


function getRawDataOfOrganization() {
  return organization;
}

result += `<h1>${getRawDataOfOrganization().name}</h1>`;
getRawDataOfOrganization().name = newName;

일단 이렇게 캡슐화를 하게 되면 변수는 물론이고 조작하는 방법도 통제가 가능하다.

2단계 Organization 클래스를 만든다.

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

const organization = new Organization({ name: "김요한", country: "KR" });

function getRawDataOfOrganization() {
  return organization._data;
}

function getOrganization() {
  return organization;
}


3단계 Organization 클래스 생성자 내에서 각 데이터들을 설정해준다.

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

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

  get country() {
    return this._country;
  }

  set country(arg) {
    this._country = arg;
  }
}

복잡한 레코드 캡슐화하기

하지만 보통 실무 레코드 중에서 더 복잡한 경우가 있다.

 "1920":{
   name:"김요한",
   id:"1920",
   usages:{
     "2022":{
       "1": 50,
       "2":55
     },
     "2021":{
       "1":70,
       "2":75,
     }
   }
 }

이렇게 중첩된 레코드는 더 까다롭다.

사용되는 곳 코드를 보면 아래와 같을 것이다.

const abc = (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 getRawDataOfCustomers() {
  return customerData;
}
function setRawDataOfCustomers(data) {
  customerData = data;
}

그럼 이렇게 바뀐다.

getRawDataOfCustomers()[customerID].usages[year][month] = amount;
function compareUsage(customerID, laterYear, month) {
  const later = getRawDataOfCustomers()[customerID].usages[laterYear][month];
  const earlier =
    getRawDataOfCustomers()[customerID].usages[laterYear - 1][month];
  return {
    laterAmount: later,
    change: later - earlier,
  };
}

어째 지금 상태는 더 복잡해진 느낌이다. 빨리 하나씩 캡슐화를 진행해보자.

단계 2: 전체 데이터 구조를 표현하는 클래스를 정의하고, 이를 반환하는 함수를 새로 만든다.

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

// 최상위에서는..
function getCustomerData() {
  return customerData;
}
function getRawDataOfCustomers() {
  return customerData._data;
}

function setRawDataOfCustomers(arg) {
  customerDataData = new CustomerData(arg);
}

3단계: 데이터 구조 안으로 깊이 들어가서 세터로 뽑아내는 작업을 한다.

function setUsage(customerId, year, month, amount) {
  return (getRawDataOfCustomers()[customerId].usages[year][month] = amount);
}
setUsage(customerId, year, month, amount);
// 사용하는 곳 
getCustomerData().setUsage(customerId, year, month, amount);

4단계: setUsage 함수를 고객 데이터 클래스로 옮긴다.

class CustomerData {
  constructor(data) {
    this._data = data;
  }
  setUsage(customerId, year, month, amount) {
    this._data[customerId].usages[year][month] = amount;
  }
}

값을 수정하는 부분을 명확하게 드러내고 한곳에 모아두는 일이 중요하다.

5단계: 빠진 부분이 없는지는 데이터를 깊은복사해서 반환하여 본다.

function getCustomerData() {
  return customerData;
}
function getRawDataOfCustomers() {
  return customerData._data;
}

function setRawDataOfCustomers(arg) {
  customerDataData = new CustomerData(arg);
}
class CustomerData {
  constructor(data) {
    this._data = data;
  }
  setUsage(customerId, year, month, amount) {
    this._data[customerId].usages[year][month] = amount;
  }
  get rawData() {
    return _.cloneDeep(this.data);  // 빠진 부분이 없는지 깊은 복사로 확인
  }
}

읽는 방법

읽는 방법은 여러가지가 있지만, 우선 읽는 코드를 모두 독립 함수로 추출하는 방법이 있다.

class CustomerData {
  constructor(data) {
    this._data = data;
  }
  setUsage(customerId, year, month, amount) {
    this._data[customerId].usages[year][month] = amount;
  }
  get rawData() {
    return _.cloneDeep(this.data);
  }
  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,
  };
}

또는 원본 데이터를 복사해서 줘도 된다. 하지만 cloneDeep을 도느라 조금 느려질 수 있다.

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