본문 바로가기
Study/Refactoring

리팩터링 1

by irerin07 2023. 8. 31.
728x90

1.1 자, 시작해보자!

public class Statement {
    public String statement(Invoice invoice, Map<String, Play> plays) {
    int totalAmount = 0;
    int volumeCredits = 0;

    StringBuilder result = new StringBuilder("Statement for " + invoice.customer + "\n");
    NumberFormat format = NumberFormat.getCurrencyInstance(Locale.US);
    format.setMinimumFractionDigits(2);

    for (Performance perf : invoice.performances) {
      Play play = plays.get(perf.playID);
      int thisAmount = 0;

      switch (play.type) {
        case "tragedy":
          thisAmount = 40000;

          if (perf.audience > 30) {
            thisAmount += 1000 * (perf.audience - 30);
          }

          break;

        case "comedy":
          thisAmount = 30000;

          if (perf.audience > 20) {
            thisAmount += 10000 + 500 * (perf.audience - 20);
          }

          thisAmount += 300 * perf.audience;

          break;

        default:
          throw new IllegalArgumentException("unknown type: " + play.type);
      }

      volumeCredits += Math.max(perf.audience - 30, 0);

      if ("comedy".equals(play.type)) {
        volumeCredits += Math.floorDiv(perf.audience, 5);
      }

      result.append(" ")
        .append(play.name)
        .append(": ")
        .append(format.format(thisAmount / 100.0))
        .append(" (")
        .append(perf.audience).append(" seats)\n");

      totalAmount += thisAmount;
    }

    result.append("Amount owed is ")
      .append(format.format(totalAmount / 100.0))
      .append("\n");
    result.append("You earned ")
      .append(volumeCredits)
      .append(" credits\n");

    return result.toString();
  }
}
Statement for BigCo
 Hamlet: $650.00 (55 seats)
 As You Like It: $580.00 (35 seats)
 Othello: $500.00 (40 seats)
Amount owed is $1,730.00
You earned 47 credits

1.2 예시 프로그램을 본 소감

  • 사람은 코드의 미적 상태에 민감하다. 설계가 나쁜 시스템은 수정하기 어렵다.
  • 프로그램의 작동 방식을 더 쉽게 파악할 수 있도록 코드를 여러 함수와 프로그램 요소로 재구성한다.
    • 빈약한 구조의 프로그램은 구조를 바로잡으면 수정 작업이 훨씬 수월해진다.

수정할 부분

  1. 청구 내역을 HTML로 출력하는 기능
    1. 청구 결과에 문자열을 추가하는 문장 각각을 조건문으로 감싸야한다. 이런 경우라면 해당 함수의 복사본을 만들어 해당 복사본에서 HTML을 출력하는 식으로 처리할 수 있지만 해당 함수에 로직 변경등이 발생하는 경우 원본과 복사본 모두 수정해야 하며 일관되게 수정했는지도 확인해야 한다. 오래 사용할 프로그램이라면 중복 코드는 골칫거리가 된다.
  2. 더 많은 장르의 연기를 하고 싶어하는 배우들의 요구사항
    1. 공연료와 적립 포인트 계산법에 영향을 미친다.

1.3 리팩터링의 첫 단계

  • 리팩터링 할 코드 영역을 꼼꼼하게 검사해줄 테스트 코드를 작성한다.
  • 리팩터링 기법들이 버그 발생 여지를 최소화 하도록 구성되어있지만서도 실수는 언제나 발생할 수 있다. 왜? 실제 작업은 사람이 수행하기 때문에

statement()의 테스트의 구성

해당 함수는 문자열을 반환하므로 다양한 장르의 공연들로 구성된 공연료 청구서 몇개를 미리 작성하여 문자열 형태로 준비해둔다. 그 다음 statement()가 반환한 문자열과 비교를 한다.

