본문 바로가기
개발

[리팩터링 2판] JavaScript 리팩터링 도서학습 #4

by 개발과 운동, 그리고 책장 2025. 4. 24.

해당 내용은 위의 책의 내용을 제가 이해한대로 정리해둔 내용입니다.

이번 포스팅에선 Chapter07, Chapter08 의 내용에 대해 다룹니다. 

 

  • Chapter07 캡슐화 
  • Chapter08 기능 이동

7,8장에서 소개하는 주요 리팩터링 기법들을 요약하고 예제와 함께 정리했습니다.

Chapter07 캡슐화 

1.레코드 캡슐화하기
2.컬렉션 캡슐화하기
3.기본형을 객체로 바꾸기
4.임시 변수를 질의 함수로 바꾸기
5.클래스 추출하기
6.클래스 인라인하기
7.위임 숨김하기
8.중개자 제거하기
9.알고리즘 교체하기

 


캡슐화의 효과 


변경 영향도 최소화: 내부 구현 변경 시 클라이언트 코드 영향 감소
코드 품질 향상: 책임 분리로 응집도 높이고 결합도 낮춤
유지보수 용이성: 명확한 책임 분배로 코드 변경이 용이해짐
테스트 용이성: 작고 독립적인 단위로 테스트 작성 쉬워짐


캡슐화 기법 

1.레코드 캡슐화하기

*  데이터를 객체로 감싸고 접근자를 통해서만 조작 가능하게 함
-  데이터 구조 변경 유연성 확보, 유효성 검증 추가 가능성, 관련 동작 함께 배치

 

//Before
const organization = { name: "Acme Gooseberries", country: "GB" };

// 직접 접근
console.log(organization.name);
organization.name = "Acme Berry Company";
//After
class Organization {
  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; }
}

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

// 접근자를 통한 접근
console.log(organization.name);
organization.name = "Acme Berry Company";


2.컬렉션 캡슐화하기

* 컬렉션 직접 반환 대신 복사본이나 추가/제거 메서드 제공
- 컬렉션 무결성 보호, 변경 이벤트 발생 가능, 클래스가 컬렉션 관리 책임 가짐

//Before
class Person {
  constructor(name) {
    this._name = name;
    this._courses = [];
  }
  
  get courses() { return this._courses; }
  set courses(courses) { this._courses = courses; }
}

// 외부에서 컬렉션 직접 조작
const person = new Person("John");
person.courses.push(new Course("Math", true));
//After 
class Person {
  constructor(name) {
    this._name = name;
    this._courses = [];
  }
  
  get courses() { return [...this._courses]; } // 복사본 반환
  
  addCourse(course) { this._courses.push(course); }
  removeCourse(course) {
    const index = this._courses.indexOf(course);
    if (index >= 0) this._courses.splice(index, 1);
  }
}

// 메서드를 통한 안전한 조작
const person = new Person("John");
person.addCourse(new Course("Math", true));


3.기본형을 객체로 바꾸기

* 단순 문자열, 숫자 등을 전용 클래스로 변환
- 도메인 개념 명확화, 관련 동작 응집, 유효성 검증 중앙화, 비교 로직 개선

//Before
class Order {
  constructor(data) {
    this.priority = data.priority; // priority는 문자열: "high", "normal", "low"
  }
}

// 우선순위 확인 로직이 여러 곳에 중복됨
if (order.priority === "high" || order.priority === "rush") {
  // 긴급 처리
}
//After
class Priority {
  constructor(value) {
    this._value = value;
  }
  
  toString() { return this._value; }
  
  get _index() {
    return ["low", "normal", "high", "rush"].indexOf(this._value);
  }
  
  higherThan(other) { return this._index > other._index; }
  equals(other) { return this._index === other._index; }
}

class Order {
  constructor(data) {
    this.priority = new Priority(data.priority);
  }
  
  get priority() { return this._priority; }
  set priority(value) {
    this._priority = (value instanceof Priority) ? value : new Priority(value);
  }
}

// 의미있는 비교 가능
if (order.priority.higherThan(new Priority("normal"))) {
  // 우선 처리
}


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

