본문 바로가기
python

리팩터링 7장

by 볼록티 2023. 3. 15.
728x90
반응형

7. 캡슐화

  • 모듈을 분리하는 가장 중요한 기준은 감추어야할 부분들을 다른 곳에서 보이지 않고 얼마나 잘 숨기느냐에 있다. 대표적인 형태로는 아래와 같은 방법으로 캡슐화해서 숨길 수 있다.
    • 레코드 캡슐화하기(1)
    • 컬렉션 캡슐화하기(2)
  • 기본형 데이터는 기본형을 객체로 바꾸기(3) 방법을 통해서 캡슐화할 수 있다.
  • 리팩터링 중 임시 변수가 걸리적 거릴때는 임시 변수를 질의 함수로 바꾸기(4)를 활용한다.
  • 클래스는 본래 정보를 숨기는 용도로 설계되는데, 클래스 추출하기(5), 클래스 인라인하기(6) 를 활용해 클래스를 만들 수 있다.
  • 클래스는 내부정보 뿐만아니라 사이의 연결관계를 숨기는 데도 유용한데 이 때 위임 숨기기(7)를 활용한다.
  • 너무 많이 숨기다보면 인터페이스가 비대해지니 이 때 반대 기법인 중재자 제거하기(8)를 사용한다.
  • 알고리즘을 통째로 바꿔야 할 때가 있는데, 이때는 함수 추출하기로 알고리즘 전체를 함수 하나에 담은 뒤 알고리즘 교체하기(9)를 적용한다.

7.1 레코드 캡슐화하기

  • 클래스로 레코드를 묶어 Class의 Getter를 통하여 값을 구분하자.
organization = {name: "애크미 구스베리", country: "GB"};

⬇️

class Organization{
  constructor(data) {
  this._name = data.name;
  this._country = data.country;
  }
  
  get name()  {return this._name;}
  set name(arg) {this._name = arg;}
  get country() {return this._country;}
  set country(arg) {this._country = arg;} 
}

배경.

  • 레코드는 의미 있는 단위로 데이터를 전달할 수 있게 해주지만, 계산해서 얻는 값과 그렇지 않은 값을 명확히 구분해 저장해야줘야 하는 번거로움이 있다.
  • 가변 데이터를 사용할 때, 객체를 사용하면 어떻게 저장했는지를 숨기면서 메서드로 제공할 수 있다.
  • 사용자는 무엇이 저장된 값이고 무엇이 계산된 값인지 알 필요가 없으며, 이렇게 캡슐화를 하면 이름을 바꿀 때도 좋다.
  • 해시맵처럼 프로그래밍 작업에는 유용하지만 필드를 명확히 알려주지 않아 코드를 직접 확인해야 하는 단점 때문에 명시적인 레코드로 리팩터링하는 것보다 차라리 레코드 대신 클래스를 사용하는 편이 낫다.

절차.

  1. 레코드를 담은 변수를 캡슐화한다.
    ➡️ 레코드를 캡슐화하는 함수의 이름은 검색하기 쉽게 지어준다.
  2. 레코드를 감싼 단순한 클래스로 해당 변수의 내용을 교체한다. 이 클래스에 원본 레코드를 반환하는 접근자도 정의하고, 변수를 캡슐화하는 함수들이 이 접근자를 사용하도록 수정한다.
  3. 테스트한다.
  4. 원본 레코드 대신 새로 정의한 클래스 타입의 객체를 반환하는 함수들을 새로 만든다.
  5. 레코드를 반환하는 예전 함수를 사용하는 코드를 4에서 만든 새 함수를 사용하도록 바꾼다. 필드에 접근할 때는 객체의 접근자를 사용한다. 적절한 접근자가 없다면 추가한다. 한 부분을 바꿀 때마다 테스트한다.
    ➡️ 중첩된 구조처럼 복잡한 레코드인 경우 먼저 데이터를 갱신하는 클라이언트들에 주의해서 살펴본다. 클라이언트가 데이터를 읽기만 한다면 데이터의 복제본이나 읽기전용 프락시를 반환할지 고려해보자.
  6. 클래스에서 원본 데이터를 반환하는 접근자와 (1에서 검색하기 쉬운 이름을 붙여둔) 원본 레코드를 반환하는 함수들을 제거한다.
  7. 테스트한다.
  8. 레코드의 필드도 데이터 구조인 중첩 구조라면 레코드 캡슐화하기컬렉션 캡슐화하기를 재귀적으로 적용한다.

