[리팩터링 2판] - 캡슐화

Lee Jeong Min·2022년 8월 28일
0

리팩터링 2판

목록 보기
7/12
post-thumbnail

리팩터링 2판의 Chatper 07를 보고 정리한 글입니다.

모듈을 분리하는 가장 중요한 기준은 시스템에서 각 모듈이 자신을 제외한 다른 부분에 드러나지 않아야 할 비밀을 얼마나 잘 숨기느냐에 있을 것이다.

이 장에서는 위와 같은 캡슐화와 관련된 리팩터링을 다룬다.

레코드 캡슐화화기

배경

레코드는 연관된 여러 데이터를 직관적인 방식으로 묶을 수 있어서 각각을 따로 취급할 때보다 훨씬 의미 있는 단위로 전달할 수 있게 해준다. 그러나 계산해서 얻을 수 있는 값과 그렇지 않은 값을 명확히 구분해 저장해야 하는 점이 번거롭다.

이런 경우 가변 데이터를 저장하는 용도로 저자는 레코드보다 객체(클래스로 만든)를 선호한다고 한다.

절차

  1. 레코드를 담은 변수를 캡슐화한다.
  2. 레코드를 감싼 단순한 클래스로 해당 변수의 내용을 교체한다. 이 클래스에 원본 레코드를 반환하는 접근자도 정의하고, 변수를 캡슐화하는 함수들이 이 접근자를 사용하도록 수정한다.
  3. 테스트한다.
  4. 원본 레코드 대신 새로 정의한 클래스 타입의 객체를 반환하는 함수들을 새로 만든다.
  5. 레코드를 반환하는 예전 함수를 사용하는 코드를 4에서 만든 새 함수를 사용하도록 바꾼다. 필드에 접근할 때는 객체의 접근자를 사용한다.
  6. 클래스에서 원본 데이터를 반환하는 접근자와 원본 레코드를 반환하는 함수들을 제거한다.
  7. 테스트한다.
  8. 레코드의 필드도 데이터 구조인 중첩 구조라면 레코드 캡슐화하기와 컬렉션 캡슐화하기를 재귀적으로 적용한다.

예시

간단한 레코드 캡슐화하기

리팩터링 전의 코드

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

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

리팩터링 이후 코드

  1. 먼저 이 상수를 캡슐화하자.(변수 캡슐화하기)
function getRawDataOfOrganization() { return organization;}

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

이렇게 캡슐화하는 목적은 변수 자체는 물론 그 내용을 조작하는 방식도 통제하기 위해서다.

  1. 레코드를 클래스로 바꾸고, 4 새 클래스의 인스턴스를 반환하는 함수를 새로 만든다.
class Organization {
  constructor(data) {
    this._data = data;
  }
}

const organization = new Organization({ name: '애크미 구스베리', country: 'GB' });
function getRawDataOfOrganization() {
  return organization._data;
}
function getOrganization() {
  return organization;
}
  1. 레코드를 갱신하던 코드는 모두 세터를 사용하도록 고친다. 6. 다 바꿨다면 앞에서 이상한 이름으로 지었던 임시 함수를 제거한다.(getRawDataOfOrganization 함수 제거)
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. 레코드 캡슐화를 재귀적인 방법으로 데이터 구조를 제공할 수 있다.

컬렉션 캡슐화하기

배경

가변 데이터를 모두 캡슐화하면 데이터 구조가 언제 어떻게 수정되는지 파악하기 쉬워진다. 컬렉션과 같은 가변 데이터를 다룰 때, 게터가 컬렉션 자체를 반환하도록 하면 그 컬렉션의 원소들이 바뀌어 버릴 수 있다.

이런 문제를 방지하기 위해 항상 컬렉션을 소유한 클래스를 통해서만(클래스안의 메서드) 원소를 변경하도록 하면 프로그램을 개선하면서 컬렉션 변경 방식도 원하는 대로 수정할 수 있다.

컬렉션 게터가 원본 컬렉션을 반환하지 않게 만들어서 클라이언트가 실수로 컬렉션을 바꿀 가능성을 차단하는게 낫다.

절차

  1. 아직 컬렉션을 캡슐화하지 않았다면 변수 캡슐화하기부터 한다.
  2. 컬렉션에 원소를 추가/제거하는 함수를 추가한다.
    → 컬렉션 자체를 통째로 바꾸는 세터는 제거한다.
  3. 정적 검사를 수행한다.
  4. 컬렉션을 참조하는 부분을 모두 찾는다. 컬렉션의 변경자를 호출하는 코드가 모두 앞에서 추가한 추가/제거 함수를 호출하도록 수정한다. 하나씩 수정할 때마다 테스트한다.
  5. 컬렉션 게터를 수정해서 원본 내용을 수정할 수 없는 읽기전용 프락시나 복제본을 반환하게 한다.
  6. 테스트한다.

