본문 바로가기
Programming/13. Book

리팩터링 - 7장

by @sangseophwang 2024. 3. 2.

Chapter 07. 캡슐화

✏️ 레코드 캡슐화하기

배경

  • 대부분의 프로그래밍 언어는 데이터 레코드를 표현하는 구조를 제공한다.
  • 이 구조에는 장, 단점이 있는데,
    • 데이터를 직관적인 방식으로 묶을 수 있어서 의미 있는 단위로 전달할 수 있게 해준다는 장점이 있다.
    • 계산해서 얻을 수 있는 값과 그렇지 않은 값을 명확히 저장해야 하는 단점이 있다.
      • e.g.) { start: 1, end: 5, length: 5 } → 값의 범위에 해당하는 length는 연산 값일 수 있다.
  • 이에 가변 데이터를 저장하는 용도로는 레코드보다 객체가 나을 수 있다.
    • 객체를 사용하면 어떻게 저장했는지를 숨긴 채 각각의 값을 메서드로 제공할 수 있다.
  • 레코드 구조는 두 가지로 구분할 수 있다.
    • 필드 이름을 노출하는 형태
    • (필드를 외부로부터 숨겨서) 내가 원하는 이름을 쓸 수 있는 형태
      • 주로 라이브러리에서 해시, 맵, 해시맵, 딕셔너리 등의 이름으로 제공
  • 이 중 해시맵은 다양한 프로그래밍 작업에 유용하지만 필드를 명확히 알려주지 않는다는 단점이 있다.
  • 이러한 불투명한 레코드는 명시적인 레코드로 리팩터링해도 되지만, 클래스를 사용하거나 JSON과 같은 포맷으로 직렬화하는 방식이 나을 수 있다.

절차

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

예시

Before

const organization = { name: "셜록", country: "GB" };
const newName = "처칠";

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

After

  • 레코드를 클래스로 변경
  • 레코드를 읽는 코드는 게터로 변경
  • 레코드를 갱신하는 코드는 세터로 변경
  • 새 클래스의 인스턴스를 반환하는 함수 생성
class Organization {
  constructor(data) {
    this._data = data;
    this._country = country;
  }
  
  get name()                { return this._data.name; }
  get country()             { return this._country; }
  
  set name(aString)         { this._data.name = aString; }
  set country(aCountryCode) { this._country = aCountryCode; }
}

const organization = new Organization({ name: "셜록", country: "GB" });

function getOrganization() {return organization;}

✏️ 컬렉션 캡슐화하기

배경

  • 컬렉션 변수로의 접근을 캡슐화하면서 게터가 컬렉션 자체를 반환하도록 한다면, 그 컬렉션을 감싼 클래스가 눈치채지 못하는 상태에서 컬렉션의 원소들이 바뀌어버릴 수 있다.
  • 내부 컬렉션을 직접 수정하지 못하게 막는 방법
    • 절대 컬렉션 값을 반환하지 않게 하기
      • 컬렉션에 접근하려면 컬렉션이 소속된 클래스의 적절한 메서드를 반드시 거치게 하는 것이다.
      • aCustomer.order.size() => aCustomer.numberOfOrder()
      • 저자는 이 방식에 동의하지 않는다고 한다.
    • 컬렉션 읽기전용으로 제공하기
    • 컬렉션 게터를 제공하되 내부 컬렉션의 복제본 반환하기
      • 복제본을 수정해도 캡슐화된 원본 컬렉션에는 아무런 영향을 주지 않는다.

절차

  1. 아직 컬렉션을 캡슐화하지 않았다면 변수 캡슐화하기부터 한다.
  2. 컬렉션에 원소를 추가/제거하는 함수를 추가한다.
  3. 컬렉션을 참조하는 부분을 모두 찾는다. 그리고 앞에서 추가한 추가/제거 함수를 호출하도록 수정한다.
  4. 컬렉션 게터를 수정해 원본 내용을 수정할 수 없는 읽기전용 프록시나 복제본을 반환하게 한다.