예시.

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

➡️ organization 상수는 프로그램 곳곳에서 사용하는 자바스크립트 객체로써, 아래와 같이 읽고 쓴다.

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

상수를 캡슐화해보자(1).

function getRawDataOfOrganization() {return organization}

➡️ 읽고 쓰는 코드는 아래와 같이 바뀐다. 게터를 찾기 쉽도록 의도적으로 임시로 이상한 이름을 붙인 것입니다.

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

레코드를 아래와 같이 클래스로 바꾸고(2), 새 클래스의 인스턴스를 반환하는 함수를 새로 만든다(4).

----Organization 클래스...
  class Organization{
    constructor(data){
      this._data = data;
    }
  }

----최상위...
  const organization = new Organization({nama: "애크미 구스베리", country: "GB"});
  function getRawDataOfOrganization() {return organization._data;}
  function getOrganization() {return organization;}

레코드를 사용하던 코드에서 레코드를 갱신하던 코드는 모두 세터를 사용하도록 고친다(5).

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

➡️ 레코드를 읽는 코드는 모두 게터를 사용하도록 바꾼다.

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

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

function getRawDataOfOrganization() {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;}
}

➡️ 입력 데이터 레코드와의 연결을 끊어주는 이점이 생긴다. 특히 이 레코드를 참조하여 캡슐화를 깰 우려가 있는 코드가 많을 때 좋다. 데이터를 개별 필드로 펼치지 않았다면 _data를 대입할 때 복제하는 식으로 처리했을 것이기 때문이다.

JSON 문서처럼 여러 겹 중첩된 레코드인 경우에 캡슐화하는 방법을 알아보자. 갱신할 코드에 주의해야 하는 것은 같지만, 읽는 코드를 다룰 때는 선택지가 몇 가지 더 생긴다.

"1920":{
  name: "마틴 파울러",
  id: "1920",
  usages: {
    "2016": {
      "1": 50,
      "2": 55,
      // 나머지 달은 생략.
    },
    "2015":{
      "1": 70,
      "2": 63,
      // 나머지 달은 생략.
    }
  }
},
"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 getRawDataOfCustomer() {return customerData;}
function setRawDataOfCustomer(arg) {customerData = arg;}

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

----읽기 예...
function compareUsage(customerID, laterYear, month) {
  const later = getRawDataCustomers()[customerID].usages[laterYear][month];
  const earlier = getRawDataOfCustomers()[customerID].usages[laterYear-1][month];
  return {laterAmount: later, change: later - earlier};
}

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

---- class CustomerData{
  constructor(data) {
    this._data = data;
  }
}

---- 최상위...
  function getCustomerData()  {return customerData;}
  function getRawDataOfCustomers()  {return customerData._data;}
  function setRawDataOfCustomers(arg) {customerData = new CustomerData(arg);}

여기서 중요한 부분은 데이터를 쓰는 코드다. getRawDataOfCustomers()를 호출한 후에 데이터를 변경할 때도 주의를 해야한다.
고객 객체에는 세터가 없으니 데이터 구조 깊이 까지 들어가서 값을 바꾸는데 이를 보완하기 위해 아래와 같이 데이터 구조 안으로 들어가는 코드를 세터로 뽑아내는 작업을 해준다.

----쓰기 예....
setUsage(customerID, year, month, amount);

----최상위....
function setUsage(customerID, year, month, amonth){
  getRawDataOfCustomer()[customerID].usges[year][month] = amount;
}

이제 이 함수를 고객 데이터 클래스로 옮기면 아래와 같다. getCustomerData()뒤에 setUsage가 붙은 것을 알 수 있다.

----쓰기 예...
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 getRawDataOfCustomer() {return customerData.rawData;}
  function setRawDataOfCustomer(arg) {customerData = new CustomerData(arg);}
  

----CustomerData 클래스...
  get rawData() {
    return _.cloneDeep(this._data)
  }

깊은 복사는 lodash라이브러리의 cloneDeep()을 사용한다.

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

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

이 방법의 장점은 customerData의 모든 쓰임을 명시적인 API로 제공한다는 것이다.
이 클래스만 보면 데이터 사용방법을 모두 파악할 수 있다. 그리고 다른 방법으로는 실제 데이터를 제공할 수도 있다.

