Skip to main content

7장 캡슐화

written by
Danbi
Danbi 🏆Android Engineer

캡슐화*#

관련된 속성과 행위들을 하나로 묶는 것, 그의 목적은 외부로부터 구체적인 정보를 노출시키지 않도록 하기 위함
모듈을 분리하는 가장 중요한 기준: 시스템에서 각 모듈이 자신을 제외한 다른 부분에 드러내지 않아야 할 비밀을 얼마나 잘 숨기는지

그렇다면 7장 캡슐화 리팩터링에서 말하는 캡슐화의 방법? 데이터에 대한 접근을 함수로만 한정하는 것

7-1 레코드 캡슐화하기#

배경#

레코드의 캡슐화 = 레코드를 클래스로 만드는 것

레코드는 연관된 여러 데이터를 직관적으로 묶을 수 있다. 또한 대부분의 프로그래밍 언어는 데이터 레코드를 표현하는 구조를 제공한다.
하지만 계산해서 얻을 수 있는 값 이나 변할 수 있는 값 과 같이 가변 데이터를 저장하는 용도로는 레코드보다 객체를 저자는 더 선호하고 있다.

예시#

예시 1. 간단한 레코드 캡슐화하기#

상수를 예시로 들어보자

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; //쓰기 예

그런데 방금 변수 캡슐화하기(6.6)를 정식으로 따르지 않고, 게터를 찾기 쉽도록 의도적으로 이 상한 이름을 붙였다. 이 게터는 임시로만 사용할 것이기 때문이다.
레코드를 캡슐화하는 목적은 변수 자체는 물론 그 내용을 조작하는 방식도 통제하기 위해서이다.
❷. 이렇게 하려면 레코드를 감싼 단순한 클래스로 해당 변수의 내용을 교체한다. 이 클래스에 원본 레코드를 반환하는 접근자도 정의하고, 변수를 캡슐화하는 함수들이 이 접근자를 사용하도록 수정한다.
❹. 원본 레코드 대신 새로 정의한 클래스 타입의 객체를 반환하는 함수들을 새로 만든다.

//Organization 클래스...class Organization {    constructor(data) {        this._data = data;    }}
//최상위...const organization = new Organization({ name: "애크미 구스베리", country: "GB" });function getRawDataOfOrganization() {    return organization._data;}function getOrganization() {    return organization;}

객체로 만드는 작업은 끝이 났고, ❺. 레코드를 사용하던 코드를 살펴보고 레코드를 갱신하던 코드는 모두 세터를 사용하도록 고친다.

//Organization 클래스...set name(aString) { this._data.name = aString; }
//클라이언트...getOrganization().name = newName;

마찬가지로, 레코드를 읽는 코드는 모두 게터를 사용하게 바꾼다.

Organization 클래스...get name() { return this._data.name; }
클라이언트...result +=  `<h1>${organization.name}</h1>`;

❻. 다 바꿨다면 (앞에서 이상한 이름으로 지었던) 임시 함수를 제거한다.

function getOrganization() { return organization._data; }

function getOrganization() {    return organization;}

마지막으로 _data의 필드들을 객체 안에 바로 펼쳐놓으면 더 깔끔해질 것이다.

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;    }}

이렇게 하면 입력 데이터 레코드와의 연결을 끊어준다는 이점이 생긴다.

예시 2. 중첩된 레코드 캡슐화하기#

앞에서는 단순한 레코드를 캡슐화하는 방법을 보았다. 하지만 JSON 문서처럼 여려 겹 중첩된 레코드라면 어떻게 해야 할까?
기본 절차는 같고, 갱신하는 코드에 주의해야한다는 점도 같지만, 읽는 코드를 다룰 때는 선택지가 몇 가지 더 생긴다. 다음은 중첩된 레코드이다. 이 데이터는 고객 정보를 저장한 해시맵으로, 고객 ID를 키로 사용한다.