1.4 statement() 함수 쪼개기

  • 본 예제와 같이 긴 함수를 리팩터링 할 때에는 먼저 전체 동작을 각각의 부분으로 나눌 수 있는 지점을 찾는다. 예를 들면 예제의 switch문을 들 수 있다.
  • switch문은 한 번의 공연에 대한 요금을 계산하고 있다.
    • 이렇게 코드를 분석해서 얻은 정보는 휘발성이 높기 때문에 미루지 말고 바로 리팩터링을 반영해야 한다.
  • 코드 조각을 별도 함수로 추출하는 방식으로 앞서 파악한 정보를 코드에 반영할 것이다.
  • 추출한 함수에는 그 코드가 하는 일을 설명하는 이름을 지어준다.
  • 이런 식으로 코드 조각을 함수로 함수로 추출하는 것을 ‘함수 추출하기’라고 한다
  • 별도 함수로 빼냈을 때 유효범위를 벗어나는 변수, 즉 새 함수에서는 곧바로 사용할 수 없는 변수의 존재 여부를 확인한다.
    • 이번 예제에서는 perf, play, thisAmount가 해당한다.
  • perf, play는 추출한 함수에도 필요하지만 값을 변경하지 않기 때문에 매개변수로 전달 받는다.
  • thisAmount는 함수 안에서 값이 변화하기 때문에 주의해야 한다.
  • 수정한 뒤 곧바로 테스트를 진행한다.
  • 아무리 간단한 수정이라도 리팩터링 후에는 항상 테스트를 하도록 하자.
    • 수정할 떄마다 테스트하면, 오류가 발생해도 그 변경폭이 좁기 때문에 문제 해결이 훨씬 쉽다.
    • 조금씩 변경하고 매번 테스트 하는 것은 리팩터링 절차의 핵심
  • 성공적으로 테스트까지 통과했다면 커밋을 한다.
    • 중간에 문제가 생기더라도 이전의 상태로 쉽게 돌아갈 수 있다.
    • 어느정도 유의미한 단위로 커밋이 쌓이면 저장소로 푸쉬 한다.
    •  
  • 함수 추출 뒤에는 추출된 함수를 들여다보며 더 명확하게 표현할 수 있는 간단한 방법은 없나 확인한다.
    • 우선 변수명 thisAmount를 result로 변경할 수 있겠다.
    • 저자는 매개변수의 역할이 뚜렷하지 않은 경우 부정 관사(a/an)를 붙인다고 한다.

좋은 코드는 하는 일이 명확히 드러나야 하며, 변수 이름은 큰 역할을 한다.

play 변수 제거하기

  • amontFor()의 매개변수 : aPerformance, play
    • aPerformance는 루프 변수에서 오기 때문에 반복문을 통해 자연스레 값이 변한다.
    • play는 개별 공연 aPerformance에서 얻기 때문에 애초에 매개변수로 전달할 필요가 없이 amountFor()에서 다시 계산하면 된다.
  • 임시 변수를 질의 함수로 바꾸기를 사용해 리팩터링 한다.
    • 대입문의 우변을 함수로 추출 plays.get(aPerformance.getPlayId());
  • 변수 인라인하기를 적용해 인라인된 변수를 제거한다.
    • Play play = playFor(perf);를 제거
    • 기존 play 사용하던 부분을 payFor(perf); 메소드 로 변경
  • 함수 선언 바꾸기 를 적용해 amountFor() 메소드에서 play 가 사용된 부분을 playFor(perf);로 변경
    • 필요 없어진 play 매개변수 제거
  • 이번 리팩토링을 수행하면서 기존에는 루프를 한 번 돌때마다 한 번 조회하던 공연 정보를 총 세 번이나 조회한다.
    • 우선은 성능에 큰 이슈가 없으니 넘어가도록 한다. 제대로 리팩터링 된 코드라면 성능 개선하기도 훨씬 수월하다.
  • 지역 변수 제거의 가장 큰 이점 : 추출 작업이 훨씬 편해진다. 유효 범위를 신경 써야 할 대상이 줄어들기 때문.
  • amountFor()에 전달해야하는 인수를 모두 처리했으니 다시 변수 인라인하기를 사용하여 thisAmount()변수를 amountFor(perf)로 인라인 하여 리팩터링 한다.