하지만 클라이언트가 데이터를 직접 수정하지 못하게 막을 방법이 없기 때문에 앞선 방법인 내부 데이터를 복제해서 제공하는 것이 간단하다고 할 수 있다.

두번째 방법은 레코드 캡슐화를 재귀적으로 하는 것으로써 가장 확실하게 제어할 수 있다.
이방법을 적용하려면 고객 정보 레코드를 클래스로 바꾸고, 바로 뒤에 나올 컬렉션 캡슐화하기로 레코드를 다루는 코드를 리팩터링해서 고객 정보를 다루는 클래스를 생성한다.

때로는 새로만든 클래스와 게터를 잘 혼합해서, 게터는 데이터 구조를 깊이 탐색하게 만들고, 원본 데이터를 그대로 반환하지 말고 객체로 감싸서 반환하는게 효과적일 수 있다.

7.2 컬렉션 캡슐화하기

  • 필요한 인터페이스만 노출해보자.
  • 무분별한 게터 세터를 난발하지 않고, 게터시 필요한 add(), remove()와 같은 인터페이스만 노출해보자.
class Person{
  get courses() {return this._coursees;}
  set courses(aList) {this._courses = aList;}
}

⬇️

class Person{
  get courses() {return this._courses.slice();}
  addCourse(aCourse) {...}
  removeCourse(aCourse) {...}
}

세터부분이 바뀐걸 알 수 있다.

배경.

  • 가변 데이터를 캡슐화하는걸 추천하는데, 객체 지향 개발자들은 컬렉션 변수(예: list, tuple)로의 접근을 캡슐화하면서 게터가 컬렉션 자체를 반환하도록 할 때, 해당 컬렉션을 감싼 클래스가 눈치채지 못하는 상태에서 컬렉션의 원소들이 바뀔 수 있다. 그래서 필자는 add(), remove()와 같은 컬렉션 변경자 메소드를 사용한다고 한다.
  • 내부 컬렉션을 수정하지 못하게 하기 위해서 내부 컬렉션에 접근시 클래스의 적절한 메소드를 반드시 거치도록 한다.
  • 컬렉션을 읽기전용으로 제공할 수 있다.
  • 가장 흔히 사용하는 방식은 컬렉션 세터를 제공하되 내부 컬렉션의 복제본을 반환하는 것이다.

절차.

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

예시.

----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._course.indexOf(aCourse)
    if (index === -1) fnIfAbsent();
    else this._courses.splice(index, 1);
  }

방금 추가한 메서드를 호출하도록 아래와 같이 코드를 수정한다.

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

더이상 setCourses()를 사용할 일이 없으니 제거해준다. 세터를 제공해야할 특별한 이유가 있다면 아래의 코드와 같이 복제본을 필드에 저장하도록 한다.

----Person 클래스...
  set courses(aList) {this._courses = aList.slice();}
  
// 메서드들을 사용하지 않고 아무도 목록을 변경할 수 없게하고 싶다면 아래와 같이 복제본을 제공하도록 한다.
  get courses() {return this._courses.slice();}

강박을 갖고 불필요한 복제본을 만드는게 디버깅하는 것 보다 낫다. 컬렉션 관리를 책임지는 클래스라면 항상 복제본을 제공해야 한다.

7.3 기본형을 객체로 바꾸기

  • 단순히 String이나 Number로 사용되던 특정 상태를 객체로 바꿉니다.
  • 객체로 바꾸면 함수(higherThan)를 추가할 수 있으므로 상태를 비교하는 작업 등을 객체 내부로 캡슐화할 수 있습니다.
orders.filter(o => "high" === o.priority || "rush" === o.priority);

⬇️

orders.filter(o => o.priority.higherThan(new Priority("normal")))

배경.

  • 필자는 데이터를 표현하는 전용 클래스를 정의하는 편이며 프로그램이 커질 수록 유용하다고 함.
  • 단순한 정보를 숫자나 문자열 같은 간단한 데이터 항목으로 표현하지만 나중에 포매팅이나 지역코드 추출 같은 특별한 동작이 필요해질 때 드는 노력을 줄일 수 있다.

절차.

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

예시.

----Order 클래스...
  constructor(data){
    this.priority = data.priority;
    //나머지 초기화 코드 생략
  }
  
