시작하며

코드를 변경할때 전체적인 구조나 로직을 뜯어 고쳐야할 필요성을 느낄때가 있다. 기획의 방향이 크게 바뀌었거나 기존 코드가 악취를 풍겨서 생각보다 전체적으로 고쳐야 할때 선택의 기로에 서게 된다. 기한이 촉박해서 혹은 내가 작성하지 않은 코드여서 제대로 이해하지 못하고 나쁜 방향으로 리팩터링할까봐 고민되지만 그럼에도 항상 리팩터링하려고 노력한다. 지금하는 노력이 나중에 도움이 된다고 믿기 때문이고 다른 개발자들과 책에서도 그렇게 말하고 있기 때문이다.




캡슐화

레코드 캡슐화하기

//리팩토링 전
const organization = { name: 'Acme Gooseberries', country: 'GB' };
//리팩토링 후
class Organization {
  #name;
  #country;
  constructor (data) {
    this.#name = data.name;
    this.#country = data.country;
  }

  get name() {
    return this.#name;
  }

  set name(value) {
    this.#name = value;
  }
  
  get country() {
    return this.#country;
  }

  set country(value) {
    this.#country=value;
  }

  get rowData() {
    return { name: this.#name, country: this.#country };
  }
}

레코드는 연관된 여러 데이터를 직관적인 방식으로 묶을 수 있어서 각각을 따로 취급할 때보다 훨씬 의미 있는 단위로 전달할 수 있게 해준다. 객체를 사용하면 어떻게 저장했는지를 숨긴 채 세 가지 값을 각각의 메서드로 제공할 수 있다. 사용자는 무엇이 저장된 값이고 무엇이 계산된 값인지 알 필요가 없고 캡슐화하면 이름을 바꿀때도 좋다.



컬렉션 캡슐화하기

//리팩토링 전
export class Person {
  #name;
  #courses;
  constructor(name) {
    this.#name = name;
    this.#courses = [];
  }

  get name() {
    return this.#name;
  }

  get courses() {
    return this.#courses;
  }