적립 포인트 계산 코드 추출하기

  • volumeCredits는 반복문을 돌 때마다 값을 누적해야 하기 때문에 살짝 더 까다롭다.
    • 최선의 방법은 추출한 함수에서 volumeCredits의 복제본을 초기화한 뒤 계산 결과를 반환토록 하는 것이다.

format 변수 제거하기

  • 임시 변수는 자신이 속한 루틴에서만 의미가 있어서 루틴이 길고 복잡해지기 쉽기 때문에 나중에 문제를 일으킬 수 있다.
  • format은 임시 변수에 함수를 대입한 형태인데, 함수를 직접 선언해 사용하도록 바꾼다.
  • 한가지 마음에 걸리는 것은 format은 이 함수가 하는 일을 충분히 설명해주지 못한다.
    • 그래서 이 함수의 핵심인 화폐 단위 맞추기의 느낌을 살리는 이름을 골라 함수 선언 바꾸기 를 적용한다.

이름 짓기는 중요하지만 쉽지 않다.

긴 함수를 작게 쪼개는 리팩터링은 이름을 잘 지어야만 효과가 있다.

volumeCredits 변수 제거하기

  • 이 변수는 반복문을 한 바퀴 돌 때마다 값을 누적시키기 때문에 리팩터링이 까다롭다.
  • 반복문 쪼개기를 사용해 volumeCredits값이 누적되는 부분을 따로 빼낸다.
  • 그 다음 문장 슬라이드하기를 적용해 volumeCredits 변수를 선언하는 문장을 반복문 바로 앞으로 옮긴다.
  • volumeCredits 값 갱신과 관련한 문장들을 한데 모아두면 임시 변수를 질의 함수로 바꾸기가 수월해진다.
  • 이번에도 역시 volumeCredits 값 계산 코드를 함수로 추출하는 작업부터 한다.
  • 추출이 끝났다면 volumeCredits 변수를 인라인 한다.

소프트웨어의 성능은 대체로 코드의 몇몇 작은 부분에 의해 결정되기에 그 외의 부분을 수정한다고 해도 성능 차이를 체감하기 어렵다.

하지만 때로는 리팩터링이 성능에 상당한 영향을 미치기도 하지만 우선은 개의치 않고 리팩터링을 진행한다. 코드가 잘 다듬어져 있어야 성능 개선 작업을 훨씬 수월하게 진행할 수 있기 때문이다.

진행하던 리팩터링을 마무리하고 성능 개선 작업을 시작하자.

작업의 단계를 잘게 나누는 것도 도움이 된다.

volumneCredits 변수를 제거하는 작업의 단계는 다음과 같이 세분화 되어 있다.

  1. 반복문 쪼개기 (컴파일 - 테스트 - 커밋)
  2. 문장 슬라이드 하기 (컴파일 - 테스트 - 커밋)
  3. 함수 추출하기 (컴파일 - 테스트 - 커밋)
  4. 변수 인라인하기 (컴파일 - 테스트 - 커밋)

이렇게 하면 리팩터링 중간에 오류가 발생하더라도 가장 최근 커밋으로 돌아가 테스트에 실패한 리팩터링의 단계를 더 잘게 나누어 다시 시도하고, 문제를 해결하기 수월해진다.

totalAmount 변수 제거

  • 반복문 쪼개기, 변수 초기화 문장 옮기기, 함수 추출의 단계를 거치는데 여기엔 문제가 하나 있다.
  • 추출할 함수의 이름으로 “totalAmount”가 제일 좋겠지만 이미 동일한 이름의 변수가 있다. 이런 경우 일단 아무 이름이나 붙여 진행을 한다.
  • 그 다음 totalAmount변수를 인라인 한 다음 함수 이름을 더 의미있게 고친다.

