본문 바로가기

Untagged

리팩터링 2판 요약 - 챕터 6

728x90
728x90

6.1 함수 추출하기

배경

코드 이해를 돕기 위함

절차

  1. 함수를 새로 만들고 목적을 잘 드러내는 이름을 붙인다.
  2. 추출할 코드를 원본 함수에서 복사해 새 함수에 넣는다.
  3. 추출한 코드 중 원본 함수의 지역 변수를 레퍼런스 하거나 추출한 함수의 유효범위를 벗어나는 변수는 없는지 검사한다. 있다면 매개변수로 전달한다.
  4. 변수를 다 처리하면 컴파일
  5. 추출한 코드 부분을 새로 만든 함수의 호출로 바꾼다.
  6. 테스트한다.
  7. 추출한 함수와 비슷한 코드가 다른 곳에 있는지 살펴보고 있으면 추출한 함수로 바꿀지 결정한다.

6.2 함수 인라인하기

배경

6.1의 반대로 함수 이름보다 코드 그 자체가 이해하기 쉬운 경우가 있다.

절차

  1. 다형 메서드(Polymorphic Method)인지 확인. 그렇다면 하면 안 된다.
  2. 함수를 호출하는 곳을 모두 찾는다.
  3. 각 호출을 본문(인라인 화)으로 바꾼다.
  4. 한 번에 모두 바꾸는 게 아니라 하나씩 바꿔보며 테스트한다.
  5. 함수 정의를 삭제한다.

6.3 변수 추출하기

배경

복잡한 계산이 들어간 표현식은 이해하기 어려울 수 있음

절차

  1. 추출하려는 표현식에 부작용은 없는지 확인
  2. 불변 변수를 하나 선언하고 이름 붙일 표현식을 복제
  3. 원본 표현식을 새로 만든 변수로 교체
  4. 테스트
  5. 표현식을 여러 곳에서 사용한다면 하나씩 바꿔보면서 테스트

6.4 변수 인라인하기

var basePrice = costumer.BasePrice;

return basePrice;

 

=>

return customer.BasePrice;

 

절차

  1. 대입문의 우변 표현식에서 부작용은 없는지 살펴본다.
  2. 도중에 값이 바뀌는지 검증하기 위해 변수가 immutable하지 않다면 immutable 하게 만든 후 테스트한다.
  3. 이 변수를 가장 처음 쓰는 코드를 가 대입문으로 바꾼다.
  4. 테스트한다.
  5. 이 과정을 반복하여 모든 변수를 대체한다.

6.5 함수 선언 바꾸기

배경

메서드 이름을 잘 짓는 것은 중요하다. 메서드에 어떤 매개변수를 선정할지도 중요하다.

 

변경 사항을 잘 보고 선언과 호출문을 바로 변경할 수 있는지 판단한다. 그럴 경우 "간단한 절차"를 따른다.

 

그리고 변경이 복잡하거나 호출문을 점진적으로 수정할 때는 "마이그레이션 절차"를 적용하면 된다.

간단한 절차

  1. 매개변수 제거를 원하면 본문에서 해당 매개변수를 레퍼런스하는 곳을 살펴본다.
  2. 메서드 선언을 바꾼다.
  3. 기존 메서드를 레퍼런스하는 부분을 모두 바꾼다.
  4. 테스트한다.

마이그레이션 절차

  1. 추출을 수월하게 하려면 본문을 적절히 리팩터링 한다.
  2. 함수 본문을 새 함수로 추출한다.
  3. 매개변수를 추가해야 하는 경우 "간단한 절차"로 추가한다.
  4. 테스트한다.
  5. 기존 함수를 인라인 한다.(새 함수로 바꾼다.)
  6. 이름을 임시로 붙였다면 함수 선언 바꾸기를 다시 해서 적절한 이름으로 만든다.
  7. 테스트한다.

상황이 복잡할 수 있기 때문에 간접 호출로 우회하는 방법도 있다.

마이그레이션 절차 예시

다음 함수를 수정해 보자.

float circum(float radius)
{
  return 2 * 3.141592f * radius;
}

 

함수를 추출하여 전달 형태로 바꾼다.(2)

float Circum(float radius)
{
  return Circumference(radius);
}

float Circumference(float radius)
{
  return 2 * 3.141592f * radius;
}

 

수정한 코드를 테스트한 뒤(4) 예전 함수를 인라인 한다.(5) 그리고 예전 함수를 모두 새 함수로 바꿔준다.(7)

 

하나씩 바꿀 때마다 테스트를 하면서 모두 완료하면 기존 함수를 삭제한다.

 

함수 선언 바꾸기는 직접 수정이 불가능한 외부 API 또는 라이브러리가 사용되는 부분에 쓰기 좋은 리팩터링 방법이다.

