Skip to main content

9장 데이터 조직화

written by
Mengkki
Mengkki 🏆Front End Engineer

9-1 변수 쪼개기#

배경#

변수에 값을 여러번 대입할 수 밖에 없는 경우가 아닌 이상 변수에는 값을 단 한번만 대입해야 한다.

대입이 두 번 이상 이뤄진다면 그 변수가 여러 가지 역할을 수행한다는 신호다. 역할이 둘 이상인 변수가 있다면 쪼개야한다!

절차#

  1. 변수를 선언한 곳과 값을 처음 대입하는 곳에서 변수 이름을 바꾼다
    1. 이후의 대입이 항상 i = i + 어쩌구 형태라면 수집 변수이므로 쪼개면 안 된다. 수집 변수는 총합 계산, 문자열 연결, 스트림에 쓰기, 컬렉션에 추가하기 등의 용도로 흔히 쓰인다
  2. 가능하면 이때 불변으로 선언한다
  3. 이 변수에 두 번째로 값을 대입하는 곳 앞까지의 모든 참조(이 변수가 쓰인 곳)을 새로운 변수 이름으로 바꾼다
  4. 두 번째 대입 시 변수를 원래 이름으로 다시 선언한다.
  5. 테스트한다
  6. 반복한다. 매 반복에서 변수를 새로운 이름으로 선언하고 다음번 대입 때까지의 모든 참조를 새 변수명으로 바꾼다. 이 과정을 마지막 대입까지 반복한다.

예시#

function distanceTravelled(scenario, time) {  let result;  let acc = scenario.primaryForce / scenario.mass;  let primaryTime = Math.min(time, scenario.delay);
  result = 0.5 * acc * primaryTime * primaryTime;
  let secondaryTime = time - scenario.delay;
  if(secondaryTime > 0) {    let primaryVelocity = acc * scenario.delay;    acc = (scenario.primaryForce + scenario.secondaryForce) / scenario.mess;
    ...  }
  return result;}

acc 변수에 값이 두번 대입되었다. 역할이 두개라는 신호!

하나는 첫번째 힘이 유발한 초기 가속도를 저장하는 역할이고, 다른 하나는 두번째 힘까지 반영된 후의 가속도를 저장하는 역할이다. 쪼개버립시다

function distanceTravelled(scenario, time) {  let result;  const primaryAcceleration = scenario.primaryForce / scenario.mess;
  let primaryTime = Math.min(time, scenario.delay);
  result = 0.5 * primaryAcceleration * primaryTime * primaryTime;
  let secondaryTime = time - scenario.delay;
  if(secondaryTime > 0) {    let primaryVelocity = primaryAcceleration * scenario.delay;
    let acc = (scenario.primaryForce + scenario.secondaryForce) / scenario.mess;
    ...  }
  return result;}

첫번째 용도를 대표하는 이름인 primaryAcceleration 이름으로 변경되었고, const를 사용해 값을 다시 대입할 수 없도록 했다

function distanceTravelled(scenario, time) {  let result;  const primaryAcceleration = scenario.primaryForce / scenario.mess;
  let primaryTime = Math.min(time, scenario.delay);
  result = 0.5 * primaryAcceleration * primaryTime * primaryTime;
  let secondaryTime = time - scenario.delay;
  if(secondaryTime > 0) {    let primaryVelocity = primaryAcceleration * scenario.delay;    const secondaryAcceleration = (scenario.primaryForce + scenario.secondaryForce) / scenario.mess;
    ...  }
  return result;}

이제 두번째 용도에 적합한 이름으로도 수정해주었다.

예시: 입력 매개변수의 값을 수정할 때#

변수 쪼개기의 또 다른 예로 입력 매개변수를 생각해 볼 수 있다(매개변수도 변수이기 때문)!

function discount(inputValue, quantity) {  if (inputValue > 50) {    inputValue = inputValue - 2;  }
  if (quantity > 100) {    inputValue = inputValue - 1;  }
  return inputValue;}

여기서 inputValue는 함수에 데이터를 전달하는 용도와 결과를 호출자에 반환하는 용도로 쓰였다

이 상황이라면 inputValue를 쪼개야 한다

function discount(originalInputValue, quantity) {  let inputValue = originalInputValue;
  if (inputValue > 50) {    inputValue = inputValue - 2;  }
  if (quantity > 100) {    inputValue = inputValue - 1;  }
  return inputValue;}

그 다음 변수 이름 바꾸기를 수행해서 각각의 쓰임에 어울리는 이름을 지어준다

function discount(inputValue, quantity) {  let result = inputValue;
  if (inputValue > 50) {    result = result - 2;  }
  if (quantity > 100) {    result = result - 1;  }
  return result;}