예시

Before

// 코드 내용 : 수업 목록을 필드로 지니고 있는 Person 클래스, 코스 정보를 지니고 있는 Course 클래스

class Person {
  constructor(name) {
    this._name = name;
    this._courses = [];
  }

  get name()           { return this._name; }
  get courses()        { return this._courses; }
  set courses(courses) { this._courses = courses; }
}

class Course {
  constructor(name, isAdvanced) {
    this._name = name;
    this._isAdvanced = isAdvanced;
  }

  get name()       { return this._name; }
  get isAdvanced() { return this._isAdvanced; }
}

// 클라이언트
advancedCoursesLength = aPerson.courses.filter((course) => course.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));
}

After

  • 수업을 하나씩 추가하고 제거하는 메서드를 Person 클래스에 추가
  • 직접 접근하는 대신 추가/제거 메서드를 활용해 데이터를 제어하도록 수정
class Person {
  constructor(name) {
    this._name = name;
    this._courses = [];
  }

  get name()        { return this._name; }

  // 📍 복제본 제공
  get courses()     { return this._courses.slice(); }
  set courses(list) { this._courses = list.slice(); }

  addCourse(course) { this._courses.push(course); }
  
  removeCourse(course, runIfAbsent){
    const index = this._courses.indexOf(course);
    if (index === -1) {
      runIfAbsent();
      return;
    }
    
    this._courses.splice(index, 1);
  }
}


// 클라이언트
const refactoringCourse = new Course('리팩토링', true);

for(const name of readBasicCourseNames(filename)) {
  aPerson.addCourse(new Course(name, false));
}

aPerson.addCourse(course);
aPerson.removeCourse(course, () => console.log('제거 완료!'));

Deep dive

  • 포인트는 데이터에 직접 접근하지 못하게 하는 것 + 원본은 유지하고 복제본으로 제어하는 것!

✏️ 기본형을 객체로 바꾸기

배경

  • 처음에 간단한 데이터 항목으로 표현하던 코드들에 여러 동작들이 추가되기 시작하면 금새 중복 코드가 늘어나기 마련이다.
  • 단순한 출력 이상의 기능이 필요해지는 순간 그 데이터를 표현하는 전용 클래스를 정의하는 것이 좋다.
  • 프로그램이 커질수록 클래스는 그 빛을 발하게 된다.

절차

  1. 아직 변수를 캡슐화하지 않았다면 캡슐화한다.
  2. 단순한 값 클래스를 만든다.
  3. 값 클래스의 인스턴스를 새로 만들어 필드에 저장하도록 세터를 수정한다.
  4. 새로 만든 클래스의 게터를 호출한 결과를 반환하도록 게터를 수정한다.
  5. 함수 이름을 바꾸면 원본 접근자의 동작을 더 잘 드러낼 수 있는지 검토한다.

예시

Before

// 코드 내용: 우선순위 클래스와 우선순위를 필터링해 최우선순위 개수를 반환하는 클라이언트 함수

class Order {
  constructor(data) {
    this.priority = data.priority;
  }
  
  ...
}

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

After

  • Order는 우선순위 클래스에서 값을 가져오는 값 클래스로 변경
  • Priority에는 값 검증, 비교 및 연산 로직을 담도록 작성
// 확장 버전

class Order {
  ...

  get priority()        { return this._priority; }
  get priorityString()  { return this._priority.toString(); }
  set priority(aString) { this._priority = new Priority(aString); }
}

class Priority {
  constructor(value) {
    // 📍 검증 로직
    if (value instanseof Priority) return value;
    if (Priority.legalValues().includes(value)) {
      this._value = value;
    } else {
      throw new Error(`${value}는 유효하지 않은 우선순위입니다.`)
    }
  }
  
  get _index()  { return Priority.legalValues().findIndex(s => s === this._value); }
  
