9장 데이터 조직화
#
9-1 변수 쪼개기#
배경변수에 값을 여러번 대입할 수 밖에 없는 경우가 아닌 이상 변수에는 값을 단 한번만 대입해야 한다.
대입이 두 번 이상 이뤄진다면 그 변수가 여러 가지 역할을 수행한다는 신호다. 역할이 둘 이상인 변수가 있다면 쪼개야한다!
#
절차- 변수를 선언한 곳과 값을 처음 대입하는 곳에서 변수 이름을 바꾼다
- 이후의 대입이 항상 i = i + 어쩌구 형태라면 수집 변수이므로 쪼개면 안 된다. 수집 변수는 총합 계산, 문자열 연결, 스트림에 쓰기, 컬렉션에 추가하기 등의 용도로 흔히 쓰인다
- 가능하면 이때 불변으로 선언한다
- 이 변수에 두 번째로 값을 대입하는 곳 앞까지의 모든 참조(이 변수가 쓰인 곳)을 새로운 변수 이름으로 바꾼다
- 두 번째 대입 시 변수를 원래 이름으로 다시 선언한다.
- 테스트한다
- 반복한다. 매 반복에서 변수를 새로운 이름으로 선언하고 다음번 대입 때까지의 모든 참조를 새 변수명으로 바꾼다. 이 과정을 마지막 대입까지 반복한다.
#
예시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 필드 이름 바꾸기#
배경이름은 중요하다. 레코드 구조체의 필드 이름들도 그렇다!
클래스의 게터와 세터 메서드는 클래스 사용자 입장에서는 필드와 다를 바 없기 때문에 게터와 세터 이름 바꾸기도 레코드 구조체 필드 이름 바꾸기와 똑같이 중요하다
#
절차- 레코드의 유효 범위가 제한적이라면 필드에 접근하는 모든 코드를 수정한 후 테스트한다. 이후 단계는 필요 없다
- 레코드가 캡슐화되지 않았다면 우선 레코드를 캡슐화한다
- 캡슐화한 객체 안의 private 필드명을 변경하고, 그에 맞게 내부 메서드들을 수정한다
- 테스트한다
- 생성자의 매개변수 중 필드와 이름이 겹치는 게 있다면 함수 선언 바꾸기로 변경한다
- 접근자들의 이름도 바꿔준다
#
예시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 파생 변수를 질의 함수로 바꾸기#
배경가변 데이터는 소프트웨어에 문제를 일으키는 가장 큰 골칫거리이다. 그렇다고 가변 데이터를 완전히 배제하기란 현실적으로 불가능 할 때가 많기 때문에, 가변 데이터의 유효 범위를 가능한 좁히는 것이 좋다
가변 데이터의 유효 범위를 좁히는 데 효과가 좋은 방법으로, 값을 쉽게 유추할 수 있는 변수들을 모두 제거하는 것이 있다.
그러나 만약에 새로운 데이터 구조를 생성하는 변형 연산을 사용할 것이거나, 데이터 구조를 받아 다른 데이터 구조로 변환해서 반환하는 함수를 사용할 것이라면 그대로 두는 것도 괜찮다~
#
절차- 변수 값이 갱신되는 지점을 모두 찾는다. 필요하면 변수 쪼개기를 활용해 각 갱신 지점에서 변수를 분리한다.
- 해당 변수의 값을 계산해주는 함수를 만든다.
- 해당 변수가 사용되는 모든 곳에 어서션을 추가하여 함수의 계산 결과가 변수의 값과 같은지 확인한다
- 테스트한다
- 변수를 읽는 코드를 모두 함수 호출로 대체한다.
- 테스트한다
- 변수를 선언하고 갱신하는 코드를 죽은 코드제거하기로 없앤다
#
예시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 참조를 값으로 바꾸기#
배경객체를 다른 객체에 중첩하면 내부 객체를 참조 혹은 값으로 취급할 수 있다. 참조냐 값이냐의 차이는 내부 객체의 속성을 갱신하는 방식에서 가장 극명하게 드러난다.
- 참조로 다루는 경우
내부 객체는 그대로 둔 채 그 객체의 속성만 갱신
- 값으로 다루는 경우
새로운 속성을 담은 객체로 기존 내부 객체를 통째로 대체
필드를 값으로 다룬다면 내부 객체의 클래스를 수정하여 값 객체로 만들수 있다. 값 객체는 불변이기 때문에 자유롭게 활용하기 좋다.
그러나 특정 객체를 여러 객체에서 공유해서 공유 객체의 값을 변경했을 때 관련 객체 모두에게 알려줘야 한다면 값이 아니라 참조로 다뤄야 한다
#
절차- 후보 클래스가 불변인지 혹은 불변이 될수있는지 확인한다
- 각각의 세터를 하나씩 제거한다
- 이 값 객체의 필드들을 사용하는 동시성 비교 메서드를 만든다
- 대부분의 언어는 이런 상황에 사용할 수 있도록 오버라이딩 가능한 동치성 비교 메서드를 제공한다. 동치성 비교 메서드를 오버라이드 할 때는 보통 해시코드 생성 메서드도 함께 오버라이드해야한다.
#
예시사람 객체가 있고, 이 객체는 다음 코드처럼 생성 시점에는 전화번호가 올바로 설정되지 못하게 짜여 있다고 해보자.
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 값을 참조로 바꾸기#
배경이전것과 반대.
같은 고객이 여러 주문을 했을 때, 고객을 값으로도 참조로도 다룰 수 있는데
값으로 다루는 경우 각 주문마다 고객 데이터가 복사되어 있을것이고 참조로 다룬다면 단 하나의 고객 데이터를 여러 주문이 바라보게 된다
고객 데이터를 갱신할 일이 없다면 어떤 방식이든 상관없다. 그러나 문제가 되는 상황은 데이터를 갱신해야 할 때...
이런 상황이라면 복제된 데이터들을 참조로 바꿔주는 것이 좋다. 데이터가 하나면 갱신된 내용이 해당 고객의 모든 주문에 반영될것이기 때문
값을 참조로 바꾸면 엔티티 하나당 객체도 단 하나만 존재하게 되는데, 그러면 보통 이런 객체들을 한데 모아놓고 클라이언트들의 접근을 관리해주는 일종의 저장소가 필요해진다.
각 엔티티를 표현하는 객체를 한 번만 만들고, 객체가 필요한 곳에서는 모두 이 저장소로부터 얻어 쓰는 방식이 된다.
#
절차- 같은 부류에 속하는 객체들을 보관할 저장소를 만든다
- 생성자에서 이 부류의 객체들 중 특정 객체를 정확히 찾아내는 방법이 있는지 확인한다
- 호스트 객체의 생성자들을 수정하여 필요한 객체를 이 저장소에서 찾도록 한다.
#
예시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);
#
방법- 상수를 선언하고 매직 리터럴을 대입한다
- 해당 리터럴이 사용되는 곳을 모두 찾는다
- 찾은 곳 각각에서 리터럴이 새 상수와 똑같은 의미로 쓰였는지 확인하여, 같은 의미라면 상수로 대체한다