* 임시 변수를 함수로 추출하여 재사용성 높임
- 코드 중복 제거, 가독성 향상, 디버깅 용이성, 최적화 기회 제공

//Before
class Order {
  constructor(quantity, price) {
    this._quantity = quantity;
    this._price = price;
  }
  
  getTotal() {
    const basePrice = this._quantity * this._price;
    const discount = Math.max(0, this._quantity - 500) * this._price * 0.05;
    const shipping = Math.min(basePrice * 0.1, 100);
    return basePrice - discount + shipping;
  }
}
//After
class Order {
  constructor(quantity, price) {
    this._quantity = quantity;
    this._price = price;
  }
  
  get basePrice() { return this._quantity * this._price; }
  get discount() { return Math.max(0, this._quantity - 500) * this._price * 0.05; }
  get shipping() { return Math.min(this.basePrice * 0.1, 100); }
  
  getTotal() {
    return this.basePrice - this.discount + this.shipping;
  }
}


5.클래스 추출하기

* 함께 변경되는 데이터와 메서드를 별도 클래스로 분리
- 단일 책임 원칙 적용, 복잡성 감소, 재사용성 향상, 테스트 용이성

//Before
class Person {
    constructor(name, areaCode, number) {
      this._name = name;
      this._areaCode = areaCode;
      this._number = number;
    }
    
    get telephoneNumber() {
      return `(${this._areaCode}) ${this._number}`;
    }
 
    // 기타 메서드들...
  }
  //After
  class Person {
    constructor(name, areaCode, number) {
      this._name = name;
      this._telephone = new TelephoneNumber(areaCode, number);
    }
    
    get telephoneNumber() { return this._telephone.toString(); }
  }
  
  class TelephoneNumber {
    constructor(areaCode, number) {
      this._areaCode = areaCode;
      this._number = number;
    }
    
    toString() { return `(${this._areaCode}) ${this._number}`; }
  }


6.클래스 인라인하기

* 불필요한 클래스를 상위 클래스로 통합
- 과도한 분리 해소, 불필요한 위임 제거, 코드 이해도 증가

// Before
class Person {
    constructor(name) {
      this._name = name;
      this._telephoneNumber = new TelephoneNumber();
    }
    
    get name() { return this._name; }
    get telephoneNumber() { return this._telephoneNumber.telephoneNumber; }
    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; }
  }
  
  class TelephoneNumber {
    constructor() {
      this._areaCode = null;
      this._number = null;
    }
    
    get telephoneNumber() { return `(${this._areaCode}) ${this._number}`; }
    get areaCode() { return this._areaCode; }
    set areaCode(arg) { this._areaCode = arg; }
    get number() { return this._number; }
    set number(arg) { this._number = arg; }
  }
  // After
  class Person {
    constructor(name) {
      this._name = name;
      this._areaCode = null;
      this._number = null;
    }
    
    get name() { return this._name; }
    get telephoneNumber() { return `(${this._areaCode}) ${this._number}`; }
    get officeAreaCode() { return this._areaCode; }
    set officeAreaCode(arg) { this._areaCode = arg; }
    get officeNumber() { return this._number; }
    set officeNumber(arg) { this._number = arg; }
  }


7.위임 숨김하기

* 클라이언트가 위임 객체에 직접 접근하지 않도록 중개 메서드 제공
- 의존성 감소, 내부 구현 변경에 외부 영향 최소화, API 간소화

// Before
class Person {
    constructor(name) {
      this._name = name;
      this._department = new Department();
    }
    
    get name() { return this._name; }
    get department() { return this._department; }
    set department(arg) { this._department = arg; }
  }
  
  class Department {
    get manager() { return this._manager; }
    set manager(arg) { this._manager = arg; }
  }
  
  const manager = person.department.manager;
  // After
  class Person {
    constructor(name) {
      this._name = name;
      this._department = new Department();
    }
    
    get name() { return this._name; }
    get manager() { return this._department.manager; }
    set department(arg) { this._department = arg; }
  }
  
  class Department {
    get manager() { return this._manager; }
    set manager(arg) { this._manager = arg; }
  }
  
  const manager = person.manager;  // department에 직접 접근하지 않음