  // 📍 비교 로직
  static legalValues() { return ['low', 'normal', 'high', 'rush']; }
  toString()           { return this._value; }
  equals(other)        { return this._index === other._index; }
  higherThan(other)    { return this._index > other._index; }
  lowerThan(other)     { return this._index < other._index; }  
}

// 📍 활용 예시
const highPriorityCount = orders.filter((order) => 
    order.priority.higherThan(new Priority('normal'))).length;

Deep dive

  • 위와 같이 확장성을 고려해 여러 비교, 검증 로직을 추가하는 것도 좋지만 간단할 경우 아래와 같이 작성해도 좋지 않을까?
class Order {
  constructor(data) {
    this.priority = data.priority;
  }

  isHighPriority(){
    return 'high' === this.priority || 'rush' === this.priority
  }
}

const highPriorityCount = orders.filter((order) => order.isHighPriority()).length;

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

배경

  • 함수 안에서 어떤 코드의 결괏값을 뒤에서 다시 참조할 목적으로 임시 변수를 쓰기도 한다.
    • 이를 통해 코드의 반복을 줄이고 값의 의미를 설명할 수 있어 유용하다.
    • 그런데 한 걸음 더 나아가 아예 함수로 만드는 게 나을 수도 있다.
  • 긴 함수의 한 부분을 별도 함수로 추출하고자 할 때 먼저 변수들을 각각의 함수로 만들면 일이 수월해진다.
    • 의존 관계나 부수효과 파악 용이
    • 코드 중복 제거
  • 임시 변수를 질의 함수로 바꾼다고 다 좋아지는 건 아니다.
    • 특히 ‘옛날 주소’처럼 스냅샷 용도로 쓰이는 변수에는 이 리팩토링을 적용하면 안 된다.

절차

  1. 변수가 사용되기 전에 값이 확실히 결정되는지 확인한다.
  2. 읽기 전용으로 만들 수 있는 변수는 읽기 전용으로 만든다.
  3. 변수 대입문을 함수로 추출한다.
  4. 변수 인라인하기로 임시 변수를 제거한다.

예시

Before

// 코드 내용: 할인율에 따라 가격을 연산하는 Order 클래스

class Order {
  constructor(quantity, item) {
    this._quantity = quantity;
    this._item = item;
  }

  get price() {
    const basePrice = this._quantity * this._item.price;
    const discountFactor = 0.98;
    
    if (basePrice > 1000) discountFactor -= 0.03;
    return basePrice * discountFactor;
  }
}

After

  • 임시 변수인 basePrice와 discountFactor을 메서드로 변환
  • 게터 함수로 변환
  • 기존 변수를 제거하고 추출한 함수 대입
class Order {
  constructor(quantity, item) {
    this._quantity = quantity;
    this._item = item;
  }
  
  get basePrice() {
    return this._quantity * this._item.price;
  }

  get discountFactor(){
    return this.basePrice > 1000 ? 0.95 : 0.98
  }

  get price() {
    return this.basePrice * this.discountFactor;
  }
}

✏️ 클래스 추출하기

배경

  • 메서드와 데이터가 너무 많은 클래스는 이해하기 쉽지 않으니 잘 살펴보고 적절히 분리하는 것이 좋다.
    • 특히 일부 데이터와 메서드를 따로 묶을 수 있다면 어서 분리하라는 신호다.
    • 함께 변경되는 일이 많거나 서로 의존하는 데이터들도 분리한다.
  • 제거해도 다른 필드나 메서드들이 논리적으로 문제가 없다면 분리할 수 있다는 뜻이다.

절차

  1. 클래스의 역할을 분리할 방법을 정하고 분리될 역할을 담당할 클래스를 새로 만든다.
  2. 원래 클래스의 생성자에서 새로운 클래스의 인스턴스를 생성해 필드에 저장해둔다.
  3. 분리될 역할에 필요한 필드들을 새 클래스로 옮긴다.
  4. 메서드들도 새 클래스로 옮긴다.
  5. 양쪽 클래스의 인터페이스를 살펴보며 불필요한 메서드를 제거하고 이름을 맞게 변경한다.