매개변수 추가 예시

다음 메서드에 대해 어떤 true/false 플래그에 따라 로직이 달라지도록 바뀌어야 한다.

void BookFlight(int customerId)
{
  m_ReservationQ.Add(customerId);
}

 

먼저 새 메서드를 추출하여 여기에 플래그 값으로 전달할 매개변수를 추가한다.

void BookFlightCapsule(int customerId, bool isPreemtion)
{
  BookFlight(customerId);
}

 

프로그래밍 언어에 따라 assertion을 추가하여 실제로 쓰이는지 확인할 수 있는데, 새 매개변수를 빠트린 부분을 찾는데 도움이 된다.

 

그런 다음 호출되는 부분마다 인라인 하기를 적용하고 테스트를 한다. 완료하면 기존 메서드를 제거하고 메서드의 이름을 바꾼다.

매개변수 변경 예시

매개변수로 전달되는 변수를 다른 걸로 바꿔야 하는 경우 마이그레이션 절차가 도움 된다. 다음 예시는 어떤 유저의 id가 블랙리스트인지 확인하는 메서드이다.

bool IsBlacklist(User user)
{
  return m_BlacklistSet.Contains(user.Id);
}

 

Id만 사용하도록 하는 새로운 메서드를 추출하여 감싼 후 인라인으로 교체할 수 있다.

bool IsBlacklist(User user)
{
  return IsBlackListId(user.Id);
}

bool IsBlacklistId(int userId)
{
  return m_BlacklistSet.Contains(userId);
}

 

====>

 

bool IsBlacklist(int userId)
{
  return m_BlacklistSet.Contains(userId);
}

6.6 변수 캡슐화 하기

배경

데이터를 수정하는 것보다 함수를 수정하는 게 더 쉬우므로 함수로 캡슐화를 한다. 전역 데이터 같이 사용 범위가 넓은 데이터는 변경해야 할 일이 있을 때 상당히 까다로워진다.

 

데이터를 캡슐화하면 데이터의 사용이나 변경을 감지하기 쉽다.(*디버깅)

 

데이터의 유효범위(사용처)가 넓을수록 캡슐화를 해야 한다.

절차

  1. 변수로의 접근과 갱신을 담당하는 함수를 만든다.
  2. 정적으로 검사한다.
  3. 직접 레퍼런스 하는 부분을 모두 함수 호출로 바꾼다. 하나씩 바꿀 때마다 테스트한다.
  4. 접근 범위를 제한한다.
  5. 테스트한다.
  6. 변수 값이 레코드라면 "레코드 캡슐화하기" 기법을 고려한다.

변수의 접근 자체를 제한할 수 없는 경우가 있으면 차선으로 변수 이름을 바꿔본다. __privateOnly_ 같은 문구가 붙어있으면 조금이나마 도움이 된다.


기본적인 캡슐화 기법은 레퍼런스를 캡슐화해서 외부에서 값을 레퍼런스 할 때는 제어(확인)할 수 있다.

 

변수의 필드 변경을 제어하려면

  1. 해당 변수를 반환하는 함수에서 깊은 복사로 생성한 복제본을 반환하여 변경 자체를 막는다.
  2. "레코드 캡슐화하기"로 해당 변수를 클래스 같은 구조로 추상화한다.

 

이 방식은 레코드 구조에서 깊이가 1인 필드만 유효하다.

6.7 변수 이름 바꾸기

var a = height * width; // Before
var area = height * width; // After

배경

이해도를 높이기 위해 변수 이름을 잘 지어야 한다.

절차

  1. 폭넓게 쓰이는 변수라면 "변수 캡슐화하기"를 고려한다.
  2. 변경하고 테스트한다.(*원본에는 하나씩 바꾸라고 되어있는데 요즘은 그냥 IDE나 텍스트 에디터의 변수 바꾸기 기능을 써도 될 것이다.)

6.8 매개변수 객체 만들기

배경

여러 데이터가 여러 부분에서 등장하는 경우가 있는데 하나의 구조로 묶어준다.

 

새로 만든 구조가 영역을 간결하게 표현하는 새로운 추상 개념으로 올라가면서 코드의 큰 그림을 다시 그릴 수 있게 해 준다.

 

데이터 뭉치를 묶다보면 그 자체로 역할을 할 수 있는 클래스가 될 수 있다.

절차

  1. 적당한 구조가 없으면 새로 만든다.(클래스나 구조체)
  2. 테스트한다.
  3. "함수 선언 바꾸기"로 새 데이터 구조를 매개변수로 추가한다.
  4. 테스트한다.
  5. 함수 호출 시 새 구조 인스턴스를 넘기도록 수정한다. 하나씩 하면서 테스트한다.
  6. 기존 매개변수를 사용하던 코드에서 새 구조의 원소를 사용하도록 한다.
  7. 다 바꿨으면 기존 매개변수를 삭제하고 테스트한다.