8.중개자 제거하기

* 단순 위임만 수행하는 중개 메서드 제거, 직접 접근 허용
- 불필요한 위임 제거로 코드 간소화, 과도한 캡슐화 방지

// Before
class Person {
  constructor(name) {
    this._name = name;
    this._department = new Department();
  }
  
  get name() { return this._name; }
  get manager() { return this._department.manager; }
  get chargeCode() { return this._department.chargeCode; }
  get department() { return this._department; }
  set department(arg) { this._department = arg; }
}

const manager = person.manager;
const chargeCode = person.chargeCode;
// After
class Person {
  constructor(name) {
    this._name = name;
    this._department = new Department();
  }
  
  get name() { return this._name; }
  get department() { return this._department; }
  set department(arg) { this._department = arg; }
}

const manager = person.department.manager;
const chargeCode = person.department.chargeCode;


9.알고리즘 교체하기

* 복잡한 알고리즘을 더 간단하고 명확한 알고리즘으로 교체
- 가독성 향상, 유지보수성 개선, 성능 최적화, 더 나은 라이브러리 활용

// Before
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
function foundPerson(people) {
  const candidates = ["Don", "John", "Kent"];
  const found = people.find(p => candidates.includes(p));
  return found || "";
}

Chapter08 기능 이동

1. 함수 옮기기
2. 필드 옮기기
3. 문장을 함수로 옮기기
4. 문장을 호출한 곳으로 옮기기
5. 인라인 코드를 함수 호출로 바꾸기
6. 문장 슬라이드하기
7. 반복문 쪼개기
8. 반복문을 파이프라인으로 바꾸기
9. 죽은 코드 제거하기

 

기능 이동 효과

불필요한 코드를 제거하여 코드베이스를 간소화하고 유지보수성 및 가독성을 향상시킨다.

 

 

  • 응집도 향상: 관련 코드를 함께 배치하여 모듈성 개선
  • 책임 명확화: 코드의 기능과 책임 경계를 명확히 설정
  • 가독성 증가: 코드의 의도를 더 명확하게 표현
  • 유지보수성 개선: 코드 변경 시 영향 범위 최소화
  • 테스트 용이성: 기능별로 분리된 코드는 단위 테스트하기 쉬움

 

기능 이동 기법

1. 함수 옮기기

* 함수를 관련 데이터가 있는 모듈이나 클래스로 이동
- 함수와 데이터 응집도 향상, 컨텍스트 접근성 개선, 코드 재사용성 증가

// Before
class Account {
  get overdraftCharge() {
    if (this._type.isPremium) {
      const baseCharge = 10;
      if (this._daysOverdrawn <= 7) {
        return baseCharge;
      } else {
        return baseCharge + (this._daysOverdrawn - 7) * 0.85;
      }
    } else {
      return this._daysOverdrawn * 1.75;
    }
  }
}
// After
class Account {
  get overdraftCharge() {
    return this._type.overdraftCharge(this._daysOverdrawn);
  }
}

class AccountType {
  overdraftCharge(daysOverdrawn) {
    if (this.isPremium) {
      const baseCharge = 10;
      if (daysOverdrawn <= 7) {
        return baseCharge;
      } else {
        return baseCharge + (daysOverdrawn - 7) * 0.85;
      }
    } else {
      return daysOverdrawn * 1.75;
    }
  }
}


2. 필드 옮기기

* 필드를 더 적합한 클래스로 이동
- 데이터와 사용 코드 간 거리 최소화, 객체 간 책임 경계 명확화, 결합도 감소

// Before
class Customer {
  constructor(name, discountRate) {
    this._name = name;
    this._discountRate = discountRate;
    this._contract = new CustomerContract(new Date());
  }
  
  get discountRate() { return this._discountRate; }
}

class CustomerContract {
  constructor(startDate) {
    this._startDate = startDate;
  }
}
// After
class Customer {
  constructor(name, discountRate) {
    this._name = name;
    this._contract = new CustomerContract(new Date(), discountRate);
  }
  
  get discountRate() { return this._contract.discountRate; }
}