----클라이언트...
  highPriorityCount = orders.filter(o => "high" === o.priority || "rush" === o.priority).length;
  
  

변수 캡슐화(1)

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

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

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

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

변환함수(toString()) 를 사용함으로써 클라이언트 입장에서 속성 자체를 받은게 아니라 해당 속성을 문자열로 표현한 값을 요청한게 된다.

만든 Priority 클래스를 사용하도록 접근자들을 수정한다(4).

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

현재 게터가 반환하는 값은 우선순위 자체가 아니라 우선순위를 표현하는 문자열이다. 그래서 아래와 같이 함수 이름(priorityString)을 바꿔준다.

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

String이 붙어서 유형을 쉽게 알 수 있다면 세터의 이름은 그대로 둬도 좋다.

더 가다듬기.

Priority 객체를 제공하는 게터를 Order 클래스에 만들어 본다.

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

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

Priority 클래스는 다른 곳에서도 유용할 수 있으니 Order의 세터가 Priority 인스턴스를 받도록 해주기 위해 Priority의 생성자를 아래와 같이 변경한다.

----Priority 클래스...
  constructor(value) {
    if (value instanceof Priority) return value;
    this._value = value;
  }

우선순위 값을 검증하고 비교하는 로직을 추가한 예는 아래의 코드와 같다.

----Priority 클래스...
  constructor(value) {
    if (value instanceof Priority) return value;
    if (Priority.legalValues().includes(value))
      this._value = value;
    else
      throw new Error(`<${value}>는 유효하지 않은 우선순위입니다.`);
  }
  
  toString() {return this._value;}
  get _index() {return Priority.legalValues().findIndex(s => s === this._value);}
  static legalValues() {return ['low', 'normal', 'high', 'rush'];}
  equals(other) {return this._index > other._index;}
  higherThan(other) {return this._index > other._index;}
  lowerThan(other) {return this._index < other._index;}

위와 같이 우선순위를 값 객체로 만들기 위해 equals() 메서드를 추가해 불변이 되도록하면 아래와 같이 클라이언트 코드를 더 의미있게 작성할 수 있다.

----클라이언트...
  highPriorityCount = orders.filter(o => o.priority .higherThan(new Priority("normal"))).length;

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

  • 비즈니스 로직에 있는 계산된 임시 변수를 제거합니다.
  • 계산된 임시 변수는 함수로 캡슐화합니다.
const basePrice = this._quantity * this._itemPrice;
if (basePrice > 1000)
  return basePrice
else
  return basePrice * 0.98

⬇️

get basePrice() {this._quantity * this._itemPrice;}
...
if (this.basePrice > 1000)
  return this.basePrice * 0.95;
else
  return this.basePrice * 0.98;

배경.

  • 함수 안에서 결괏값을 다시 참조할 목적으로 임시 변수를 쓰기도 하는데 이는 계산 코드의 반복을 줄이고 값의 의미를 설명할 수 있어 유용하지만 함수로 만들어 사용하는 편이 나을 때가 많다.
  • 추출한 함수에서 별도로 변수를 따로 전달할 필요가 없어 추출한 함수와 원래 함수와의 경계를 더 분명히 할 수 있다. 이로써 부자연스러운 의존 관계나 부수효과를 찾고 제거하는데 좋다.
  • 변수에 값을 한번 대입하고 더 복잡한 코드에 다시 대입하는 경우는 모두 질의 함수로 추출해야한다.

절차.

  1. 변수가 사용되기 전에 값이 확실히 결정되는지, 변수를 사용할 때마다 계산 로직이 매번 다른 결과를 내지는 않는지 확인한다.
  2. 읽기전용으로 만들 수 있는 변수는 읽기전용으로 만든다.
  3. 테스트한다.
  4. 변수 대입문을 함수로 추출한다.
    ➡️ 변수와 함수가 같은 이름을 가질 수 없다면 함수 이름을 임시로 짓는다. 또한, 추출한 함수가 부수효과를 일으키지는 않는지 확인한다. 부수효과가 있다면 질의 함수와 변경 함수 분리하기로 대처한다.
  5. 테스트한다.
  6. 변수 인라인하기로 임시변수를 제거한다.

예시.

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

테스트를 했다면 변수를 인라인한다. basePrice를 제거할 수 있게 됨.

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

discountFactor 변수도 같은 순서로 처리해준다. discountFactor에 값을 대입하는 문장이 두개인데, 모두다 추출한 함수에 넣어야한다. 원본 변수는 마찬가지로 const로 만든다.