9-2 필드 이름 바꾸기#

배경#

이름은 중요하다. 레코드 구조체의 필드 이름들도 그렇다!

클래스의 게터와 세터 메서드는 클래스 사용자 입장에서는 필드와 다를 바 없기 때문에 게터와 세터 이름 바꾸기도 레코드 구조체 필드 이름 바꾸기와 똑같이 중요하다

절차#

  1. 레코드의 유효 범위가 제한적이라면 필드에 접근하는 모든 코드를 수정한 후 테스트한다. 이후 단계는 필요 없다
  2. 레코드가 캡슐화되지 않았다면 우선 레코드를 캡슐화한다
  3. 캡슐화한 객체 안의 private 필드명을 변경하고, 그에 맞게 내부 메서드들을 수정한다
  4. 테스트한다
  5. 생성자의 매개변수 중 필드와 이름이 겹치는 게 있다면 함수 선언 바꾸기로 변경한다
  6. 접근자들의 이름도 바꿔준다

예시#

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

여기서 name을 title로 바꾸고 싶다고 해보자. 이 객체는 코드베이스 곳곳에서 사용되며 그중 이 title을 변경하는 부분도 있다.

우선 organization 레코드를 클래스로 캡슐화 하자

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;  }}
const organization = new Organization({name: '애크미 구스베리', country: 'GB'});

이제 name에서 새로운 이름인 title을 사용하도록 바꿔준다

class Organization {  constructor(data) {    this._title = data.title;    this._country = data.country;  }
  get title() {    return this._title;  }  set title(aString) {    this._title = aString;  }  get country() {    return this._country;  }  set country(aCountryCode) {    this._country = aCountryCode;  }}
const organization = new Organization({title: '애크미 구스베리', country: 'GB'});

9-3 파생 변수를 질의 함수로 바꾸기#

배경#

가변 데이터는 소프트웨어에 문제를 일으키는 가장 큰 골칫거리이다. 그렇다고 가변 데이터를 완전히 배제하기란 현실적으로 불가능 할 때가 많기 때문에, 가변 데이터의 유효 범위를 가능한 좁히는 것이 좋다

가변 데이터의 유효 범위를 좁히는 데 효과가 좋은 방법으로, 값을 쉽게 유추할 수 있는 변수들을 모두 제거하는 것이 있다.

그러나 만약에 새로운 데이터 구조를 생성하는 변형 연산을 사용할 것이거나, 데이터 구조를 받아 다른 데이터 구조로 변환해서 반환하는 함수를 사용할 것이라면 그대로 두는 것도 괜찮다~

절차#

  1. 변수 값이 갱신되는 지점을 모두 찾는다. 필요하면 변수 쪼개기를 활용해 각 갱신 지점에서 변수를 분리한다.
  2. 해당 변수의 값을 계산해주는 함수를 만든다.
  3. 해당 변수가 사용되는 모든 곳에 어서션을 추가하여 함수의 계산 결과가 변수의 값과 같은지 확인한다
  4. 테스트한다
  5. 변수를 읽는 코드를 모두 함수 호출로 대체한다.
  6. 테스트한다
  7. 변수를 선언하고 갱신하는 코드를 죽은 코드제거하기로 없앤다

예시#

class ProductionPlan {  get production() {    return this._production;  }
  applyAdjustment(anAdjustment) {    this._adjustments.push(anAdjustment);    this._production += anAdjustment.amount;  }}

조정값 adjustment를 적용하는 과정에서 직접 관련이 없는 값 production까지 갱신했다.

그런데 이 누적값은 매번 갱신하지 않고도 계산할 수 있다

하지만 그것은 착각일 수도 있으니 assertion을 추가하여 안전한지 확인한다

class ProductionPlan {  get production() {    assert(this._production === this.calculateProduction);    return this._production;  }  get calculateProduction() {    return this._adjustments.reduce((sum, a) => sum + a.amount, 0);  }}

어서션을 무사히 통과한다면 필드를 반환하던 코드를 계산 결과를 직접 반환하도록 수정하고, calculateProduction 메서드를 인라인한다.

class ProductionPlan {  get production() {    return this._adjustments.reduce((sum, a) => sum + a.amount, 0);  }
  applyAdjustment(anAdjustment) {    this._adjustments.push(anAdjustment);    this._production += anAdjustment.amount;  }}

마지막으로 옛 변수를 참조하는 모든 코드를 죽은 코드 제거하기로 정리한다

class ProductionPlan {  get production() {    return this._adjustments.reduce((sum, a) => sum + a.amount, 0);  }
  applyAdjustment(anAdjustment) {    this._adjustments.push(anAdjustment);  }}