예시

Before

// 클래스 내용: 이름, 전화번호, 직장 주소 코드, 직장 전화번호가 담긴 사람 클래스
// 목표 : 전화번호 관련 동작을 별도 클래스로 추출하기

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

After

  • 전화번호 관련 메서드를 TelephoneNumber 클래스로 이동
  • 이동한 메서드는 순수한 번호를 뜻하므로 office를 제거한 네이밍으로 변경 (함수 선언 바꾸기)
  • 전화번호를 읽기 좋은 포맷으로 출력하는 메서드 추가
class Person {
  constructor {
    this._telephoneNumber = new TelephoneNumber();
  }
  
  get officeAreaCode()    {return this._telephoneNumber.areaCode;}
  set officeAreaCode(arg) {return this._telephoneNumber.areaCode = arg;}
  
  get officeNumber()      {return this._telephoneNumber.number;}
  set officeNumber(arg)   {return this._telephoneNumber.number = arg;}
  
  get telephoneNumber()   {return this._telephoneNumber.toString;}
}

class TelephoneNumber {
  ...
  
  get areaCode()    {return this._areaCode;}
  set areaCode(arg) {this._areaCode = arg;}

  get number()      {return this._number;}
  set number(arg)   {this._number = arg;}
  
  get toString()    {return `${this._areaCode} ${this._number}`}
}

✏️ 클래스 인라인하기

배경

  • 클래스 인라인하기는 클래스 추출하기의 반대 리팩터링 기법이다.
  • 역할을 옮긴 후 특정 클래스에 남은 역할이 거의 없을 때
    • 가장 많이 사용하는 클래스로 흡수시킨다.
  • 또는 두 클래스의 기능을 지금과 다르게 배분하고 싶을 때도 인라인하면 좋다.

절차

  1. 소스 클래스의 각 public 메서드에 대응하는 메서드들을 타깃 클래스에 생성한다.
  2. 소스 클래스의 메서드를 사용하는 코드를 모두 타깃 클래스의 위임 메서드를 사용하도록 바꾼다.
  3. 소스 클래스의 메서드와 필드를 모두 타깃 클래스로 옮긴다.
  4. 소스 클래스를 제거한다.
  5. 조의를 표한다 🙏🏻

예시

Before

// 코드 내용: 배송 추적 정보를 표현하는 TrackingInformation 클래스와
//          이를 일부처럼 사용하는 Shipment 클래스

// 목표: Shipment 클래스로 인라인하기

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

class Shipment {
  ...
  
  get trackingInfo()        { return this._trackingInformation.display; }
  get trackingInformation() { return this._trackingInformation; }

  set trackingInformation(trackingInformation) {
    this._trackingInformation = trackingInformation;
  }
}

After

  • 외부에서 직접 호출하는 TrackingInformation 메서드들을 Shipment로 옮긴다.
  • 이 과정을 반복한 후 TrackingInformation 클래스를 제거한다.
class 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; }
}

✏️ 위임 숨기기

배경

  • 모듈화 설계를 제대로 하는 핵심은 캡슐화다.
  • 캡슐화는 모듈들이 시스템의 다른 부분에 대해 알아야 할 내용을 줄여준다.
  • 캡슐화는 필드를 숨기는 것 이상의 역할을 한다.
    • 서버 객체의 필드를 가리키는 객체(위임 객체)의 메서드를 호출하려면 클라이언트는 이 위임 객체를 알아야 한다.
    • 이 때 의존성이 생기게 되는데, 이러한 의존성을 없애려면 서버 자체에 위임 메서드를 만들어 위임 객체의 존재를 숨기면 된다.
    • 그러면 위임 객체가 수정되더라도 서버 코드만 고치면 되며, 클라이언트는 아무런 영향을 받지 않는다.
      • 일종의 수정 범위(depth) 줄이기?