class CustomerContract {
  constructor(startDate, discountRate) {
    this._startDate = startDate;
    this._discountRate = discountRate;
  }
  
  get discountRate() { return this._discountRate; }
  set discountRate(arg) { this._discountRate = arg; }
}


3. 문장을 함수로 옮기기

* 특정 함수와 관련된 문장을 해당 함수 내로 이동
- 동일 목적 코드 통합, 중복 제거, 함수의 완전성과 응집도 향상

// Before
function renderPerson(person) {
  const result = [];
  result.push(`<p>${person.name}</p>`);
  result.push(renderPhoto(person.photo));
  result.push(`<p>제목: ${person.photo.title}</p>`);
  return result.join("\n");
}

function renderPhoto(photo) {
  return `<img src="${photo.url}" />`;
}
function renderPerson(person) {
  const result = [];
  result.push(`<p>${person.name}</p>`);
  result.push(renderPhoto(person.photo));
  return result.join("\n");
}

function renderPhoto(photo) {
  return [
    `<img src="${photo.url}" />`,
    `<p>제목: ${photo.title}</p>`
  ].join("\n");
}


4. 문장을 호출한 곳으로 옮기기

* 여러 함수에서 다르게 동작하는 코드를 호출자로 이동
- 함수 역할 명확화, 맥락에 따른 동작 차별화, 함수 간 책임 경계 재조정

// Before
function renderPerson(person) {
  const result = [];
  result.push(`<p>${person.name}</p>`);
  result.push(photoData(person.photo));
  return result.join("\n");
}

function photoData(photo) {
  return [
    `<img src="${photo.url}" />`,
    `<p>제목: ${photo.title}</p>`
  ].join("\n");
}
// After
function renderPerson(person) {
  const result = [];
  result.push(`<p>${person.name}</p>`);
  result.push(`<img src="${person.photo.url}" />`);
  result.push(`<p>제목: ${person.photo.title}</p>`);
  return result.join("\n");
}

function photoData(photo) {
  return `<img src="${photo.url}" />`;
}


5. 인라인 코드를 함수 호출로 바꾸기

* 중복된 코드를 함수로 추출하고 함수 호출로 대체
- 코드 중복 제거, 의도 명확화, 표준 라이브러리 활용으로 가독성 향상

// Before
let appliesToMass = false;
for (const s of states) {
  if (s === "MA") appliesToMass = true;
}

// After
appliesToMass = states.includes("MA");


6. 문장 슬라이드하기

* 관련 코드를 함께 모으기 위해 문장 위치 이동
- 코드 응집도 향상, 변수 사용 지점 최적화, 논리적 코드 그룹화

// Before
const pricingPlan = retrievePricingPlan();
const order = retreiveOrder();
let charge;
const chargePerUnit = pricingPlan.unit;
const units = order.units;
let discount;
charge = chargePerUnit * units;
let discountableUnits = Math.max(0, units - pricingPlan.discountThreshold);
discount = discountableUnits * pricingPlan.discountFactor;
if (order.isRepeat) discount += 20;
charge = charge - discount;
chargeOrder(charge);
// After
const pricingPlan = retrievePricingPlan();
const order = retreiveOrder();
const units = order.units;
const chargePerUnit = pricingPlan.unit;
let charge = chargePerUnit * units;

let discount = 0;
let discountableUnits = Math.max(0, units - pricingPlan.discountThreshold);
discount = discountableUnits * pricingPlan.discountFactor;
if (order.isRepeat) discount += 20;

charge = charge - discount;
chargeOrder(charge);


7. 반복문 쪼개기

* 여러 일을 수행하는 반복문을 용도별로 분리
- 단일 책임 원칙 적용, 가독성 향상, 코드 변경 용이성 확보

// Before
function calculateTotals(invoices) {
  let averageAge = 0;
  let totalSalary = 0;
  let count = 0;
  
  for (const invoice of invoices) {
    totalSalary += invoice.salary;
    averageAge += invoice.age;
    count++;
  }
  
  averageAge = averageAge / count;
  
  return { averageAge, totalSalary };
}
// After
function calculateTotals(invoices) {
  const totalSalary = invoices.reduce((sum, invoice) => sum + invoice.salary, 0);
  
  const count = invoices.length;
  const averageAge = invoices.reduce((sum, invoice) => sum + invoice.age, 0) / count;
  
  return { averageAge, totalSalary };
}