"1920" : {  name: "마틴 파울러",  id: "1920",  usages: {    "2016": {      "1": 50,      "2": 55,      //나머지 달 생략    },    "2015": {      //나머지 달 생략    }  },  "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 };}

이번 캡슐화도 앞에서와 마찬가지로 변수 캡슐화부터 시작한다.

function getRawDataOfCustomers { return customerData; }function setRawDataOfCustomers { customerData = arg; }
//쓰기 예...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 };}

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

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

여기서 가장 중요한 부분은 데이터를 쓰는 코드다. 따라서 getRawDataOfCustomers()를 호출한 후 데이터를 변경하는 경우에도 주의해야 한다. 값을 쓰는 예를 다시 한번 상기해보면

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

기본 절차에 따르면 고객 객체를 반환하고 필요한 접근자를 만들어서 사용하게 하면 된다. 현재 고객 객체에는 이 값을 쓰는 세터가 없어서 데이터 구조 안으로 깊이 들어가 값을 바꾸고 있다. 따라서 데이터 구조 안으로 들어가는 코드를 세터로 뽑아내는 작업부터 한다. (함수 추출하기)

//쓰기 예...setUsage(customerID, year, month, amount);
//최상위...function setUsage(customerID, year, month, amount) {    getRawDataOfCustomers()[customerID].usages[year][month] = amount;}

그런 다음 이 함수를 고객 데이터 클래스로 옮긴다(함수 옮기기)

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

저자는 큰 데이터 구조를 다룰수록 쓰기 부분에 집중한다고 한다. 캡슐화에서는 값을 수정하는 부분을 명확하게 드러내고 한 곳에 모아두는 일이 굉장히 중요하다. 이렇게 쓰는 부분을 찾앙 수정하다 보면 빠진 건 없는지 확인하는 방법은 여러가지다. 우선 getRawDataOfCustomers()에서 데이터를 깊은 복사하여 반환하는 방법이 있다.

//최상위...function getCustomerData() {    return customerData;}function getRawDataOfCustomers() {    return customerData.rawData;}function setRawDataOfCustomers(arg) {    customerData = new CustomerData(arg);}
//CustomerData 클래스...get rawData() {  return _.cloneDeep(this._data);}

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

//CustomerData 클래스...usage(customeID, 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 };}

rawData() 메서드를 사용하여 내부 데이터를 복제해서 제공한다.

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

생길 수 있는 문제점은 데이터 구조가 클수록 복제 비용이 커져서 성능이 느려질 수 있다. 상황에 따라 성능 비용을 감당할 수 있을 수도 있으니 상황에 맞게 쓰면 된다.
또 다른 문제점은 클라이언트가 원본을 수정한다고 착각할 수 있다는 것이다. 이럴 때는 읽기전용 프락시를 제공하거나 복제본을 동결시켜 데이터를 수정하려 할 때 에러를 던지도록 만들 수도 있다.

7-2 컬렉션 캡슐화하기#

배경#

레코드와 컬렉션의 차이: getter, setter를 설정해도 예상치 못한 변경이 일어날 수 있다

가변 데이터를 모두 캡슐화하면 데이터 구조가 언제 어떻게 수정되는지 파악하기 쉬워서 필요한 시점에 데이터 구조를 변경하기도 쉬워진다. 하지만 컬렉션을 다룰 때는 캡슐화 과정에서 실수가 나오기도 한다. 예를 들어 컬렉션 변수로의 접근을 캡슐화하면서 게터가 컬렉션 자체를 반환하도록 한다면, 그 컬렉션을 감싼 클래스가 눈치채지 못한 상태에서 컬렉션의 원소들이 바뀌어버릴 수 있다.

예시#

수업(course) 목록을 필드로 지니고 있는 Person 클래스를 예시로 들어보자.

//Person 클래스..constructor(name) {    this._name = name;    this._course = [];}get name() {return this._name;}get courses() {return this._courses}set courses(aList) {this._courses = aList;}}
//Course 클래스...constructor(name, isAdvanced) {this._name = name;this._isAdvanced = isAdvanced;}get name() {return this._name;}get isAdvanced() {return this._isAdvanced;}

클라이언트는 Person이 제공하는 수업 컬렉션에서 수업 정보를 얻는다.

numAdvancedCourses = aPerson.courses.filter((c) => c.isAdvanced).length;

모든 필드가 접근자 메서드로 보호받고 있으니 이렇게만 해도 데이터를 캡슐화했다고 생각하기 쉽다. 하지만 세터를 이용해 수업 컬렉션을 통째로 설정한 클라이언트는 누구든 이 컬렉션을 마음대로 수정할 수 있다.

//클라이언트...const basicCourseNames = readBasicCourseNames(filename);aPerson.courses = basicCourseNames.map((name) => new Course(name, false));

클라이언트 입장에서는 다음처럼 수업 목록을 직접 수정하는 것이 훨씬 편할 수 있다.

//클라이언트...for (const name of readBasicCourseNames(filename)) {    aPerson.courses.push(new Course(name, false));}

하지만 이런 식으로 목록을 갱신하면 Person 클래스가 더는 컬렉션을 제어할 수 없으니 캡슐화가 깨진다. 필드를 참조하는 과정만 캡슐화했을 뿐 필드에 담긴 내용은 캡슐화하지 않은 게 원인이다.

❷. 컬렉션에 원소를 추가/제거하는 함수를 추가한다. 먼저 클라이언트가 수업을 하나씩 추가하고 제거하는 메서드를 Person에 추가해보자

//Person 클래스...addCourse(aCourse) {this._courses.push(aCourse);}removeCourse(aCourse, fnIfAbsent = () => {throw new RangeError();}) {    const index = this._courses.indexOf(aCourse);    if (index === -1) fnIfAbsent();    else this._courses.splice(index, 1);}

❹. 컬렉션의 변경자를 직접 호출하던 참조하는 부분을 모두 찾는다. 방금 추가한 메서드를 사용하도록 수정한다.

//클라이언트...for (const name of readBasicCourseNames(filename)) {    aPerson.addCourse(new Course(name, false));}

❷. 컬렉션 자체를 통째로 바꾸는 세터는 제거한다. 세터를 제거할 수 없다면 인수로 받은 컬렉션 을 복제해 저장하도록 만든다.

//Person 클래스...set courses(aList) {this._courses = aList.slice();}

❺. 이 메서드들을 사용하지 않고서는 아무도 목록을 변경할 수 없게 만드는 방식을 선호한다면, 복제본을 제공한다. 컬렉션 게터를 수정해서 원본 내용을 수정할 수 없는 읽기전용 프락시나 복제본을 반환하게 한다.

get courses() {return this._courses.slice();}

7-3 기본형을 객체로 바꾸기#

배경#

단순한 출력 이상의 기능이 필요해지는 순간

개발 초기에는 단순한 정보를 숫자열 같은 간단한 데이터 항목으로 표현할 때 많다. 그러다 개빌이 진행되면서 간단했던 데이터 항목들은 특별한 동작이 필요해지면서 중복 코드로 늘어난다. 저자는 단순한 출력 이상의 기능이 필요해지는 순간 그 데이터를 표현하는 전용 클래스를 정의한다. 시작은 별 차이가 없어보이지만 점차 코드가 복잡해질수록 그 진가를 발휘한다.

예시#

레코드 구조에서 데이터를 읽어 들이는 단순한 주문 클래스를 보자.

// Order 클래스    constructor(data) {        this.priority = data.priority;        //나머지 초기화 코드 생략    }

클라이언트에서는 이 코드를 다음처럼 사용한다.

highPriorityCount = orders.filters((o) => "high" === o.priority || "rush" === o.priority).length;

❶. 변수를 캡슐화하지 않았다면 캡슐화한다. 데이터 값을 다루기 전에 항상 변수부터 캡슐화한다.

//Order 클래스    get priority() { return this._priority; }    set priority() { this._priority = aString; }

우선순위 속성을 초기화하는 생성자에서 방금 정의한 세터를 사용할 수 있다. 이렇게 필드를 자가 캡슐화하면 필드 이름을 바꿔도 클라이언트 코드는 유지할 수 있다.

❷. 우선순위 속성을 표현하는 값 클래스 Priority를 만든다. 이 클래스는 표현할 값을 받는 생성자와 그 값을 문자열로 반환하는 변환 함수로 구성된다.

class Priority {    constructor(value) {        this._value = value;    }    toString() {        return this._value;    }}

❹❺. 방금 만든 Priority 클래스를 사용하도록 접근자들을 수정한다.

//Order 클래스...get priority() {return this._priority.toString();}set priority(aString) {this._priority = new Priority(aString);}

❼. 이렇게 Priority 클래스를 만들고 나면 Order 클래스의 게터가 이상해진다. 이 게터가 반환하는 값은 우선순위 자체가 아니라 우선순위를 표현하는 문자열이다. 그러니 즉시 함시 함수 이름을 바꿔준다. (함수 선언 바꾸기)

//Order 클래스...get priorityString() {return this._priority.toString();}set priority(aString) {this._priority = new Priority(aString);}
//클라이언트...highPriorityCount = orders.filter((o) => "high" === o.priorityString || "rush" === o.priorityString).length;

7-4. 임시 변수를 질의 함수로 바꾸기#

배경#

임시변수를 함수로 만들어 사용하는 편이 나을 때

함수 안에서 어떤 코드의 결괏값을 뒤에서 다시 참조할 목적으로 임시 변수를 쓰기도 한다. 임시 변수를 사용하면 값을 계산하는 코드가 반복되는 걸 줄이고 값의 의미를 설명할 수도 있어 유용하다.
하지만 변수 대신 함수로 만들어두면 비슷한 계산을 수행하는 다른 함수에서도 사용할 수 있어 코드 중복이 줄어든다. 여러 곳에서 똑같은 방식으로 계산되는 변수를 발견할 때마다 함수로 바꿀 수 있는지 살펴본다. 이 리팩터링은 클래스 안에서 적용할 때 효과가 가장 크다. 클래스는 추출할 메서드들에 공유 컨텍스트를 제공하기 때문이다.

예시#

//Order클래스...    constructor(quantity, item) {        this._quantity = quantity;        this._item = item;    }
    get price() {        var basePrice = this._quantity * this._item.price;        var discountFactor = 0.98;
        if(basePrice > 1000) {            discountFactor -= 0.03;            return basePrice * discountFactor;        }  }

❶. 변수가 사용되기 전에 값이 확실히 결정되는지, 변수를 사용할 때마다 계산 로직이 매번 다른 결과를 내지는 않는지 확인한다. 여기서 임시 변수인 basePrice와 discountFactor는 매번 다른 결과를 낼 수 있는 값이므로 메서드로 바꿔보자.

❷. 읽기 전용으로 만들 수 있는 변수는 읽기전용으로 만든다. basePrice에 const를 붙여 읽기전용으로 만들고 ❸. 테스트한다.

//Order클래스...    constructor(quantity, item) {        this._quantity = quantity;        this._item = item;    }
    get price() {        const basePrice = this._quantity = this._item.price;        var discountFactor = 0.98;
        if(basePrice > 1000) {            discountFactor -= 0.03;            return basePrice * discountFactor;        }  }

❹. 대입문의 우변을 게터로 추출한다.

//Order클래스...    get price() {            const basePrice = this.basePrice;            var discountFactor = 0.98;
            if(basePrice > 1000) {                discountFactor -= 0.03;                return basePrice * discountFactor;            }    }    get basePrice() {        return this._quantity * this._item.price;    }

❺. 테스트한 다음 ❻. 변수를 인라인한다.

//Order클래스...    get price() {            var discountFactor = 0.98;            if(basePrice > 1000) {                discountFactor -= 0.03;                return basePrice * discountFactor;            }    }

discountFactor 변수도 같은 순서로 처리한다.

//Order클래스...    get price() {        return this.basePrice * this.discountFactor    }

7-5. 클래스 추출하기#

배경#

메서드와 데이터가 너무 많은 클래스를 분류할 수 있을 때

클래스는 반드시 명확하게 추상화하고 소수의 주어진 역할만 처리해야 한다.
하지만 실무에서는 연산을 추가하고 데이터도 보강하면서 클래스가 점점 커진다. 이렇게 역할이 갈수록 많아지고 덧댈수록 클래스는 굉장히 복잡해진다.

예시#

//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;}

❶. 클래스의 역할을 분리할 방법을 정한다. 여기서 전화번호 관련 동작을 별도 클래스로 뽑아보자면,
❷. 분리될 역할을 담당할 클래스를 새로 만든다. 빈 전화번호를 표현하는 telephoneNumber 클래스를 정의한다.

class telephoneNumber {}

❸. 원래 클래스의 생성자에서 새로운 클래스의 인스턴스를 생성하여 필드에 저장해둔다. Person 클래스의 인스턴스를 생성할 때 전화번호 인스턴스도 함께 생성해 저장해둔다.

//Person 클래스...constructor() {    this._telephoneNumber = new TelephoneNumber()}
//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._officeNumber = arg;}
//Person 클래스...get officeNumber() {return this._telephoneNumber.officeNumber;}set officeNumber(arg) {this._telephoneNumber.officeNumber = arg;}

다시 테스트해보고, ❺. telephoneNumber() 메서드를 옮긴다.

//TelephoneNumber 클래스...get telephoneNumber() {return '(${this.officeAreaCode}) ${this.officeNumber}';}
//Person 클래스...get telephoneNumber() {return this._telephoneNumber.telephoneNumber;}

❻. 양쪽 클래스의 인터페이스를 살펴보면서 불필요한 메서드를 제거하고, 이름도 새로운 환경에 맞게 바꾼다. 새로 만든 클래스는 순수한 전화번호를 뜻하므로 사무실(office)이란 단어를 쓸 이유가 없다. 마찬가지로 전화번호라는 뜻도 메서드 이름에서 다시 강조할 이유가 없다. 그러므로 메서드들의 이름을 적절히 바꿔준다.(함수 선언 바꾸기)

//TelephoneNumber 클래스...get areaCode() {return this._areaCode;}set areaCode(arg) {this._areaCode = arg;}get number() {return this._number;}set number(arg) {this._number = arg;}
//Person 클래스...get officeAreaCode() {return this._telephoneNumber.areaCode;}set officeAreaCode(arg) {this._telephoneNumber.areaCode = arg;}get officeNumber() {return this._telephoneNumber.number;}set officeNumber(arg) {this._telephoneNumber.number = arg;}

마지막으로 전화번호를 사람이 읽기 좋은 포맷으로 출력하는 역할도 전화번호 클래스에 맡긴다.

//TelephoneNumber 클래스...toString() {return '(${this.areaCode}) ${this.number}';}
//Person 클래스...get telephoneNumber() {return this._telephoneNumber.toString();}

❼. 새 클래스를 외부로 노출할지 정한다. 노출하려거든 새 클래스에 참조를 값으로 바꾸기를 적용할지 고민해본다. 전화번호는 여러모로 쓸모가 많으니 이 클래스는 클라이언트에게 공개하는 것이 좋다. 'office'로 시작하는 메서드들을 없애고 TelephoneNumber의 접근자를 바로 사용하도록 바꿀 수 있다.

7-6. 클래스 인라인하기#

배경#

클래스 추출하기의 반대: 두 클래스를 하나로 합치기

특정 클래스에 남은 역할이 거의 없을 때
두 클래스의 기능을 지금과 다르게 배분하고 싶을 때

예시#

배송 추적 정보를 표현하는 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 클래스의 일부처럼 사용된다.

//Shipment 클래스...get trackingInfo() {    return this._trackingInformation.display;}get trackingInformation() {return this._trackingInformation;}set trackingInformation(aTrackingInformation) {    this._trackingInformation = aTrackingInformation;}

TrackingInformation 은 예전에는 유용했을지 몰라도 현재는 제 역할을 하고 있지 못하다. Shipment 클래스로 인라인하자. 먼저 TrackingInformation의 메서드를 호출하는 코드를 찾는다.

//클라이언트...aShipment.trackingInformation.shippingCompany = request.vendor;

❶. 소스 클래스의 각 public 메서드에 대응하는 메서드들을 타깃 클래스에 생성한다. 이 메서드들은 단순히 작업을 소스 클래스로 위임해야 한다. 외부에서 직접 호출하는 TrackingInformation의 메서드들을 모조리 Shipment로 옮긴다. 보통의 함수 옮기기와는 약간 다르게, Shipment에 위임 함수를 만들고 ❷. 클라이언트가 이를 호출하도록 수정하는 것이다.

//Shipment 클래스...set shippingCompany(arg) {this._trackingInformation.shippingCompany = arg;}
//클라이언트...aShipment.trackingInformation.shippingCompany = request.vendor;

클라이언트에서 사용하는 TrackingInformation의 모든 요소를 이런 식으로 처리한다.
❸. 소스 클래스의 메서드와 필드를 모두 타깃 클래스로 옮긴다. 다 고쳤다면 TrackingInformation의 모든 요소를 Shipment로 옮긴다.
먼저 display() 메서드를 인라인한다.

//Shipment 클래스...get trackingInfo() {    return '${this.shippingCompany}: ${this.trackingNumber}';}

다음은 배송 회사 필드 차례다.

//Shipment 클래스...get shippingCompany() {return this._trackingInformation._shippingCompany;}set shippingCompany(arg) {this._trackingInformation._shippingCompany = arg;}

이 과정을 반복하고, ❹. 소스 클래스인 TrackingInformation 클래스를 삭제한다.

//Shipment 클래스...get trackingInfo() {return '${this.shippingCompany}: ${this.trackingNumber}';}get shippingCompany() {return this._shippingCompany;}set shippingCompany(arg) {this._shippingCompany = arg;}get trackingNumber() {return this._trackingNumber;}set trackingNumber(arg) {this._trackingNumber = arg;}

7-7. 위임 숨기기#

배경#

메서드와 데이터가 너무 많은 클래스를 분류할 수 있을 때

모듈화 설계를 제대로 하는 핵심은 캡슐화이다.
캡슐화가 잘 되어 있다면 무언가를 변경해야 할 때 함께 고려해야 할 모듈 수가 적어져서 코드를 변경하기가 훨씬 쉬워진다.

예시#

사람(Person)과 사람이 속한 부서(Department)를 정의했다.

//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;}set manager(arg) {this._manager = arg;}

클라이언트에서 어떤 사람이 속한 부서의 관리자를 알고 싶다고 하자. 그러기 위해서는 부서의 객체부터 얻어와야한다.

//클라이언트...manager = aPerson.department.manager;

부서 클래스가 관리자 정보를 제공하고 있다.

❶ 이러한 의존성을 줄이려면 클라이언트가 부서 클래스를 볼 수 없게 숨기고, 대신 사람 클래스에서 간단한 위임 메서드를 만들면 된다.

//Person 클래스...get manager() {return this._department.manager;}

❷ 클라이언트가 이 메서드를 사용하도록 고친다.

//클라이언트...
manager = aPerson.department.manager;

❸ 모두 수정했다면, 위임객체를 얻는 사람 클래스의 department() 접근자를 삭제한다.

7-8. 중개자 제거하기#

배경#

위임 숨기기의 반대: 클래스가 단순히 중개자로 전락할 때

예시#

자신이 속한 부서(department) 객체를 통해 관리자(manager)를 찾는 사람(person) 클래스를 살펴보자

//클라이언트...manager = aPerson.manager;
//Person 클래스...get manager() {return this._department.manager;}
//Department 클래스...get manager() {return this._manager;}

department 에 접근하여 manager의 정보를 얻고 있는데, 이런 위임 메서드가 많아지면 Person 클래스의 상당 부분이 그저 위임하는데만 쓰일 것이다. 그럴 때는 중개자를 제거하는 편이 낫다.
❶ 먼저 위임 객체(Deparment)를 얻는 게터를 만든다.

//Person 클래스...get department() {return this._department;}

❷ 각 클라이언트가 부서 객체를 직접 사용하도록 고친다.

//클라이언트...manager = aPerson.department.manager;

❸ 클라이언트를 모두 고쳤다면 Person의 manager() 메서드를 삭제한다. Person에 단순한 위 임 메서드가 더는 남지 않을 때까지 이 작업을 반복한다.

위임 숨기기나 중개자 제거하기는 적당히 섞어서 써도 된다. 자주 쓰는 위임은 그대로 두는 편이 클라이언트 입장에서 편하거나 상황에 맞게 처리하면 된다.

7-9. 알고리즘 교체하기#

배경#

간명한 방법을 찾아내면 복잡한 기존 코드를 간명한 방식으로 고치기

리팩터링하면 복잡한 대상을 단순한 단위로 나눌 수 있지만, 때로는 알고리즘 전체를 걷어내고 훨씬 간결한 알고리즘으로 바꿔야 할 때가 있다. 문제를 더 확실히 이해하고 훨씬 쉽게 해결하는 방법을 발견했을 때 이렇게 한다. 내 코드와 똑같은 기능 을 제공하는 라이브러리를 찾았을 때도 마찬가지다.
이 작업에 착수하려면 반드시 메서드를 가능한 한 잘게 나눴는지 확인해야 한다. 거대하고 복 잡한 알고리즘을 교체하기란 상당히 어려우니 알고리즘을 간소화하는 작업부터 해야 교체가 쉬워진다.