예시: 소스가 둘 이상일 때#

class ProductPlan {  constructor(production) {    this._production = production;    this._adjustments = [];  }
  get production() {    return this._production;  }
  applyAdjustment(anAdjustment) {    this._adjustments.push(adAdjustment);    this._production += anAdjustment.amount;  }}

어서션 코드를 앞의 예와 똑같이 작성한다면 _production의 초깃값이 0이 아니면 실패한다

이 파생 데이터를 대체할 방법은 아직 있다. 앞에서의 차이라면 변수 쪼개기를 먼저 적용하는 것뿐이다

class ProductPlan {  constructor(production) {    this._initialProduction = production;    this._productionAccumulator = 0;    this._adjustments = [];  }
  get production() {    return this._initialProduction + this._productionAccumulator;  }}

이제 어서션을 추가한다

class ProductPlan {  constructor(production) {    this._initialProduction = production;    this._productionAccumulator = 0;    this._adjustments = [];  }
  get production() {    assert(this._productionAccumulator === this.calculateProductionAccumulator);
    return this._initialProduction + this._productionAccumulator;  }
  get calculateProductionAccumulator() {    return this._adjustments.reduce((sum, a) => sum + a.amount, 0);  }}

그 다음은 이전과 같다. 그런데 이번에는 calculateProductionAccumulator를 인라인하지 않는게 더 좋아보이네요~

9-4 참조를 값으로 바꾸기#

배경#

객체를 다른 객체에 중첩하면 내부 객체를 참조 혹은 값으로 취급할 수 있다. 참조냐 값이냐의 차이는 내부 객체의 속성을 갱신하는 방식에서 가장 극명하게 드러난다.

  • 참조로 다루는 경우

내부 객체는 그대로 둔 채 그 객체의 속성만 갱신

  • 값으로 다루는 경우

새로운 속성을 담은 객체로 기존 내부 객체를 통째로 대체

필드를 값으로 다룬다면 내부 객체의 클래스를 수정하여 값 객체로 만들수 있다. 값 객체는 불변이기 때문에 자유롭게 활용하기 좋다.

그러나 특정 객체를 여러 객체에서 공유해서 공유 객체의 값을 변경했을 때 관련 객체 모두에게 알려줘야 한다면 값이 아니라 참조로 다뤄야 한다

절차#

  1. 후보 클래스가 불변인지 혹은 불변이 될수있는지 확인한다
  2. 각각의 세터를 하나씩 제거한다
  3. 이 값 객체의 필드들을 사용하는 동시성 비교 메서드를 만든다
    1. 대부분의 언어는 이런 상황에 사용할 수 있도록 오버라이딩 가능한 동치성 비교 메서드를 제공한다. 동치성 비교 메서드를 오버라이드 할 때는 보통 해시코드 생성 메서드도 함께 오버라이드해야한다.

예시#

사람 객체가 있고, 이 객체는 다음 코드처럼 생성 시점에는 전화번호가 올바로 설정되지 못하게 짜여 있다고 해보자.

class Person {  constructor() {    this._telephoneNumber = new TelephoneNumber();  }
  get officeAreaCode() {    return this._telephoneNumver.areaCode;  }  set officeAreaCode(arg) {    this._telephoneNumber.areaCode = arg;  }  get officeNumber() {    return this._telephoneNumber.number;  }  set officeNumber(arg) {    this._telephoneNumber.number = arg;  }}
class TelephoneNumber {  get areaCode() {    return this._areaCode;  }  set areaCode(arg) {    this._areaCode = arg;  }  get number() {    return this._number;  }  set number(arg) {    this._number = arg;  }}

가장먼저 전화번호를 불변으로 만들기다. 필드들의 세터들만 제거하면 된다. 생성자에서 입력받아 설정하도록 하자

class Person {  constructor() {    this._telephoneNumber = new TelephoneNumber();  }
  get officeAreaCode() {    return this._telephoneNumver.areaCode;  }  set officeAreaCode(arg) {    this._telephoneNumber = new TelephoneNumber(arg, this.officeNumber);  }  get officeNumber() {    return this._telephoneNumber.number;  }  set officeNumber(arg) {    this._telephoneNumber = new TelephoneNumber(this.officeAreaCode, arg);  }}
class TelephoneNumber {  constructor(areaCode, number) {    this._areaCode = areaCode;    this._number = number;  }
  get areaCode() {    return this._areaCode;  }  get number() {    return this._number;  }}

이제 전화번호는 불변이 되었으니 값이 될 준비가 끝났다. 값 객체로 인정받으려면 동치성을 값 기반으로 평가해야 한다. 그러나 자바스크립트는 그게 없음... equals 메서드를 직접 작성해보자