예시

어떤 집합 자료구조에 특정 범위를 벗어난 값의 목록을 찾으려고 한다면 다음과 같은 함수를 고려할 수 있다.

var m_ValueSet = new SortedSet<int>();

// Insertions...

List<int> FindOutliners(SortedSet<int> st, int min, int max)
{
    var result = new List<int>();
    
    foreach (var element in st)
    {
        if (element < min || element > max)
        {
            result.Add(element);
        }
    }
    
    return result;
}


// Call FindOutliners
var outliners = FindOutliners(m_ValueSet, 0, 100);

 

여기서 min과 max는 항상 붙어 다니는 데이터 뭉치이므로 이들을 클래스로 묶으면 다음과 같이 바꿀 수 있다.

class IntRange
{
    public Min
    {
        get => m_Min;
    }
    
    public Max
    {
        get => m_Max;
    }
    
    int m_Min;
    int m_Max;
    
    public IntRange(int min, int max)
    {
        m_Min = min;
        m_Max = max;
    }
}

...

List<int> FindOutliners(SortedSet<int> st, IntRange intRange)
{
    var result = new List<int>();
    
    foreach (var element in st)
    {
        if (element < intRange.Min || element > intRange.Max)
        {
            result.Add(element);
        }
    }
    
    return result;
}

 

이렇게 클래스로 묶고 나면 최솟값과 최댓값을 필요하는 곳에 유용하게 활용할 수 있다.

6.9 여러 함수를 클래스로 묶기

배경

공통 데이터를 중심으로 긴밀하게 엮이는 함수 그룹이 대상

 

클래스로 묶으면 함수들의 공통 환경을 명확히 표현하고 매개변수를 줄일 수 있으며 다른 시스템에 전달하는 레퍼런스를 제공할 수 있다.

 

클래스를 지원하지 않는 언어의 경우 Function as Object 패턴을 이용할 수 있다.

절차

  1. 함수들이 공유하는 데이터 레코드를 캡슐화
  2. 공통으로 사용하는 레코드를 사용하는 각 함수를 새 클래스로 옮김
  3. 데이터 조작하는 로직들은 함수로 추출해 새 클래스로 옮김

6.10 여러 함수를 변환 함수로 묶기

배경

한 데이터로 여러 결과를 도출할 때 쓸 수 있다.(어떤 데이터 하나로 서로 다른 결과를 반환하는 함수들이 존재하는 경우)

 

"여러 함수를 클래스"로 묶기와 비슷하게 쓸 수 있으며 프로젝트의 스타일에 따라 고른다. 다만 원본 데이터가 코드 안에서 갱신되는 경우는 "여러 함수를 클래스로 묶기"를 쓰는 것이 좋다.

 

도출 로직이 중복되는 것을 해소할 수 있다.

절차

  1. 변환할 레코드를 입력받아 그대로 반환하는 함수를 생성한다. 이것이 변환 함수이다.
  2. 묶을 함수 중 하나를 변환 함수로 옮긴다. 그리고 그 처리 결과를 새 필드로 기록한다. 클라이언트가 이 코드를 사용하도록 한다.
  3. 테스트한다.
  4. 다 옮길 때까지 1번으로 돌아간다.

6.11 단계 쪼개기

배경

각 단계는 하나의 대상에 집중해야 함. 그래야 다른 모듈의 내용을 자세히 알지 않아도 수정을 용이하게 할 수 있음

 

이 기법이 적용되는 대표 예시로 컴파일러가 있으며 규모가 큰 소프트웨어에 적용됨.

절차

  1. 두 번째 단계에 해당하는 코드를 함수 추출한다.
  2. 테스트한다.
  3. 중간 데이터 구조를 만들어서 추출한 함수의 매개변수에 추가한다.
  4. 테스트한다.
  5. 추출한 함수의 매개변수를 검토한다. 매개변수 중에 첫 번째 단계에서 만들어진 것이 있다면 중간 데이터 구조로 옮기도록 한다.
  6. 첫 번째 단계에 해당하는 코드를 함수로 추출하고 이 함수에서 중간 데이터 구조를 반환하도록 한다.

 

 

728x90
728x90

'Untagged' 카테고리의 다른 글

PS 노베이스의 ICPC 도전 가이드  (1) 2024.01.01
리팩터링 2판 요약 - 챕터 3  (0) 2023.12.03
리팩터링 2판 요약 - 챕터 2  (1) 2023.11.12
[numpy] unravel_index  (0) 2022.01.02