절차

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

예시

Before

// 목표 : 클라이언트에서 어떤 사람이 속한 부서의 관리자 얻기

class Person {
  constructor(name) {
    this._name = name;
  }

  get name()          { return this._name; }
  get department()    { return this._department; }

  set department(arg) { this._department = arg; }
}

class Department {
  ...

  get chargeCode()    { return this._chargeCode; }
  get manager()       { return this._manager; }
  
  set chargeCode(arg) { this._chargeCode = arg; }
  set manager(arg)    { this._manager = arg; }
}

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

// 동작 방식
get departmet() => class Department => get manager()

After

  • Person 클래스에 위임 메서드인 manager()를 생성해 값 제공
class Person {
  ...

  get manager() { return this._department.manager; }
}

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

✏️ 중개자 제거하기

배경

  • 단순히 전달만 하는 위임 메서드를 추가해 사용하게 되면 서버 클래스는 중개자(middle man) 역할로 전락할 수 있다.
  • 이 때는 차라리 클라이언트가 위임 객체를 직접 호출하는 게 나을 수 있다.

데메테르 법칙 (Law of Demeter), LOD

- 또 다른 이름으로는 최소 지식 원칙(Principle of least knowledge)라고 한다.

- 내부 정보를 가능한 한 숨기고 밀접한 모듈과만 상호작용하여 결합도를 낮추자는 원칙.

- 모듈은 자신이 조작하는 객체의 속사정을 몰라야 한다는 것을 의미

- 남용 시 이 과정에서 위임 혹은 래퍼(wrapper) 메서드가 너무 늘어나는 등의 부작용이 있을 수 있다.

 
  • A에서 C의 정보를 얻어 오기 위한 방법은 C에서 B로, B에서 A로 값을 캡슐화하여 값을 가져오도록 한다.

절차

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

예시

Before

// 코드 내용 : 자신이 속한 부서 객체를 통해 관리자를 찾는 사람 클래스를 통해 관리자 호출

manager = aPerson.manager;

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

class Department {
  ...
  
  get manager() { return this._manager; }
}

After

// Person 클래스에서 직접 department 위임 객체를 얻어 manager 호출 (중개자 제거)

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

manager = aPerson.department.manager;

✏️ 알고리즘 교체하기

배경

  • 어떤 목적을 달성하는 방법은 여러 가지가 있게 마련이다.
    • 알고리즘도 마찬가지다.
  • 리팩터링하면 복잡한 대상을 단순한 단위로 나눌 수 있지만,
    • 때로는 알고리즘 전체를 걷어내고 훨씬 간결한 알고리즘으로 바꿔야 할 때가 있다.
  • 이 작업에 착수하려면 반드시 메서드를 가능한 한 잘게 나눴는지 확인해야 한다.
    • 거대한 알고리즘을 교체하기란 상당히 어려우니 먼저 간소화하는 작업부터 해야 교체가 쉬워진다.

절차

  1. 교체할 코드를 함수 하나에 모은다.
  2. 이 함수만을 이용해 동작이 정상 작동하는지 테스트한 후 대체할 알고리즘을 준비한다.
  3. 기존 알고리즘과 새 알고리즘의 결과를 비교한 후 같다면 교체한다.

예시

Before

// 함수 내용: people을 순회하면서 해당 이름에 해당하는 string 반환

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

After

// 추출할 값을 배열로 생성 후 컬렉션 파이프라인(find, includes)으로 대체

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

'Programming > 13. Book' 카테고리의 다른 글

모던 리액트 Deep Dive - 12장  (0) 2024.03.03
리팩터링 - 12장  (0) 2024.03.02
리팩터링 - 11장  (0) 2024.03.02
리팩터링 - 10장  (0) 2024.03.02
리팩터링 - 8, 9장  (0) 2024.03.02

댓글