예시

리팩터링 전의 코드

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

현재 이 코드는 세터를 이용해 수업 컬렉션을 통째로 설정한 클라이언트는 누구든 이 컬렉션을 마음대로 수정할 수 있다.

리팩터링 이후 코드

  1. 제대로 캡슐화를 위해 수업을 하나씩 추가하고 제거하는 메서드를 Person에 추가하자.
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);
  }
  1. 컬렉션의 변경자를 직접 호출하던 코드를 모두 찾아 방금 추가한 메서드를 사용하도록 변경한다.
// 클라이언트측 코드 변경
  1. 개별 원소를 추가하고 제거하는 메서드를 제공하기 때문에 기존의 세터는 사용할 이유가 없어 제거한다.
    → 세터를 제공해야할 이유가 있다면 컬렉션의 복제본을 필드에 저장하게 만든다.
  1. 게터를 사용할때 복제본을 반환하도록 만든다.
get courses() {return this._courses.slice();}

저자는 컬렉션을 관리할 때 복제본을 만들어서 제공하는 것을 추천함.

기본형을 객체로 바꾸기

배경

단순한 출력 이상의 기능이 필요해지는 순간 그 데이터를 표현하는 전용 클래스를 정의하자.

나중에 특별한 동작이 필요해지면 이 클래스에 추가하면 되니 프로그램이 커질수록 유용한 도구가 된다.

절차

  1. 아직 변수를 캡슐화하지 않았다면 캡슐화한다.
  2. 단순한 값 클래스를 만든다. 생성자는 기존 값을 인수로 받아서 저장하고, 이 값을 반환하는 게터를 추가한다.
  3. 정적 검사를 수행한다.
  4. 값 클래스의 인스턴스를 새로 만들어서 필드에 저장하도록 세터를 수정한다. 이미 있다면 필드의 타입을 적절히 변경한다.
  5. 새로 만든 클래스의 게터를 호출한 결과를 반환하도록 게터를 수정한다.
  6. 테스트한다.
  7. 함수 이름을 바꾸면 원본 접근자의 동작을 더 잘 드러낼 수 있는지 검토한다.
    → 참조를 값 또는 참조로 바꾸었을 때, 객체의 역할이 잘 드러나는지 검토한다.

예시

리팩터링 전의 코드

class Order {
  this.priority = data.priority;
  // 나머지 초기화 코드
}

// 클라이언트
highPriorityCount = orders.filter(o => 'high' === o.priority || "rush" === o.priority).length;

리팩터링 이후의 코드

  1. 데이터 값을 다루기 전에 항상 변수부터 캡슐화한다.
class Order {
  this._priority = data.priority;

  get priority() { return this._priority;}
  set priority(aString) { this._priority = aString;}
}
  1. 우선순위 속성을 표현하는 값 클래스 Priority를 만든다.
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);}
}
  1. 이렇게 Priority 클래스를 만들고 나면 Order 클래스의 게터가 이상해진다. 이 게터가 반환하는 값은 우선순위 자체가 아니라 우선순위를 표현하는 문자열이다. 그러니 함수 이름을 바꿔준다.
get priorityString() { return this._priority.toString();}

공식적인 리팩터링은 이후 나오는 '더 가다듬기'라는 부분 이해가 잘 안가서 스터디때 논의 해보면 좋을듯
→ 저런식으로 작성했을 때 왜 더 의미있는 코드가 되는지?

임시 변수를 질의 함수로 바꾸기

배경

변수 대신 함수로 만들어 두면 비슷한 계산을 수행하는 다른 함수에서도 사용할 수 있어 코드 중복이 줄어든다.

또한 함수간에도 긴 함수의 한 부분을 별도의 함수로 추출하면 그 경계가 더 분명해지기도 하고, 이런 경우 부자연스러운 의존 관계나 부수효과를 갖고 제거하는 데 도움이 된다.

이번 리팩터링은 클래스 안에서 적용할 때 효과가 가장 크다. 클래스는 추출할 메서드들에 공유 컨텍스트를 제공하기 때문이다.
→ 스냅숏 용도로 쓰이는 변수에는 이 리팩터링을 적용하면 안된다.

