# 03-02. 중복 코드


# ✋ Intro

  • 하나의 애플리케이션을 여러 명이서 개발하다보면 동일한 기능을 담당하는 로직이 중복해서 생기는 경우가 많이 있다.
  • 동일한 구조의 코드가 여러 개 존재한다면 하나의 로직으로 통합해서 관리하는 것이 더 좋다.

# (1) 함수 추출하기

  • 한 클래스에 딸린 두 메서드가 똑같은 표현식을 사용하는 경우에 주로 이 기법을 사용한다.

  • 함수를 추출하는 최적의 기준은 '목적과 구현을 분리'하는 방식이다.

  • 함수를 짧게 만들면 함수 호출이 많아져 성능이 느려질 것 같은 우려가 있을 수 있지만 요즘은 그럴 걱정을 할 필요가 없다.

    • 함수가 짧으면 오히려 캐싱하기가 더 쉬우므로 컴파일러가 최적화하는데 유리할 때가 많다.
  • 함수를 추출하고 짧은 함수 작성시 함수명을 잘 지어야 한다.

  • 함수 추출 절자

    • 함수를 새로 만들고 목적을 잘 드러내는 이름을 붙인다.(이 때, '어떻게'(how)가 아닌 '무엇을'(what) 하는지가 드러나야 함)
      • 만약 이름 붙이는게 떠오르지 않으면 함수로 추출하면 안 되는 신호다.
      • 일단 추출을 해보고 효과가 크지 않으면 다시 원래 상태로 인라인해도 무방하다.
    • 추출한 코드를 원본 함수에서 복사하여 새 함수에 붙여넣는다.
    • 추출한 코드 중 원본 함수의 지역 변수를 참조하거나 추출한 함수의 유효범위를 벗어나는 변수는 없는지 검사한다. 만약 있다면 매개변수로 전달한다.(변수 범위 확인)
    • 변수 처리 작업 후 컴파일을 한 다음 원본 함수에서 추출한 코드 부분을 새로 만든 함수를 호출하는 문장으로 바꾼다.

  • 함수 추출하기 예시
// before
function printOwing(invoice) {
  let outstanding = 0;
    
  console.log('----------------------');
  console.log('-------고객 채무-------');
  console.log('----------------------');
    
  // 미해결 채무 계산
  for (const o of invoice.orders) {
    outstanding += o.amount;
  }
    
  // 마감일 기록
  const today = Clock.today;
  invoice.dueDate = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 30);
    
  // 세부 사항 출력
  console.log(`고객명: ${invoice.customer}`);
  console.log(`채무액: ${outstanding}`);
  console.log(`마감일: ${invoice.dueDate.toLocaleDateString()}`);
}
// after
function printBanner() {
  console.log('----------------------');
  console.log('-------고객 채무-------');
  console.log('----------------------');
}

function printDetails(invoice, outstanding) {
  console.log(`고객명: ${invoice.customer}`);
  console.log(`채무액: ${outstanding}`);
  console.log(`마감일: ${invoice.dueDate.toLocaleDateString()}`);
}

function recordDueDate(invoice) {
  const today = Clock.today;
  invoice.dueDate = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 30);
}

function calculateOutstanding(invoice) {
  let result = 0;
    
  for (const o of invoice.orders) {
    result += o.amount;
  }
    
  return result;
}

function printOwing(invoice) {
  // 배너 출력
  printBanner();
    
  // 미해결 채무 계산
  const outstanding = calculateOutstanding(invoice);
    
  // 마감일 기록
  recordDueDate(invoice);
    
  // 세부 사항 출력
  printDetails(invoice, outstanding);
}
// 직접 리팩토링한 코드

// constants.js(다국어 library인 i18n 모듈이 설치되어 있다고 가정)
const printDetailsKey = {
  customer: i18n.$t('고객명'),
  outstanding: i18n.$t('채무액'),
  dueDate: i18n.$t('마감일'),
}

// functions.js
function printHeader() {
  console.log('----------------------');
  console.log('-------고객 채무-------');
  console.log('----------------------');
}

function calculateOutstanding(orders) {
  return orders.reduce((sum, order) => sum + order.amount, 0);
}

function calculateDueDate() {
  const today = Clock.today;
  return new Date(today.getFullYear(), today.getMonth(), today.getDate() + 30);
}

function printDetails({ customer, outstanding, dueDate }) {
  console.log(`${printDetailsKey.customer}: ${customer}`);
  console.log(`${printDetailsKey.outstanding}: ${outstanding}`);
  console.log(`${printDetailsKey.dueDate}: ${dueDate.toLocaleDateString()}`);
}