1.5 중간 점검 : 난무하는 중첩함수

  • 훨씬 좋은 구조의 코드가 완성되었다.
  • statement()는 이제 단 일곱줄 뿐이며 출력할 문장을 생성하는 일만 담당한다.
  • 계산 로직은 모두 여러 개의 보고 함수로 빼냈다.

1.6 계산 단계와 포맷팅 단계 분리하기

지금까지는 논리적인 요소를 파악하기 쉽도록 코드의 구조를 보강했다. 복잡하게 얽힌 덩어리를 잘게 쪼개는 작업은 이름을 잘 짓는 일만큼 중요하다

이제 원하던 기능 변경, statement()의 HTML 버전을 만드는 작업을 살펴보자.

계싼 코드가 모두 분리되었기 때문에 최상단 코드에 대응하는 HTML 버전만 작성하면 된다.

문제가 하나 있는데, 분리된 계싼 함수들이 텍스트 버전인 statement() 안에 중첩 함수로 들어가 있는것이다. 이 모두를 그대로 복사해서 HTML버전으로 만들고 싶지는 않다.

텍스트 버전과 HTML 버전 모두가 똑같은 계산 함수들을 사용하게 만들고 싶다.

단계 쪼개기를 사용한다.

statement()의 로직을 두 단계로 나누는 것인데, 첫 단계에서는 statement()에 필요한 데이터를 처리하고, 다음 단계에서는 앞서 처리한 결과를 텍스트가 HTML로 표현하도록 하는 것이다.

즉 첫번째 단계에서는 두번째 단계로 전달할 중간 데이터 구조를 생성하는 것이다.

그 다음으로 두 단계 사이의 중간 데이터 구조 역할을 할 객체를 만들어 renderPlainText()에 인수로 전달한다.

renderPlainText()의 다른 두 인수 invoice와 plays들을 살펴보자.

해당 인수들을 통해 전달되는 데이터를 모두 방금 만든 중간 데이터 구조로 옮기면, 계산 관련 코드는 전부 statement()함수로 모으고 renderPlainText()는 data 매개변수로 전달된 데이터만 처리하게 할 수 있다.

우선 고객정보부터 중간 데이터 구조로 옮긴다. 그리고 같은 방식으로 공연 정보까지 중간 데이터 구조로 옮기면 renderPlainText()의 invoice 매개변수를 삭제한다.

1.7 중간 점검 : 두 파일(과 두단계)로 분리됨

1.8 다형성을 활용해 계산 코드 재구성하기

연극 장르를 추가하고 장르마다 공연료와 적립 포인트 계산법을 다르게 지정하도록 기능을 수정해보자.

이번 작업의 목표는 상속 계층을 구성하여 희극 서브클래스와 비극 서브클래스가 각자의 구체적인 계싼 로직을 정의하는 것이다.

핵심이 되는 리팩터링 기법은 조건부 로직을 다형성으로 바꾸기 이다.

공연료 계산기 만들기

공연료 계산기 클래스의 객체로 아직까지는 할 수 있는 일이 없다.

우선 연극 레코드같이 가장 간단한 동작을 옮기는 작업을 수행 할 것인데, 이 작업은 다형성을 적용해야 할 만큼 차이가 크지는 않지만 모든 데이터 변환을 한 곳에서 수행할 수 있어서 코드가 더 명확해진다.

함수들을 계산기로 옮기기

지금까지는 중첩 함수를 재배치하는 것이어서 함수를 옮기는 데 부담이 없었따. 하지만 이번에는 함수를(모듈, 클래스등) 다른 컨텍스트로 옮기는 큰 작업이다.

함수 옮기기를 사용하여 단계별로 진행해보자.

1.9 상태 점검 : 다형성을 활용하여 데이터 생성하기

1.10 마치며

728x90

'Study > Refactoring' 카테고리의 다른 글

리팩터링 3  (0) 2023.08.31
리팩터링 2  (0) 2023.08.31
리팩터링 12  (0) 2023.08.30
리팩터링 11  (0) 2023.08.29
리팩터링 챕터 10  (0) 2023.08.28