절차

  1. 변수가 사용되기 전에 값이 확실히 결정되는지, 변수를 사용할 때마다 계산 로직이 매번 다른 결과를 내지는 않는지 확인한다.
  2. 읽기전용으로 만들 수 있는 변수는 읽기전용으로 만든다.
  3. 테스트한다.
  4. 변수 대입문을 함수로 추출한다.
  5. 테스트한다.
  6. 변수 인라인하기로 임시 변수를 제거한다.

예시

예시코드는 클래스 내부에서 사용되는 임시 변수를 게터로 빼내는 과정을 설명하고 있어서 간단하여 따로 작성 X

클래스 추출하기

반대되는 리팩터링: 클래스 인라인하기

배경

메서드와 데이터가 너무 많은 클래스는 이해하기가 쉽지 않으니 잘 살펴보고 적절히 분리하는 것이 좋다.

클래스는 반드시 명확하게 추상화하고 소수의 주어진 역할만 처리해야 한다는 가이드라인을 따르자.

절차

  1. 클래스의 역할을 분리할 방법을 정한다.
  2. 분리될 역할을 담당할 클래스를 새로 만든다.
  3. 원래 클래스의 생성자에서 새로운 클래스의 인스턴스를 생성하여 필드에 저장해둔다.
  4. 분리될 역할에 필요한 필드들을 새 클래스로 옮긴다. 하나씩 옮길 때마다 테스트한다.
  5. 메서드들로 새 클래스로 옮긴다. 이때 저수준 메서드, 즉 다른 메서드를 호출하기보다는 호출을 당하는 일이 많은 메서드부터 옮긴다. 하나씩 옮길 때마다 테스트한다.
  6. 양쪽 클래스의 인터페이스를 살펴보면서 불필요한 메서드를 제거하고, 이름도 새로운 환경에 맞게 바꾼다.
  7. 새 클래스를 외부로 노출할지 정한다. 노출하려거든 새 클래스에 참조를 값으로 바꾸기를 적용할지 고민해본다.

예시

리팩터링 전의 코드

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

리팩터링 이후 코드

  1. 여기서 전화번호 관련 동작을 별도 클래스로 뽑아보자. 2 먼저 빈 전화번호를 표현하는 TelephoneNumber 클래스를 정의한다.
class TelephoneNumber {
  
}
  1. 다음으로 Person 클래스의 인스턴스를 생성할 때 전화번호 인스턴스도 함께 생성해 저장해둔다.
class Person {
  constructor() {
    this._telephoneNumber = new TelephoneNumber();
  }
}

class TelephoneNumber {
  get officeAreaCode() { return this._officeAreaCode;}
  set officeAreaCode(arg) {this._officeAreaCode = arg;}
}
  1. 그런 다음 필드들을 하나씩 새 클래스로 옮긴다.
// 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;
  }
  1. 정리할 차례로, 메서드 이름의 부분을 적절히 바꿔준다.
// TelephoneNumber 클래스와 Person 클래스의 메서드 명을 바꿔줌
  1. 전화번호라는 클래스는 여러모로 쓸모가 많으니 클라이언트에게 공개하자.
    → 이왕 쓸것이라면 전화번호를 값 객체로 만드는 게 나으니 참조를 값으로 바꾸기부터 적용하자.(이 리팩터링은 9장에서 다룸)

클래스 인라인하기

반대 리팩터링: 클래스 추출하기

배경

두 가지의 경우 이 클래스 인라인하기를 사용한다.

  • 특정 클래스에 남은 역할이 거의 없을 때 인라인하기 적용
  • 두 클래스의 기능을 지금과 다르게 배분하고 싶을 때도 클래스를 인라인(클래스를 합친뒤 새로운 클래스를 추출하는 식으로 사용)

절차

  1. 소스 클래스의 각 public 메서드에 대응하는 메서드들을 타깃 클래스에 생성한다. 이 메서드들은 단순히 작업을 소스 클래스로 위임해야 한다.
  2. 소스 클래스의 메서드를 사용하는 코드를 모두 타깃 클래스의 위임 메서드를 사용하도록 바꾼다. 하나씩 바꿀 때마다 테스트한다.
  3. 소스 클래스의 메서드와 필드를 모두 타깃 클래스로 옮긴다. 하나씩 옮길 때마다 테스트한다.
  4. 소스 클래스를 삭제하고 조의를 표한다.

예시

리팩터링 전의 코드

배송 추적 정보를 표현하는 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;