// invoice라는 객체에 key 값으로 customer, orders만 있었다고 가정
function printOwing(invoice) {
  printHeader();

  const customer = invoice.customer;

  const outstanding = calculateOutstanding(invoice.orders);

  const dueDate = calculateDueDate();

  printDetails({ customer, outstanding, dueDate });
}

# (2) 문장 슬라이드하기

  • 비슷한 부분을 한 곳에 모아 함수 추출하기를 더 쉽게 적용할 수 있는 기법이다.

  • 코드 조각을 슬라이드할 때는 무엇을 슬라이드할지와 슬라이드할 수 있는지 여부를 확인해야 한다.

    • 무엇을 슬라이드할지는 맥락과 관련이 깊은데 주로 요소를 선언 코드를 슬라이드하여 처음 사용하는 곳까지 끌어내리는 기법도 존재한다.
    • 특히 슬라이드할 수 있는지 여부를 확인할 때 이전 동작과 달라지지 않는지 꼭 확인해야 한다.
    • 슬라이드가 안전한 지를 판단하려면 관련된 연산이 무엇이고 어떻게 구성되는지를 완벽히 이해해야 한다.

  • 조건문이 있을 때 문장 슬라이드하는 예시 코드
// before
let result;

if (availableResources.length === 0) {
  result = createResource();
  allocatedResources.push(result);
} else {
  result = availableResources.pop();
  allocatedResources.push(result);
}

return result;
// before
let result;

if (availableResources.length === 0) {
  result = createResource();
} else {
  result = availableResources.pop();
}

allocatedResources.push(result);

return result;

# ➕ 명령-질의 분리 원칙(command-query separation principle)

  • 함수는 성격에 따라 크게 두 가지로 분류할 수 있다.
    • 어떤 동작을 수행하는 명령과 답을 구하는 질의로 분리할 수 있다.
    • 이 두 역할은 한 곳에 섞이면 안 되는데 이러한 원칙을 명령-질의 분리 원칙이라고 한다.
// before
function getFirstName() {
  let firstName = document.querySelector("#firstName").value;
  firstName = firstName.toLowerCase();
  setCookie("firstName", firstName);
  if (firstName === null) {
    return "";
  }
  return firstName;
}
 
let activeFirstName = getFirstName();
  • 위 코드는 명령-질의 분리 원칙을 따르지 않은 코드이다.
    • 함수 이름만 보면 사람 이름을 return 한다는 것을 알 수 있다.
    • 하지만 실제로 내부적으로 수행하는 일은 사람 이름 return 하기 이전에 소문자로 변환하고 쿠키에 저장하는 일들도 수행하고 있다.
    • 이렇게만 봐서는 getFirstName 함수가 명령 역할을 하는지 질의 역할을 하는지 명확히 파악하기 어렵다.
    • 심지어 소문자로 변환하는 부분은 사용자에게 물어보지 않고 바로 쿠키로 설정한다.
  • 코드를 최대한 명확하게 작성하려면 함수에서 값을 return하는 작업과 데이터의 상태를 변경하는 작업을 한 함수에서 동시에 처리해서는 안 된다.
// after
function getFirstName() {
  let firstName = document.querySelector("#firstName").value;
  if (firstName === null) {
    return "";
  }
  return firstName;
}
 
setCookie("firstName", getFirstName().toLowerCase());
  • 위 코드와 같이 명령-질의 분리 원칙에 따라 작성하면 수행할 작업을 명확히 드러내고 에러 발생 가능성도 많이 줄일 수 있다.

  • 함수와 코드 베이스가 커질수록 이러한 분리 원칙은 더욱 중요하기 때문에 잊지 말자.



# (3) 메서드 올리기

  • 같은 부모로부터 파생된 서브 클래스들에 코드가 중복되어 있다면, 각자 따로 호출되지 않도록 하는 기법이다.
  • 메서드들의 본문 코드가 똑같을 때 하나로 통합하여 정리할 수 있다.
// before
class Party {}

class Employee extends Party {
  get annualCost() {
    return this.monthlyCost * 12;
  }
}

class Department extends Party {
  get totalAnnualCost() {
    return this.monthlyCost * 12;
  }
}
// after
class Party {
  get annualCost() {
    return this.monthlyCost * 12;
  }
}

class Employee extends Party {}

class Department extends Party {}