// 먼저 함수추출하기를 한다.

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

마지막으로 변수를 인라인한다.

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

7.5 클래스 추출하기

클래스의 일부 데이터+로직을 별개의 클래스로 추출
역할을 분리할 수 있음
  • 데이터와 메서드를 따로 묶을 수 있는 경우
  • 함께 변경되는 일이 많거나 서로 의존하는 데이터
  • 제거해도 다른 필드나 메서드에 문제 없으면 분리 가능
class Person{
  get officeAreaCode() {return this._officeAreaCode;}
  get officeNumber() {return this._officeNumber;}
}

⬇️

class Person{
  get officeAreaCode() {return this._telephoneNumber.areaCode;}
  get officeNumber() {return this._telephoneNumber.number;}
}
class telephoneNumber {
  get areaCode() {return this._areaCode;}
  get number() {return this._number;}
}

배경.

  • 실무에서 연산을 추가하고 데이터를 보강하다보면 클래스는 점점 비대해져서 복잡해지게 된다.
  • 일부 데이터와 메서드를 따로 묶을 수 있다면 클래스를 분리하자.
  • 함께 변경되는 일이 많거나 서로 의존하는 데이터들도 분리한다.
  • 제거해도 다른 필드나 메서드들이 논리적으로 문제가 없다면 분리할 수 있다.

절차.

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

예시.

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

새로만든 클래스가 전화번호 자체를 의미하니 메서드들의 이름을 적절히 바꿔준다.