  set courses(courses) {
    this.#courses = courses;
//리팩토링 후
export class Person {
  #name;
  #courses;
  constructor(name) {
    this.#name = name;
    this.#courses = [];
  }

  get name() {
    return this.#name;
  }

  get courses() {
    return [...this.#courses];
  }

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

export class Course {
  #name;
  #isAdvanced;
  constructor(name, isAdvanced) {
    this.#name = name;
    this.#isAdvanced = isAdvanced;
  }

  get name() {
    return this.#name;
  }

  get isAdvanced() {
    return this.#isAdvanced;
  }
}

가변 데이터는 캡슐화하는 편이 좋다. 데이터 구조가 언제 어떻게 수정되는지 파악하기 쉬워서 필요한 시점에 데이터 구조를 변경하기도 쉬워지기 때문이다.



기본형을 객체로 바꾸기

//리팩토링 전
export class Order {
  constructor(data) {
    this.priority = data.priority;
  }
}

const orders = [
  new Order({ priority: 'normal' }),
  new Order({ priority: 'high' }),
  new Order({ priority: 'rush' }),
];

const highPriorityCount = orders.filter(
  (o) => 'high' === o.priority || 'rush' === o.priority
).length;
//리팩토링 후
export class Order {
  constructor(data) {
    this.priority = new Priority(data.priority);
  }

  isHighPriority() {
    return this.priority.higerThan( new Priority('normal'));
  }
}

class Priority {
  #value;
  constructor(value) {
    if (Priority.legalValues().includes(value)) {
      this.#value = value;
    } else {
      throw new Error(`${value} is invalid for Priority`);
    }
  }

  get index() {
    return Priority.legalValues().indexOf(this.#value);
  }

  equals(other) {
    return this.index === other.index;
  }

  higerThan(other) {
    return this.index > other.index;
  }

  lowerThan(other) {
    return this.index < other.index;
  }

  static legalValues() {
    return ['low', 'normal', 'high', 'rush'];
  }
}

const orders = [
  new Order({ priority: 'normal' }),
  new Order({ priority: 'high' }),
  new Order({ priority: 'rush' }),
];

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

개발 초기에는 단순한 정보를 숫자나 문자열 같은 간단한 데이터 항목으로 표현할 때가 많지만, 나중에 포매딩이나 지역 코드 추출 같은 특별한 동작이 필요해질때 금세 중복 코드가 늘어나서 사용할 때마다 드는 노력도 늘어나게 된다. 단순한 출력 이상의 기능이 필요해지는 순간 그 데이터를 표현하는 전용 클래스를 정의하는 편이 좋다.



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

//리팩토링 전
class Order {
  #quantity;
  #item;
  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;
  }
}
//리팩토링 후
class Order {
  #quantity;
  #item;
  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;
  }
}

임시 변수를 사용하면 값을 계산하는 코드가 반복되는 걸 줄이고 값의 의미를 설명할 수도 있어서 유용하지만 아예 함수로 만들어 사용하는 편이 나을 때가 많다. 변수 대신 함수로 만들어두면 비슷한 계산을 수행하는 다른 함수에서도 사용할 수 있어 코드의 중복이 줄어든다.



클래스 추출하기

//리팩토링 전
class Person {
  #name;
  #officeAreaCode;
  #officeNumber;
  constructor(name, areaCode, number) {
    this.#name = name;
    this.#officeAreaCode = areaCode;
    this.#officeNumber = number;
  }

  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 TelephoneNeumber {
  #number;
  #areaCode;
  constructor(areaCode, number) {
    this.#number = number;
    this.#areaCode = areaCode;
  }

  get number() {
    return this.#number;
  }

  set number(number) {
    this.#number = number;
  }

  get areaCode() {
    return this.#areaCode;
  }

  set areaCode(areaCode) {
    this.#areaCode = areaCode;
  }

  get toString() {
    return `${this.#areaCode} ${this.#number}`;
  }
}

class Person {
  #name;
  #telephoneNumber;
  constructor(name, areaCode, number) {
    this.#name = name;
    this.#telephoneNumber = new TelephoneNeumber(areaCode, number);
  }

  get name() {
    return this.#name;
  }

  set name(arg) {
    this.#name = arg;
  }

  get telephoneNumber() {
    return this.#telephoneNumber.toString;
  }

  get officeNumber() {
    return this.#telephoneNumber.number;
  }

  get officeAreaCode() {
    return this.#telephoneNumber.areaCode;
  }
}

클래스는 반드시 명확하게 추상화하고 소수의 주어진 역할만 처리해야 한다. 메서드와 데이터가 너무 많은 클래스는 이해하기가 쉽지 않으니 더 살펴보고 적절히 분리하는 것이 좋다. 일부 데이터와 메서드를 따로 묶을 수 있다면 어서 분리하라는 신호이다. 작은 일부 기능만을 위해 서브클래스를 만들거나, 확장해야 할 기능이 무엇이냐에 따라 서브클래스를 만드는 방식도 달라진다면 클래스를 나눠야 한다는 신호이다.



클래스 인라인하기

//리팩토링 전
export class TrackingInformation {
  #shippingCompany;
  #trackingNumber;
  constructor(trackingNumber, shippingCompany) {
    this.#trackingNumber = trackingNumber;
    this.#shippingCompany = shippingCompany;
  }

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

export class Shipment {
  #trackingInformation;
  constructor(trackingInformation) {
    this.#trackingInformation = trackingInformation;
  }

  get trackingInfo() {
    return this.#trackingInformation.display;
  }

  get trackingInformation() {
    return this.#trackingInformation;
  }

  set trackingInformation(trackingInformation) {
    this.#trackingInformation = trackingInformation;
  }
}
//리팩토링 후
export class Shipment {
  #shippingCompany;
  #trackingNumber;
  constructor(trackingNumber, shippingCompany) {
    this.#trackingNumber = trackingNumber;
    this.#shippingCompany = shippingCompany;
  }

  get shippingCompany() {
    return this.#shippingCompany;
  }

  set shippingCompany(arg) {
    this.#shippingCompany = arg;
  }

  get trackingNumber() {
    return this.#trackingNumber;
  }

  set trackingNumber(arg) {
    this.#trackingNumber = arg;
  }

  get trackingInfo() {
    return `${this.shippingCompany}: ${this.trackingNumber}`;
  }
}

더 이상 제 역할을 못해서 그대로 두면 안 되는 클래스는 인라인해버리는 것이 좋다. 역할을 옮기는 리팩터링을 하고 특정 클래스에 남은 역할이 거의 없을 때 이런 현상이 자주 발생하는데 이럴땐 해당 클래스를 가장 많이 사용하는 클래스로 흡수시키면 된다.



위임 숨기기

//리팩토링 전
class Person {
  #name;
  #department;
  constructor(name, department) {
    this.#name = name;
    this.#department = department;
  }

  get name() {
    return this.#name;
  }

  get department() {
    return this.#department;
  }

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

export class Department {
  #manager;
  #chargeCode;
  constructor(manager, chargeCode) {
    this.#manager = manager;
    this.#chargeCode = chargeCode;
  }

  get chargeCode() {
    return this.#chargeCode;
  }

  set chargeCode(arg) {
    this.#chargeCode = arg;
  }

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

  set manager(arg) {
    this.#manager = arg;
  }
}
//리팩토링 후
class Person {
  #name;
  #department;
  constructor(name, department) {
    this.#name = name;
    this.#department = department;
  }

  get name() {
    return this.#name;
  }

  get department() {
    return this.#department;
  }

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

  get manager() {
    return this.#department.manager;
  }

  get chargeCode() {
    return this.#department.chargeCode;
  }
}

export class Department {
  #manager;
  #chargeCode;
  constructor(manager, chargeCode) {
    this.#manager = manager;
    this.#chargeCode = chargeCode;
  }

  get chargeCode() {
    return this.#chargeCode;
  }

  set chargeCode(arg) {
    this.#chargeCode = arg;
  }

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

  set manager(arg) {
    this.#manager = arg;
  }
}

모듈화 설계를 제대로 하는 핵심은 캡슐화이다. 캡슐화는 모듈들이 시스템의 다른 부분에 대해 알아야 할 내용을 줄여주고 무언가를 변경해야 할 때 함께 고려해야 할 모듈 수가 적어져서 코드를 변경하기 훨씬 쉬워진다.



중개자 제거하기

위임 숨기기와 반대로 단순히 전달만 하는 위임 메서드들을 가지는 중개자 역할만 하는 클래스라면 차라리 클라이언트가 위임 객체를 호출하는 것이 나을 수 있다. 어느 정도 숨겨야 적절한 지를 판단하기란 쉽지 않지만 시스템에 따라 필요에 따라 언제든 리팩터링 할 수 있다.



알고리즘 교체하기

//리팩토링 전
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 '';
}
//리팩토링 후
function foundPerson(people) {
  const candidates = ['Don', 'John', 'Kent'];
  return people.find(person => candidates.includes(person)) || '';
}

같은 동작을 하는 함수를 구현하더라도 여러가지 방법이 있다. 문제를 확실히 이해하고 훨씬 쉽게 해결하는 방법을 발견했을 때 알고리즘 전체를 걷어내고 훨씬 간결한 알고리즘으로 교체하는 것 좋다.




마치며

리팩터링 공부를 하면서 클래스에 대해서 다시 공부하게 되는 계기가 되었다. 부트캠프를 수강할 때 클래스에 대해서 깊게 공부하지 못했고 현업에서는 클래스를 사용하는 경우가 없어서 기본적인 문법외에는 잘 알지 못 했었다. 캡슐화와 같은 클래스의 특성을 활용해서 어떻게 클래스를 설계하고 활용할 수 있는지에 대해 파악할 수 있었다.

profile
개발자로 성장하기

0개의 댓글