0w0

리팩터링 기초 (정리)

리팩터링 기초 (정리)

안녕하세요. 개발과 taku_76입니다.

최근 기능 개발할 때, 요건 구현은 단순한데 손이 갈 때마다 복잡해지는 기능 개선에 시간을 들인 일이 있었습니다.

여기서부터 리팩터링을 꽤 신경쓰기되어, 사내에서 리팩터링 낭독회에 참가하고, 개인적으로도 책을 읽고 있기에 이번에는 리팩터링의 기초에 대해 쓰려합니다.

리팩터링이란

리팩터링이란 소프트웨어가 외부에 보여지는 것은 유지하며, 내부 구조를 개선하는 것입니다.

리팩터링함으로 가독성이 오르고, 수정할 때 코드 변경도 용이해지게 할 수 있습니다.

가독성 높아지므로 설계할 때나 구현, 테스트 등의 공정에 도움되어 결과적으로 이후 개발에도 도움이 됩니다.

리팩터링 장점

리팩터링의 장점은 다음과 같습니다.

가독성 향상

가독성이 좋아져서, 소프트웨어 이해하기 쉬워집니다.

이전에는 읽는 것만해도 시간이 많이 들었고, 의도를 알 수 없는 코드가 있어 이해하기 어려웠습니다.

그에 반해 리팩터링한 코드는 목적이 명백해지고, 실현하고 싶은 것을 명확히 표현할 수 있습니다.

코드 변경 용이

정리된 코드는 변경이 간단합니다.

가령 중복코드가 있을 경우, 같은 변경을 여러 곳에 해야합니다.

하지만 리팩터링에 의해 중복 코드를 제거하면 한 곳만 변경하면 되며, 수정 누락 같은 염려도 없어집니다.

또한 복잡한 조건 분기가 존재하는 경우에는 조건 하나 더할 때마다 복잡도는 상승합니다.

복잡한 조건 분기는 의도하지 않은 디그레이드(의도하지 않은 버그)를 발생시킬 위험성이 있습니다.

개발 속도 향상

내부설계가 우수해진 코드는 새 개발할 때 어디에 변경하면 좋은가를 바로 알 수 있게 해줍니다.

또한 제대로 모듈화된 코드를 수정할 때는 이해해야할 곳이 한정됩니다.

기능 개발 진행중, 테스트 할 때 버그가 발견되어도 디버그가 편해서 바로 대응할 수 있습니다.

이런식으르 개발할 때에 낭비가 적어지므로, 개발 속도 향상과 이어집니다.

리팩터링 대상

리팩터링해야하는 대상 몇을 소개하겠습니다.

언급되는 것 외에도 여럿 있습니다, 코드를 읽을 때 언급된 코드가 있다면 리팩터링을 해야하는 신호입니다.

이해하기 어려운 이름

코드 이해를 원활히 하기 위해 중요한 것이 적절한 이름입니다

이를 위해서 클래스, 함수, 변수 등에는 의도를 알기어려운 이름이나

이름에 반하는 처리가 섞여있을 경우 반드시 변경해야합니다.

또 좋은 이름이 떠오르지 않을 때는 설계가 느슨한 경우가 있으니 설계를 다시 확인합시다.

중복 코드

같은 코드 구조가 여럿있다면 1곳에 정리하는 것 만으로 코드가 개선됩니다.

중복 코드가 있으면 복사된 곳곳마다 뭔가 할 때, 놓치지는 않았나 신경써야 할 필요가 생깁니다.

게다가 수정할 때도 중복 코드를 다 같이 수정해야만 합니다.

가변 가능 데이터

가변 가능 데이터는 예측하기 어려운 행동이나, 이상한 버그를 일으키는 원인이 되기 쉽습니다.

사양 변경으로 처리가 변할 때에 의도하지 않는 값으로 변할 가능성이 있기에,

설계할 때 가변인가 불변인가 주의해야합니다.

긴 함수

알려진 사실이지만, 함수가 길면 길수록 코드 이해는 어려워집니다.

긴 코드를 발견했다면 함수를 자를 수 없을까 확인합시다.

그 안에 파라메터나 일시 변수가 많은 함수는, 함수를 자른 만큼 파라메터 주고 받기가 필요하므로, 일시 변수를 감소시킬 필요가 있습니다.

기본적 리팩터링 소개

리팩터링 수법은 책에 많이 소개되어있습니다.

이번에는 간단한 예이지만, 기본적인 리팩터링과 조건 분기 리팩터링을 몇 소개하려합니다.

@ 깊게 생각해야하는 클래스 설계는 고려하지 않습니다.

함수 추출

처리마다 정리/독립시켜 함수를 추출합니다.(함수명 주의)

코드를 읽고나서 뭘 하고 있는가 조사해야하는 곳이 있다면, 목적을 명백히해서 함수로 추출해야합니다.

함수로 만들며 목적을 바로 할 수 있도록하면 함수 속까지 신경쓸 필요가 없어집니다.

간단한 JavaScript 예시를 보여드리겠습니다.

function printOwing(invoice) {
  printBanner();
  let unpaidMoney = calculateUnpaidMoney();

  // 명세서 인쇄
  console.log(`name: ${invoice.customer}`);
  console.log(`amount: ${unpaidMoney}`);
}

위 함수에서 주석에 "명세서 인쇄"가 있는 곳이 있습니다.

이런 식으로 무엇을 하는가를 설명하는 주석으로 시작하는 코드르 발견했을 때는, 이에 받는 함수로 추출해야 코드가 보기 쉬워집니다.