class TelephoneNumber {  constructor(areaCode, number) {    this._areaCode = areaCode;    this._number = number;  }
  get areaCode() {    return this._areaCode;  }  get number() {    return this._number;  }
  equals(other) {    if (!other instanceof TelephoneNumber) {      return false;    }
    return this.areaCode === other.areaCode && this.number === other.number;  }}

9-5 값을 참조로 바꾸기#

배경#

이전것과 반대.

같은 고객이 여러 주문을 했을 때, 고객을 값으로도 참조로도 다룰 수 있는데

값으로 다루는 경우 각 주문마다 고객 데이터가 복사되어 있을것이고 참조로 다룬다면 단 하나의 고객 데이터를 여러 주문이 바라보게 된다

고객 데이터를 갱신할 일이 없다면 어떤 방식이든 상관없다. 그러나 문제가 되는 상황은 데이터를 갱신해야 할 때...

이런 상황이라면 복제된 데이터들을 참조로 바꿔주는 것이 좋다. 데이터가 하나면 갱신된 내용이 해당 고객의 모든 주문에 반영될것이기 때문

값을 참조로 바꾸면 엔티티 하나당 객체도 단 하나만 존재하게 되는데, 그러면 보통 이런 객체들을 한데 모아놓고 클라이언트들의 접근을 관리해주는 일종의 저장소가 필요해진다.

각 엔티티를 표현하는 객체를 한 번만 만들고, 객체가 필요한 곳에서는 모두 이 저장소로부터 얻어 쓰는 방식이 된다.

절차#

  1. 같은 부류에 속하는 객체들을 보관할 저장소를 만든다
  2. 생성자에서 이 부류의 객체들 중 특정 객체를 정확히 찾아내는 방법이 있는지 확인한다
  3. 호스트 객체의 생성자들을 수정하여 필요한 객체를 이 저장소에서 찾도록 한다.

예시#

class Order {  constructor(data) {    this._number = data.number;    this._customer = new Customer(data.customer);  }
  get customer() {    return this._customer;  }}
class Customer {  constructor(id) {    this._id = id;  }
  get id() {    return this._id;  }}

이런 식으로 생성한 Customer 객체는 값이다. 고객 ID가 123인 주문을 다섯 개 생성한다면 독립된 고객 객체가 5개 만들어진다.

이 중 하나를 수정하더라도 나머지 4개에는 반영되지 않는다.

항상 같은 고객 객체를 사용하고 싶다면 먼저 이 유일한 객체를 저장해둘 곳이 있어야 한다. 간단한 상황이라면 나는 저장소 객체를 사용하는 편이다

let _repositoryData;
export function initialize() {  _repositoryData = {};  _repositoryData.customers = new Map();}
export function registerCustomer(id) {  if (!_repositoryData.customers.has(id)) {    _repositoryData.customers.set(id, new Customer(id));  }
  return findCustomer(id);}
export function findCustomer(id) {  return _repositoryData.customers.get(id);}

이 저장소는 Map을 이용해서 고객의 ID당 하나의 Customer 객체만을 저장한다

이제 수정해봅시다

class Order {  constructor(data) {    this._number = data.number;    this._customer = registerCustomer(data.customer);  }
  get customer() {    return this._customer;  }}

이제 특정 주문과 관련된 고객 정보를 갱신하면 같은 고객을 공유하는 주문 모두에서 갱신된 데이터를 사용하게 된다.

9-6 매직 리터럴 바꾸기#

배경#

매직 리터럴이란 소스 코드에 등장하는 일반적인 리터럴 값을 말한다.

9.80665라는 숫자가 코드에 있을때, 이 숫자가 중력을 뜻한다는 것을 숫자 자체로는 명확히 알 수 없으므로 매직 리터럴이라 할 수 있다.

이런건 상수로 정의하고 숫자대신 상수를 사용하도록 바꾸면 된다

일반적으로는 이 방법이 가장 좋다. 다른 선택지로는 그 상수가 특별히 비교 로직에 주로 쓰이는 경우인데

그때는 상수를 사용하기보다 함수 호출로 바꾸는 쪽을 선호한다고 하시네요

aValue === 'M';aValue === MALE_GENDER;보다는;isMale(aValue);

방법#

  1. 상수를 선언하고 매직 리터럴을 대입한다
  2. 해당 리터럴이 사용되는 곳을 모두 찾는다
  3. 찾은 곳 각각에서 리터럴이 새 상수와 똑같은 의미로 쓰였는지 확인하여, 같은 의미라면 상수로 대체한다