8. 반복문을 파이프라인으로 바꾸기

* 반복문을 map, filter 등의 파이프라인 연산으로 변환
- 선언적 프로그래밍으로 의도 명확화, 데이터 변환 과정 가시화, 코드 간결화

// Before
function getYoungestAgeOfAuthors(people) {
  let youngest = Number.MAX_VALUE;
  let totalSalary = 0;
  
  for (const p of people) {
    if (p.job === "author") {
      totalSalary += p.salary;
      if (p.age < youngest) youngest = p.age;
    }
  }
  
  return `최연소: ${youngest}, 총급여: ${totalSalary}`;
}
// After
function getYoungestAgeOfAuthors(people) {
  const authors = people.filter(p => p.job === "author");
  
  const youngest = Math.min(...authors.map(a => a.age));
  const totalSalary = authors.reduce((sum, a) => sum + a.salary, 0);
  
  return `최연소: ${youngest}, 총급여: ${totalSalary}`;
}


9. 죽은 코드 제거하기

* 실행되지 않는 코드 식별 및 제거
- 불필요한 코드를 제거하여 코드베이스를 간소화하고 유지보수성 및 가독성을 향상시킴

// Before
function processOrder(order) {
  let result;
  
  // legacy code that's no longer used
  if (false) {
    const legacyValue = calculateLegacyValue();
    result = legacyValue;
  }
  else {
    const newValue = calculateNewValue();
    result = newValue;
  }
  
  return result;
  
  function calculateLegacyValue() {
    // complex calculations that are no longer used
    return 100;
  }
  
  function calculateNewValue() {
    return 200;
  }
}
// After
function processOrder(order) {
  return calculateNewValue();
  
  function calculateNewValue() {
    return 200;
  }
}

 

 

마무리

책에서 보여주는 예제들은 객체지향 설계의 원칙을 효과적으로 설명하기 위한 것이지만, 실제 구현은 프로젝트의 특성과 팀의 선호도에 따라 다양하게 적용될 수 있습니다.

 

JavaScript에서 책의 예제처럼 클래스를 활용한 캡슐화가 가능하지만, JavaScript의 본질적 특성을 고려하면 함수형 프로그래밍 패러다임이 더 자연스러울 수 있습니다. 객체 리터럴과 클로저를 활용하여도 캡슐화를 구현할 수 있으며, JavaScript의 특성에 부합하는 코드로 이어집니다.

 

// 클로저를 이용한 캡슐화 예시
function createPerson(name) {
  let courses = [];  // 비공개 상태
  
  return {
    getName: () => name,
    getCourses: () => [...courses],
    addCourse: course => courses.push(course)
  };
}

 

TypeScript는 private, protected, public 같은 접근 제어자, 인터페이스와 타입 시스템, 그리고 컴파일 타임 검사 등을 통해 객체지향적 설계와 캡슐화에 더 적합한 도구를 제공합니다. 이러한 특성을 활용하면 대규모 애플리케이션에서 더 견고한 코드 구조를 만들 수 있습니다.

 

한편, '반복문 쪼개기', '죽은 코드 제거하기' 등의 리팩터링 기법은 프로그래밍 패러다임에 관계없이 코드 품질을 향상시키는 보편적인 방법들입니다. 이러한 기법들은 함수형, 객체지향, 절차적 프로그래밍 어디에서든 적용 가능하며, 코드의 가독성과 유지보수성을 개선할 수 있습니다.

 

결론적으로, 특정 프로그래밍 패러다임을 맹목적으로 따르기보다 프로젝트의 맥락과 언어가 지닌 고유한 특성을 고려하여 가장 적합한 기법을 선택하는 데 있습니다. 상황에 따라 적절한 접근법을 유연하게 채택함으로써 더 읽기 쉽고 유지보수하기 좋은 코드를 만들어낼 수 있습니다.

 

댓글