function printOwing(invoice) {
  printBanner();
  let unpaidMoney = calculateUnpaidMoney();
  printInvoiceDetails(unpaidMoney);

  function printInvoiceDetails(unpaidMoney) {
    console.log(`name: ${invoice.customer}`);
    console.log(`amount: ${unpaidMoney}`);
  }
}

함수를 추출할 때 주의점으로, 목적이 알맞은 명명을 하지 않는다면, 역으로 이해하기 어려운 코드가 되므로 명명에는 주의가 필요합니다.

또한 반대로 함수로 함으로 알기 어려워지는 코드는 함수를 제거해 인라인화 하는 경우도 있습니다.

조건 분해

복잡한 조건 처리는, 프로그램을 복잡하기 하는 원인 중 하나 입니다.

다양한 조건에 맞는 처리를 하는 코드를 쓰는 것 만으로, 긴 함수가 되고 읽기 어려워집니다.

그 결과, 그 코드의 "의도"를 이해하기가 어려워집니다.

해결책으로, 필요에 맞게 의도에 충실한 이름로 함수를 호출하도록 바꾸면 의도가 명확해집니다.

조건 분기는, 조건 판정과 조건 자체 처리를 각각 함수로 바꿔끼우는 걸 권합니다.

JavaScript 예시로 다음과 같이, 주말만 할인하는 과금 계산이 있다 칩시다.

if (days[today.getDay()] == '토요일' || days[today.getDay()] == '일요일') {
  price = quantity * plan.specialRate;
} else {
  price = quantity * plan.regularRate + plan.regularServicePrice;
}

우선 요일 판정을 추출합니다

if (specialDayOfWeek()) {
  price = quantity * plan.specialRate;
} else {
  price = quantity * plan.regularRate + plan.regularServicePrice;
}

function specialDayOfWeek() {
  return days[today.getDay()] == '토요일' || days[today.getDay()] == '일요일';
}

다음으로 조건 충족시 행동을 함수로 추출합니다.

if (specialDayOfWeek()) {
  price = specialPrice();
} else {
  price = quantity * plan.regularRate + plan.regularServicePrice;
}

function specialDayOfWeek() {
  return days[today.getDay()] == '토요일' || days[today.getDay()] == '일요일';
}
function specialPrice() {
  return quantity * plan.specialRate;
}

마지막으로 else를 함수로 추출합니다.

if (specialDayOfWeek()) {
  price = specialPrice();
} else {
  price = regularPrice();
}

function specialDayOfWeek() {
  return days[today.getDay()] == '토요일' || days[today.getDay()] == '일요일';
}
function specialPrice() {
  return quantity * plan.specialRate;
}
function regularPrice() {
  return quantity * plan.regularRate + plan.regularServicePrice;
}

취향차이일 수도 있으나, 금액계산은 삼항이어도 좋을 것 같습니다.

price = specialDayOfWeek() ? specialPrice() : regularPrice();

이런 식으로 수정함으로, 금액은 특별한 요일이면 할인, 그렇지 않으면 통상 가격이라는걸 직감적으로 알 수 있습니다.

조건 통합

여러 조건 판정이 있고, 그 조건이 다르겠지만, 결과가 같은 경우가 있습니다.

이런 조건 기술은 단일 결과로 반환하는 조건파나정에 통합합니다

조건을 통합하는 장점은 2가지 있습니다.

주의점으로 조건판정을 통합해도, 관계된 곳에 영향이 없는가 사전에 확인해야할 필요가 있습니다.

또한 중복 판정이 따로 존재하는데, 이를 하나의 판정을 묶어 가독성이 떨어진다면 리팩터링 하지 않습니다.

과정 생략한, JavaScript의 간단한 예시를 보여드리겠습니다.

if (player.accountLevel < 100) return 0;
if (player.loginPeriod < 100) return 0;

각 조건 결과는 같으므로, 조건 판정을 추출해서 논리연산자로 통합합니다.

결과로 얻는 조건 판정을 함수화해서 판정이 하나라는 의도를 명시할 수 있습니다.

if (noBonusAccount()) return 0;

function noBonusAccount() {
  return player.accountLevel < 100 || player.loginPeriod < 100;
}

가드에 의한 위치 변경

조건에는 다음과 같은 형식이 있습니다.

예외적으로 동작은 조건이 성립한 시점에 리턴하는 것이 가드라 일컬립니다.

카드를 사용하면 주요한 처리를 명확히 전달할 수 있습니다.

또한 코드 상에는 네스트를 줄일 수 있으므로 가독성 향상과 이어집니다.

function getPayAmount() {
  let result;
  if (isDead) {
    result = deadAmount();
  } else {
    if (isSeparated) {
      result = separatedAmount();
    } else {
      if (isRetired) {
        result = retireAmount();
      } else {
        result = normalPayAmount();
      }
    }
  }
  return result;
}

deadAmount(), separatedAmount(), retireAmount()는 예외적인 동작이므로, 성립된 단계에서 return하도록 수정합니다.

function getPayAmount() {
  if (isDead) return deadAmount();
  if (isSeparated) return separatedAmount();
  if (isRetired) return retireAmount();
  return normalPayAmount();
}

맺으며

이번에는 리팩터링 첫걸음이라는 개요로 예시를 소개했습니다.

읽은 책 내용을 바로 반영한다는 건 어렵지만, 어떻게 가독성 향상, 변경을 용이하게 할 수 있는가 늘 의식하며 기능을 다루면 좋겠다 생각합니다.

참고 서적