리팩터링 이후 코드

  1. 외부에서 직접 호출하는 TrackingInformation의 메서드들을 모조리 Shipment로 옮긴다. 먼저 Shipment에 위임 함수를 만들고 2. 클라이언트가 이를 호출하도록 수정하자.
// Shipment 클래스
set shippingCompany(arg) {this._trackingInformation.shippingCompany = arg;}

// 클라이언트
aShipment.shippingCompany = request.vendor;
  1. 클라이언트에서 사용하는 TrackingInformation의 모든 요소를 다 위와 같은 방법으로 고쳤다면 TrackingInformation의 모든 요소를 Shipment로 옮긴다.
// Shipment 클래스
get trackingInfo() {
  return `${this.shippingCompany}: ${this.trackingNumber}` 
}

// 배송 회사 필드 차례
// Shipment 클래스
  get shippingCompany() {return this._shippingCompany;}
  set shippingCompany(arg) {this._shippingCompany = arg;}

다 옮겼다면 TrackingInformation 클래스를 삭제한다.

위임 숨기기

반대 리팩터링: 중개자 제거하기

배경

모듈화 설계를 제대로 하는 핵심은 캡슐화이지만, 캡슐화란 단순히 필드를 숨기는 것뿐만 아니라, 위임 객체와 같은 역할도 있다.

의존성을 없애기 위해 위임 객체를 숨겨 클라이언트에 아무런 영향을 주지 않도록 하자.

절차

  1. 위임 객체의 각 메서드에 해당하는 위임 메서드를 서버에 생성한다.
  2. 클라이언트가 위임 객체 대신 서버를 호출하도록 수정한다. 하나씩 바꿀 때마다 테스트한다.
  3. 모두 수정했다면, 서버로부터 위임 객체를 얻는 접근자를 제거한다.
  4. 테스트한다.

예시

리팩터링 전의 코드

사람과 사람이 속한 부서를 다음처럼 정의

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

부서 클래스가 관리자 정보를 제공한다는 사실을 클라이언트가 알아야한다.
→ 부서 클래스의 작동방식을 알아야한다.

리팩터링 이후 코드

  1. 이러한 의존성을 줄이려면 클라이언트가 부서 클래스를 볼 수 없게 숨기고, 대신 사람 클래스에 간단한 위임 메서드를 만들면 된다.
// Person 클래스
get manager() {return this._department.manager;}
  1. 모든 클라이언트가 이 메서들르 사용하도록 고친다.
manager = aPerson.manager;
  1. 클라이언트 코드를 다 고쳤다면 사람 클래스의 department() 접근자를 삭제한다.

중개자 제거하기

반대 리팩터링: 위임 숨기기

배경

위임 숨기기 배경에서는 위임 객체를 캡슐화하는 이점을 설명했다. 그러나 이러한 위임만 하게되면, 단순히 전달만 하는 위임 메서드들이 점점 성가셔지고 클라이언트가 위임 객체를 직접 호출하는게 나을 수 있다.

어느 정도까지 숨겨야 적절한지 판단하기 어렵고 그때마다 위임 숨기기, 중개자 제거하기 리팩터링을 하자.

절차

  1. 위임 객체를 얻는 게터를 만든다.
  2. 위임 메서드를 호출하는 클라이언트가 모두 이 게터를 거치도록 수정한다. 하나씩 바꿀 때마다 테스트한다.
  3. 모두 수정했다면 위임 메서드를 삭제한다.

예시

위임 숨기기 예시를 거꾸로 하는 것과 유사하게 진행된다. (책 참조)

알고리즘 교체하기

배경

문제를 더 확실히 이해하고 훨씬 쉽게 해결하는 방법을 발견했을 때 알고리즘 전체를 걷어내고 훨씬 간결한 알고리즘으로 바꿔보자.

가능한 한 메서드를 잘게 나눴는지 확인하면서 거대하고 복잡한 알고리즘을 차근차근 간소화하는 작업부터 진행하여 교체하자.

절차

  1. 교체할 코드를 함수 하나에 모은다.
  2. 이 함수만을 이용해 동작을 검증하는 테스트를 마련한다.
  3. 대체할 알고리즘을 준비한다.
  4. 정적 검사를 수행한다.
  5. 기존 알고리즘과 새 알고리즘의 결과를 비교하는 테스트를 수행한다. 두 결과가 같다면 리팩터링이 끝난다. 그렇지 않다면 기존 알고리즘을 참고해서 새 알고리즘을 테스트하고 디버깅한다.

예시

책에 따로 나온 예시는 없음

profile
It is possible for ordinary people to choose to be extraordinary.

0개의 댓글