리팩터링 2판의 Chatper 07를 보고 정리한 글입니다.
모듈을 분리하는 가장 중요한 기준은 시스템에서 각 모듈이 자신을 제외한 다른 부분에 드러나지 않아야 할 비밀을 얼마나 잘 숨기느냐에 있을 것이다.
이 장에서는 위와 같은 캡슐화와 관련된 리팩터링을 다룬다.
레코드는 연관된 여러 데이터를 직관적인 방식으로 묶을 수 있어서 각각을 따로 취급할 때보다 훨씬 의미 있는 단위로 전달할 수 있게 해준다. 그러나 계산해서 얻을 수 있는 값과 그렇지 않은 값을 명확히 구분해 저장해야 하는 점이 번거롭다.
이런 경우 가변 데이터를 저장하는 용도로 저자는 레코드보다 객체(클래스로 만든)를 선호한다고 한다.
간단한 레코드 캡슐화하기
리팩터링 전의 코드
const organization = { name: '애크미 구스베리', country: 'GB' };
result += `<h1>${organization.name}</h1>`; // 읽기 예
organization.name = newName; // 쓰기 예
리팩터링 이후 코드
function getRawDataOfOrganization() { return organization;}
result += `<h1>${getRawDataOfOrganization().name}</h1>`; // 읽기 예
getRawDataOfOrganization().name = newName; // 쓰기 예
이렇게 캡슐화하는 목적은 변수 자체는 물론 그 내용을 조작하는 방식도 통제하기 위해서다.
class Organization {
constructor(data) {
this._data = data;
}
}
const organization = new Organization({ name: '애크미 구스베리', country: 'GB' });
function getRawDataOfOrganization() {
return organization._data;
}
function getOrganization() {
return 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;}
}
// 클라이언트 코드
...
result += `<h1>${getRawDataOfOrganization().name}</h1>`;
다음과 같이 data안의 값들을 펼쳐놓으면 입력 데이터 레코드와 연결을 끊어준다는 이점이 있다.
책에 추가적인 예시로 중첩된 레코드 캡슐화하는 예시가 나오는데 리팩터링 하는 절차는 거의 비슷하며, 중첩된 구조를 따로 독립 함수로 추출하여 데이터 클래스 안으로 옮기는 방법으로 리팩터링이 진행된다.
이런 경우 데이터를 읽는 과정에서 클라이언트가 데이터 구조를 요청할 때 실제 데이터를 제공하는 경우 1. 복제를 하거나, 2. 레코드 캡슐화를 재귀적인 방법으로 데이터 구조를 제공할 수 있다.
가변 데이터를 모두 캡슐화하면 데이터 구조가 언제 어떻게 수정되는지 파악하기 쉬워진다. 컬렉션과 같은 가변 데이터를 다룰 때, 게터가 컬렉션 자체를 반환하도록 하면 그 컬렉션의 원소들이 바뀌어 버릴 수 있다.
이런 문제를 방지하기 위해 항상 컬렉션을 소유한 클래스를 통해서만(클래스안의 메서드) 원소를 변경하도록 하면 프로그램을 개선하면서 컬렉션 변경 방식도 원하는 대로 수정할 수 있다.
컬렉션 게터가 원본 컬렉션을 반환하지 않게 만들어서 클라이언트가 실수로 컬렉션을 바꿀 가능성을 차단하는게 낫다.
리팩터링 전의 코드
class Person {
constructor(name) {
this.name = name;
this._course = [];
}
get name() {
return this._name;
}
get courses() {
return this._course;
}
set courses(aList) {
this._course = aList;
}
}
class Course {
constructor(name, isAdvanced) {
this.name = name;
this._isAdvanced = isAdvanced;
}
get name() {
return this._name;
}
get isAdvanced() {
return this._isAdvanced;
}
}
현재 이 코드는 세터를 이용해 수업 컬렉션을 통째로 설정한 클라이언트는 누구든 이 컬렉션을 마음대로 수정할 수 있다.
리팩터링 이후 코드
class Person {
...
addCourse(aCourse) {
this._Courses.push(aCourse);
}
removeCourse(
aCourse,
fnIfAbsent = () => {
throw new RangeError();
},
) {
const index = this._courses.indefOf(aCourse);
if (index === -1) fnIfAbsent();
else this.courses.splice(index, 1);
}
// 클라이언트측 코드 변경
get courses() {return this._courses.slice();}
저자는 컬렉션을 관리할 때 복제본을 만들어서 제공하는 것을 추천함.
단순한 출력 이상의 기능이 필요해지는 순간 그 데이터를 표현하는 전용 클래스를 정의하자.
나중에 특별한 동작이 필요해지면 이 클래스에 추가하면 되니 프로그램이 커질수록 유용한 도구가 된다.
리팩터링 전의 코드
class Order {
this.priority = data.priority;
// 나머지 초기화 코드
}
// 클라이언트
highPriorityCount = orders.filter(o => 'high' === o.priority || "rush" === o.priority).length;
리팩터링 이후의 코드
class Order {
this._priority = data.priority;
get priority() { return this._priority;}
set priority(aString) { this._priority = aString;}
}
class Priority {
constructor(value) {this._value = value;}
toString() {return this._value;}
}
이 상황에서 게터보다 변환 함수를 사용한것은 클라이언트 입장에서 속성 자체를 받은 게 아니라 해당 속성을 문자열로 표현한 값을 요청하게 되기 때문이다.
→ 이 부분을 보면서 지난번 배운 '목적과 구현의 분리' 라는 개념이 떠오르기도 하였음
4.5. 그런 다음 방금 만든 Priority 클래스를 사용하도록 접근자들을 수정한다.
class Order {
this._priority = data.priority;
get priority() { return this._priority.toString();}
set priority(aString) { this._priority = new Priority(aString);}
}
get priorityString() { return this._priority.toString();}
공식적인 리팩터링은 이후 나오는 '더 가다듬기'라는 부분 이해가 잘 안가서 스터디때 논의 해보면 좋을듯
→ 저런식으로 작성했을 때 왜 더 의미있는 코드가 되는지?
변수 대신 함수로 만들어 두면 비슷한 계산을 수행하는 다른 함수에서도 사용할 수 있어 코드 중복이 줄어든다.
또한 함수간에도 긴 함수의 한 부분을 별도의 함수로 추출하면 그 경계가 더 분명해지기도 하고, 이런 경우 부자연스러운 의존 관계나 부수효과를 갖고 제거하는 데 도움이 된다.
이번 리팩터링은 클래스 안에서 적용할 때 효과가 가장 크다. 클래스는 추출할 메서드들에 공유 컨텍스트를 제공하기 때문이다.
→ 스냅숏 용도로 쓰이는 변수에는 이 리팩터링을 적용하면 안된다.
예시코드는 클래스 내부에서 사용되는 임시 변수를 게터로 빼내는 과정을 설명하고 있어서 간단하여 따로 작성 X
반대되는 리팩터링: 클래스 인라인하기
메서드와 데이터가 너무 많은 클래스는 이해하기가 쉽지 않으니 잘 살펴보고 적절히 분리하는 것이 좋다.
클래스는 반드시 명확하게 추상화하고 소수의 주어진 역할만 처리해야 한다는 가이드라인을 따르자.
리팩터링 전의 코드
class Person {
get name() {
return this._name;
}
set name(arg) {
this._name = arg;
}
get telephoneNumber() {
return `(${this.officeAreaCode}) ${this.officeNumber}`;
}
get officeAreaCode() {
return this._officeAreaCode;
}
set officeAreaCode(arg) {
this._officeAreaCode = arg;
}
get officeNumber() {
return this._officeNumber;
}
set officeNumber(arg) {
this._officeNumber = arg;
}
}
리팩터링 이후 코드
class TelephoneNumber {
}
class Person {
constructor() {
this._telephoneNumber = new TelephoneNumber();
}
}
class TelephoneNumber {
get officeAreaCode() { return this._officeAreaCode;}
set officeAreaCode(arg) {this._officeAreaCode = arg;}
}
// Person 클래스
get officeAreaCode() {
return this._telephoneNumber.officeAreaCode;
}
set officeAreaCode(arg) {
this._telephoneNumber.officeAreaCode = arg;
}
// TelephoneNumber 클래스
get officeNumber() {
return this._officeNumber;
}
set officeNumber(arg) {
this._telephoneNumber.officeAreaCode = arg;
}
// TelephoneNumber 클래스와 Person 클래스의 메서드 명을 바꿔줌
반대 리팩터링: 클래스 추출하기
두 가지의 경우 이 클래스 인라인하기를 사용한다.
리팩터링 전의 코드
배송 추적 정보를 표현하는 TrackingInformation 클래스
class TrackingInformation {
get shippingCompany() {return this._shippingCompany;}
set shippingCompany(arg) {this._shippingCompany = arg;}
get trackingNumber() {return this._trackingNumber;}
set trackingNumber(arg) {this._trackingNumber = arg;}
get display() {
return `${this.shippingCompany}: ${this.trackingNumber}`
}
}
// 이 클래스는 배송 클래스의 일부처럼 사용된다.
// Shipment 클래스
get trackingInfo() {
return this.trackingInformation.display;
}
get trackingInformation() {return this._trackingInformation;}
set trackingInformation(aTrackingInformation) {
this._trackingInformation = aTrackingInformation
}
// 클라이언트에선 현재 이렇게 호출
aShipment.trackingInformation.shippingCompany = request.vendor;
리팩터링 이후 코드
// Shipment 클래스
set shippingCompany(arg) {this._trackingInformation.shippingCompany = arg;}
// 클라이언트
aShipment.shippingCompany = request.vendor;
// Shipment 클래스
get trackingInfo() {
return `${this.shippingCompany}: ${this.trackingNumber}`
}
// 배송 회사 필드 차례
// Shipment 클래스
get shippingCompany() {return this._shippingCompany;}
set shippingCompany(arg) {this._shippingCompany = arg;}
다 옮겼다면 TrackingInformation 클래스를 삭제한다.
반대 리팩터링: 중개자 제거하기
모듈화 설계를 제대로 하는 핵심은 캡슐화이지만, 캡슐화란 단순히 필드를 숨기는 것뿐만 아니라, 위임 객체와 같은 역할도 있다.
의존성을 없애기 위해 위임 객체를 숨겨 클라이언트에 아무런 영향을 주지 않도록 하자.
리팩터링 전의 코드
사람과 사람이 속한 부서를 다음처럼 정의
// Person 클래스
constructor(name) {
this._name = name;
}
get name() {return this._name;}
get department() {return this._department;}
set department(arg) {this._department = arg;}
// Department 클래스
get chargeCode() {return this._chargeCode}
set chargeCode(arg) {this._chargeCode = arg;}
get manager() {return this._manager}
get chargeCode(arg) {this._chargeCode = arg;}
위와 같은 코드에서 어떤 사람이 속한 부서의 관리자를 알고 싶은 경우 부서 객체부터 얻어와야 한다.
// 클라이언트
manager = aPerson.department.manager;
부서 클래스가 관리자 정보를 제공한다는 사실을 클라이언트가 알아야한다.
→ 부서 클래스의 작동방식을 알아야한다.
리팩터링 이후 코드
// Person 클래스
get manager() {return this._department.manager;}
manager = aPerson.manager;
반대 리팩터링: 위임 숨기기
위임 숨기기 배경에서는 위임 객체를 캡슐화하는 이점을 설명했다. 그러나 이러한 위임만 하게되면, 단순히 전달만 하는 위임 메서드들이 점점 성가셔지고 클라이언트가 위임 객체를 직접 호출하는게 나을 수 있다.
어느 정도까지 숨겨야 적절한지 판단하기 어렵고 그때마다 위임 숨기기, 중개자 제거하기 리팩터링을 하자.
위임 숨기기 예시를 거꾸로 하는 것과 유사하게 진행된다. (책 참조)
문제를 더 확실히 이해하고 훨씬 쉽게 해결하는 방법을 발견했을 때 알고리즘 전체를 걷어내고 훨씬 간결한 알고리즘으로 바꿔보자.
가능한 한 메서드를 잘게 나눴는지 확인하면서 거대하고 복잡한 알고리즘을 차근차근 간소화하는 작업부터 진행하여 교체하자.
책에 따로 나온 예시는 없음