----TelephoneNumber 클래스...
  get areaCode()  {return this._areaCode;}
  set areaCode(arg) {this._areaCode = arg;}
  get number()  {return this._number;}
  set number(arg) {return 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 클래스 인라인하기

class Person{
  get officeAreaCode() {return this._telephoneNumber.areaCode;}
  get officeNumber(arg) {this._telephoneNumber.number = arg;}
}
class TelephoneNumber {
  get areaCode()  {return this._areaCode;}
  get number()  {return this_number;}
}

⬇️

class Person {
  get officeAreaCode()  {return this._officeAreaCode;}
  get officeNumber()  {return this._officeNumber;}
}

배경.

  • 클래스 추출하기를 거꾸로 돌리는 리펙터링이다.
  • 제 역할을 못 해서 그대로 두면 안 되는 클래스가 대상이며 역할을 옮기는 리패터링 후 특정 클래스에 남은 역할이 거의 없을 때 주로 진행한다.
  • 이 클래스의 역할을 가장 많이 사용하는 클래스로 흡수한다.
  • 두 클래스의 기능을 지금과 다르게 배분하고 싶을 때에도 사용하는데, 두 클래스를 인라인 해서 하나로 합친 후, 다시 새로운 클래스로 추출할 수 있다.

절차.

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

예시.

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;

외부에서 직접 호출하는 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};`
  }
  
//그다음 ShippingCompany필드를 수정한다.
----Shipment 클래스...
  get shippingCompany() {return this._trackingInformation._shippingCompany;} //_trackingInformation 삭제
  set shippingCompany()arg {this._trackingInformation._shippingCompany = arg;} // _trackingInformation 삭제

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

Shipment클래스로 전부 옮겨지게 됐다!

7.7 위임 숨기기

manager = aPerson.department.manager;

⬇️

manager = aPerson.manager;

class Person {
  get manager() {return this.department.manager;}
}

배경.

  • 모듈화 설계를 제대로 하는 핵심은 캡슐화.
  • 의존성을 없애기 위해 서버 자체에 위임 메서드를 만들어서 위임 객체의 존재를 숨겨 위임 객체가 수정되도 서버 코드만 고치면 되서 클라이언트는 아무런 영향을 받지 않음.
  • 즉, 클라이언트가 위임 객체의 존재를 몰라도 되도록 감춤

절차.

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

클라이언트에서 어떤 사람이 속한 부서의 관리자를 알고 싶다고 하면, 부서 객체부터 가져와야한다.
클라이언트는 부서 클래스의 작동 방식, 관리자 정보를 제공한다는 사실을 알아야 한다. 이 때 의존성을 줄이려면 클라이언트가 부서 클래스를 볼 수 없게 숨기고, 사람 클래스에 위임 메서드를 만들어준다.

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

모든 클라이언트가 이 메서드를 사용하도록 고쳐준다.

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

클라이언트 코드를 다 고쳤다면 사람 클래스의 department() 접근자를 삭제한다.

----클라이언트...
  manager = aPerson.department.manager; #department 삭제.

7.8 중개자 제거하기

manaer = aPerson.manager;

class Person {
  get manager() {return this.department.maneger;}
}

배경.

  • 클라이언트가 위임 객체의 다른 기능을 사용하고 싶을 때마다 서버의 위임 메서드가 추가로 필요하니 클라이언트가 위임 객체를 직접 호출하는게 나을 수 있음.
  • 앞장의 위임 숨기기와의 균형점은 상황에 따라 다름.
  • 서버 클래스가 단순히 중개자 역할만 할 때 성가시기 때문에 제거.

‘디미터의 법칙'이라고도 하며 최소 지식 원칙(principle of least knowledge). 내부 정보를 가능한 한 숨기고 밀접한 모듈과만 상호작용하여 결합도를 낮추자는 원칙.
자칫하면 이과정에서 위임 혹은 래퍼(wrapper) 매서드가 너무 늘어나는 부작용이 일어날 수 있음.

절차.

  1. 위임 객체를 얻는 게터를 만든다.
  2. 위임 메서드를 호출하는 클라이언트가 모두 이 게터를 거치도록 수정한다. 하나씩 바꿀 때마다 테스트한다.
  3. 모두 수정했다면 위임 메서드를 삭제한다.
    -> 자동 리팩터링 도구를 사용할 때는 위임 필드를 캡슐화한 다음 이를 사용하는 모든 메서드를 인라인한다.

예시.

사용하기 쉽게 부서는 캡슐화가 되어 있다. 하지만 이런 위임 메소드가 많아지면 사람 클래스의 상당 부분이 그저 위임하는데만 쓰인다. 이럴 때 중개자를 제거하는 편이 낫다.

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

먼저 위임 객체(department)를 얻는 게터를 만든다.

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

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

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

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

위임 숨기기나 중개자 제거하기를 적당히 섞어도 된다. 자주 쓰이는 위임은 남겨둔다. 둘 중 하나를 고를 필요는 없음. 상황에 맞게 처리하는 것이 현명하다.

자동 리팩터링을 사용한다면 먼저 부서 필드를 캡슐화한다. 그러면 관리자 게터에서 부서의 public 게터를 사용할 수 있다.

----Person 클래스...
  get manager() {return this.department.manager;}

자바스크립트에서는 이 변화가 잘 드러나지 않지만, department 앞의 밑줄 '_' 을 빼면 더이상 필드를 직접 접근하지 않고 새로 만든 게터를 사용한다는 뜻이다.
그런 다음 manager() 메서드를 인라인하여 모든 호출자를 한 번에 교체한다.

7.9 알고리즘 교체하기

function foundPerson(people){
  for(let i = 0; < people.length; i++){
    if (people[i] == "Don"){
      return "Don";
    }
    if (people[i] == "Kent"){
      return "Kent";
    }
    if (people[i] == "John"){
      return "John";
    }
  }
  return "";
}

⬇️

function foundPerson(people){
  const candidates = ["Don", "John", "Kent"];
  return people.find(p => candidates.includes(p)) || ;
}

배경.

  • 복잡한 기존 코드를 더 간명한 방식으로 고침.
  • 문제를 더 확실히 이해하고 훨씬 쉽게 해결하는 방법을 발견했을 떄
  • 내 코드와 똑같은 기능을 제공하는 라이브러리가 있을 때
  • 알고리즘을 살짝 다르게 동작하도록 바꾸고 싶을 때
  • 이 작업을 하기 위해서는 반드시 메서드를 가능한 한 잘게 나눴는지 확인해야함.
  • 거대하고 복잡한 알고리즘을 교체하기란 상당히 어려우니 알고리즘을 간소화하는 작업부터 해야 교체가 쉬워짐.

절차.

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

'python' 카테고리의 다른 글

리팩터링 2탄 5/6장  (1) 2023.02.22
리팩터링 2/3/4장.  (0) 2023.02.12
리펙터링 1장.  (0) 2023.02.07
python matplotlib.pyplot 한글 깨짐 해결하기.  (3) 2020.05.24
scatter plot 그리기  (0) 2020.05.14

댓글