기능 이동

2025. 5. 8. 00:54·Programming/Refactoring

해당 챕터에서는 요소를 다른 컨택스트로 옮기는 방법론에 대해 살펴본다. 

함수 옮기기 (Move Function)

배경

좋은 소프트웨어 설계의 핵심은 모듈화(Modularity)이다.
모듈화란, 특정 기능을 수정하거나 확장하려 할 때 그와 관련된 코드만 이해해도 작업이 가능하도록 만드는 구조를 뜻한다.

객체 지향 설계에서는 클래스가 모듈화의 단위가 된다. 따라서, 함수가 어떤 클래스에 속해 있는지가 중요하다.
어떤 함수가 현재 속한 클래스보다 다른 클래스의 데이터나 메서드를 더 자주 참조한다면, 그 함수는 옮겨져야 한다.

또한, 호출자들의 위치나 앞으로의 변경 가능성을 고려해 함수를 더 적절한 위치로 이동시키는 것도 중요하다.
함수가 어디에 위치해야 할지 명확하지 않은 경우, 후보 클래스 중 하나로 옮긴 뒤 실사용을 통해 적절성을 판단해볼 수 있다.

절차

  1. 함수가 사용하는 프로그램 요소를 분석
    • 함수가 참조하는 필드, 메서드 등을 확인하고 함께 옮겨야 할 것이 있는지 검토한다.
  2. 다형성 여부 확인
    • 함수가 오버라이딩되거나 상속 구조 내에 있다면 별도의 처리 또는 리팩터링을 병행해야 한다.
  3. 함수를 타깃 클래스로 복사
    • 필요한 인자나 참조를 조정하여 타깃 클래스에서 정상 동작하도록 만든다.
  4. 정적 분석 수행
    • 컴파일이나 정적 타입 검사를 통해 문제가 없는지 확인한다.
  5. 기존 클래스에서 새 위치의 함수를 참조하도록 변경
  6. 기존 함수를 위임 함수로 변경
    • 기존 함수는 새 함수로의 호출만 위임하도록 남긴다.
  7. 테스트 수행
  8. 필요하다면 기존 함수를 제거하거나 인라인 처리

예시

Before

// 클래스: Account
class Account {
  constructor(public balance: number, public overdraftLimit: number) {}

  // 이 함수는 BankPolicy의 규칙을 자주 사용하지만, Account에 위치해 있다
  canWithdraw(amount: number): boolean {
    return amount <= this.balance + BankPolicy.getOverdraftLimit(this);
  }
}

// 클래스: BankPolicy
class BankPolicy {
  static getOverdraftLimit(account: Account): number {
    return account.overdraftLimit;
  }
}

 

함수 이동 및 조정

// 클래스: Account
class Account {
  constructor(public balance: number, public overdraftLimit: number) {}
}

// 클래스: BankPolicy
class BankPolicy {
  static getOverdraftLimit(account: Account): number {
    return account.overdraftLimit;
  }

  // canWithdraw를 BankPolicy로 이동
  static canWithdraw(account: Account, amount: number): boolean {
    return amount <= account.balance + this.getOverdraftLimit(account);
  }
}

 

기존 함수는 위임

class Account {
  constructor(public balance: number, public overdraftLimit: number) {}

  // 위임 함수로 유지
  canWithdraw(amount: number): boolean {
    return BankPolicy.canWithdraw(this, amount);
  }
}

 

After

class Account {
  constructor(public balance: number, public overdraftLimit: number) {}

  // 완전히 제거 가능
}

// 필요 시 전역에서 직접 사용
BankPolicy.canWithdraw(account, amount);

 

필드 옮기기 (Move Field)

배경

소프트웨어의 강력한 설계는 올바른 데이터 구조에서 출발한다.
좋은 데이터 구조는 코드를 단순하고 직관적으로 만든다. 반면, 부적절한 데이터 구조는 연관성이 낮은 데이터를 불필요하게 조합해야 하며, 결과적으로 코드가 복잡해진다.

경험과 도메인 지식이 쌓일수록, 처음에는 합리적이었던 데이터 구조가 프로젝트가 진행되며 부적절하다고 느껴질 수 있다.
이럴 때는 주저하지 말고 즉시 구조를 수정해야 한다.

예를 들어 어떤 필드가 함수 호출 때마다 항상 다른 객체와 함께 전달된다면, 그 필드는 아예 다른 객체로 옮기는 것이 더 자연스러울 수 있다.
"필드 옮기기" 리팩터링은 종종 더 큰 구조 변경의 출발점이 된다.

절차

  1. 필드 캡슐화
    • 직접 접근 중이라면 getter/setter를 도입한다.
  2. 기존 코드 테스트
  3. 타깃 객체에 새로운 필드 생성
    • 필요 시 getter/setter 포함.
  4. 정적 검사 수행
  5. 타깃 객체에 접근 가능한지 확인
  6. 기존 접근자들이 타깃 객체를 참조하도록 수정
  7. 테스트
  8. 기존 필드 제거
  9. 최종 테스트

예시

Before

// 클래스: Customer
class Customer {
  constructor(public name: string, public discountRate: number, public contract: CustomerContract) {}

  getDiscountRate(): number {
    return this.discountRate;
  }

  setDiscountRate(arg: number) {
    this.discountRate = arg;
  }
}

// 클래스: CustomerContract
class CustomerContract {
  constructor(public startDate: Date) {}
}

- 위 코드에서 discountRate는 CustomerContract와 강하게 연관되어 있으며, 실제 도메인상 계약 조건에 속해야 할 정보다. 이 필드를 CustomerContract로 옮기는 것이 더 자연스럽다.

 

 

 

필드 캡슐화

class Customer {
  constructor(public name: string, private _discountRate: number, public contract: CustomerContract) {}

  getDiscountRate(): number {
    return this._discountRate;
  }

  setDiscountRate(arg: number) {
    this._discountRate = arg;
  }
}
 

필드를 타깃 객체로 이동

class Customer {
  constructor(public name: string, public contract: CustomerContract) {}

  getDiscountRate(): number {
    return this.contract.getDiscountRate();
  }

  setDiscountRate(arg: number) {
    this.contract.setDiscountRate(arg);
  }
}

class CustomerContract {
  private _discountRate: number = 0;
  constructor(public startDate: Date) {}

  getDiscountRate(): number {
    return this._discountRate;
  }

  setDiscountRate(arg: number) {
    this._discountRate = arg;
  }
}
 

 

문장을 함수로 옮기기 (Move Statements into Function)

배경

중복 제거는 유지보수 가능한 코드를 만드는 데 핵심이다.
특정 함수를 호출할 때마다 반복적으로 앞뒤에 같은 문장이 붙는다면, 이 문장들을 함수 내부로 옮겨 중복을 제거할 수 있다.

이 리팩터링은 단순히 코드 줄 수를 줄이는 데 목적이 있는 것이 아니다.
함수의 책임 범위를 명확히 하고, 호출자 입장에서 코드 흐름을 더 단순하게 만든다는 점에서 중요하다.
단, 옮기려는 문장들이 해당 함수의 동작 일부라는 확신이 있을 때만 적용해야 한다.

절차

  1. 문장 이동
    • 반복되는 문장이 함수 호출과 멀리 떨어져 있다면, 먼저 문장 슬라이드하기로 가까이 옮긴다.
  2. 단일 호출자 처리
    • 타깃 함수 호출처가 하나뿐이라면, 해당 문장들을 잘라 함수 안으로 옮기고 테스트한다. 이 경우 다음 단계는 생략 가능하다.
  3. 여러 호출자 처리
    • 한 호출자에서 “기존 함수 호출 + 반복 문장”을 새 함수로 추출한다. 임시 이름을 붙인다.
  4. 다른 호출자들도 새 함수를 사용하도록 전환
    • 호출자마다 변경 후 테스트를 진행한다.
  5. 모든 호출자가 새 함수를 쓰면 기존 함수를 인라인하고 제거
  6. 함수 이름 정리
    • 새 함수 이름을 기존 함수 이름으로 교체한다.

예시

Before

function renderBanner(): void {
  console.log('***********************');
  console.log('***** 고객 로고 *****');
  console.log('***********************');
}

function renderHomepage(): void {
  renderBanner();
  console.log('환영합니다!');
}

function renderLogin(): void {
  renderBanner();
  console.log('로그인하세요.');
}

 

renderBanner 내부로 코드 이동

function renderBanner(): void {
  console.log('***********************');
  console.log('***** 고객 로고 *****');
  console.log('***********************');
}

function renderHomepage(): void {
  showBannerAndWelcome();
}

function renderLogin(): void {
  showBannerAndLogin();
}

function showBannerAndWelcome(): void {
  renderBanner();
  console.log('환영합니다!');
}

function showBannerAndLogin(): void {
  renderBanner();
  console.log('로그인하세요.');
}

 

중간 함수 인라인 + renderBanner 재작성

 
function renderBanner(message: string): void {
  console.log('***********************');
  console.log('***** 고객 로고 *****');
  console.log('***********************');
  console.log(message);
}

function renderHomepage(): void {
  renderBanner('환영합니다!');
}

function renderLogin(): void {
  renderBanner('로그인하세요.');
}

 

문장을 호출한 곳으로 옮기기 (Move Statements to Callers)

배경

추상화의 경계는 고정되지 않는다.
처음에는 잘 동작하던 추상화도, 시간이 지나면 더 이상 적절하지 않게 되는 경우가 많다.

예를 들어, 여러 곳에서 호출하던 함수가 이제는 일부 호출자에 맞게 다르게 동작해야 한다면, 더 이상 공통된 기능으로 묶어두는 것이 도움이 되지 않는다.
이럴 때는 오히려 공통 함수 내부의 특정 문장을 호출자 쪽으로 이동시키는 것이 더 유연한 구조가 된다.

이 리팩터링은 추상화 수준을 낮추고, 호출자가 더 많은 책임을 지게 만드는 구조 변화다.

절차

  1. 단순한 경우
    • 호출자가 한두 개고 함수가 단순하면, 이동할 문장을 호출자 쪽으로 옮긴다 (보통 첫 줄이나 마지막 줄). 테스트가 통과하면 종료.
  2. 복잡한 경우
    • 이동하지 않을 문장을 먼저 별도의 함수로 추출한다. 찾기 쉬운 임시 이름을 붙인다.
  3. 기존 함수를 인라인
    • 원래 함수를 호출자 쪽으로 펼친다.
  4. 추출한 함수 이름을 기존 함수 이름으로 변경

예시

Before

function renderBanner(): void {
  console.log('***************');
  console.log('**** 로고 ****');
  console.log('***************');
  console.log('환영합니다!');
}

function renderHomepage(): void {
  renderBanner();
}

function renderLogin(): void {
  renderBanner();
}
 

 

메시지 출력 코드 분리

function renderBannerOnly(): void {
  console.log('***************');
  console.log('**** 로고 ****');
  console.log('***************');
}

function renderHomepage(): void {
  renderBannerOnly();
  console.log('환영합니다!');
}

function renderLogin(): void {
  renderBannerOnly();
}
 

 

함수 이름 재정리

function renderBanner(): void {
  console.log('***************');
  console.log('**** 로고 ****');
  console.log('***************');
}

function renderHomepage(): void {
  renderBanner();
  console.log('환영합니다!');
}

function renderLogin(): void {
  renderBanner();
}

 

인라인 코드를 함수 호출로 바꾸기 (Replace Inline Code with Function Call)

배경

함수는 여러 동작을 목적에 따라 하나로 묶는 도구다.
함수를 잘 사용하면 코드의 의도가 명확해지고, 반복되는 코드를 줄이며, 변경에 강한 구조를 만들 수 있다.

이미 존재하는 함수와 동일한 동작을 하는 인라인 코드가 있다면, 이 코드를 함수 호출로 바꾸는 것이 일반적으로 더 낫다.
특히, 라이브러리 함수나 유틸 함수로 바꿀 수 있다면 더욱 좋다. 해당 함수가 잘 테스트되어 있고 유지보수도 쉬워지기 때문이다.

절차

  1. 인라인 코드가 수행하는 동작과 동일한 함수를 찾는다.
  2. 해당 코드를 함수 호출로 바꾼다.
  3. 테스트한다.

 

문장 슬라이드하기 (Slide Statements)

배경

관련된 코드는 가까이 있어야 이해하기 쉽다.
예컨대 특정 변수를 선언하고 사용하는 문장이 서로 멀리 떨어져 있다면, 코드를 읽는 사람은 해당 변수의 의미나 흐름을 파악하기 위해 자꾸 스크롤을 위아래로 움직여야 한다.

이럴 때 사용하는 것이 문장 슬라이드하기다.
슬라이드란 말 그대로 코드 조각을 조금 더 적절한 위치로 옮기는 것을 뜻한다.
특히 다음과 같은 경우에 유용하다.

  • 특정 데이터 구조를 다루는 문장들을 한데 모으고 싶을 때
  • 변수 선언을 사용하는 위치 근처로 옮기고 싶을 때
  • 함수 내 흐름을 더 논리적인 순서로 정리하고 싶을 때

주의할 점은 코드 이동 시 부수 효과(side effect)가 없는지 반드시 확인해야 한다는 것이다.
이 리팩터링은 코드의 실행 흐름을 바꾸지 않는 선에서 가독성을 높이는 데 목적이 있다.

절차

  1. 이동할 코드 조각과 목표 위치를 정한다.
  2. 그 사이에 있는 코드들을 훑어보며 슬라이드 가능한지 검토한다.
    • 참조/수정 관계를 건드리는 코드가 있다면 이동을 보류한다.
      • 슬라이드할 문장이 참조하는 변수의 선언 앞으로 이동할 수 없다.
      • 슬라이드할 문장을 참조하는 문장 뒤로 이동할 수 없다.
      • 슬라이드할 문장이 참조하는 값을 다른 코드가 수정한다면 그 코드를 건너뛸 수 없다.
      • 반대로 슬라이드할 문장이 수정하는 값을 참조하는 코드도 건너뛸 수 없다.
  3. 안전하다면 해당 코드 조각을 잘라내고, 목표 위치에 붙여넣는다.
  4. 테스트한다.

예시

Before

function calculateTotal(items: string[]): number {
  let total = 0;

  // 로깅
  console.log("계산 시작");

  for (const item of items) {
    const price = parseFloat(item.split('-')[1]);
    total += price;
  }

  // 로깅
  console.log("계산 종료");

  return total;
}

 

After

function calculateTotal(items: string[]): number {
  // 로깅
  console.log("계산 시작");

  let total = 0;

  for (const item of items) {
    const price = parseFloat(item.split('-')[1]);
    total += price;
  }

  // 로깅
  console.log("계산 종료");

  return total;
}

total 변수를 사용하는 시작점 바로 앞에 console.log("계산 시작")을 두었고, 코드 흐름도 훨씬 자연스럽게 정돈됐다.

 

반복문 쪼개기 (Split Loop)

배경

개발 현장에서 종종 하나의 반복문 안에서 여러 일을 처리하는 코드를 보게 된다.
예를 들어, 리스트를 돌면서 총합을 구하고, 동시에 최댓값을 구하는 식이다.

이렇게 하면 성능 면에서는 좋아 보일지 몰라도, 수정이나 이해는 어려워진다.
나중에 어떤 작업 하나를 변경해야 한다면, 그 반복문이 수행하는 모든 작업을 함께 이해해야 하기 때문이다.

반복문을 각 작업별로 쪼개면, 각각의 반복문이 명확한 목적을 가지게 된다.
읽기도 쉽고, 테스트도 용이하다.
그리고 “반복문을 두 번 돌리면 성능 문제가 생길 것 같아 걱정된다”는 생각은 대부분 기우에 가깝다.
성능 병목이 확인되기 전까진, 가독성과 유지보수성이 우선이다.

절차

  1. 반복문을 복제하여 두 개의 반복문을 만든다.
  2. 각 반복문이 맡은 작업만 수행하도록 코드에서 불필요한 부분을 제거한다.
  3. 테스트하여 두 반복문이 함께 원래의 동작을 유지하는지 확인한다.
  4. 필요하다면 각 반복문을 함수로 추출해 의미를 분명히 한다.

예시

Before

function analyzeTemperatures(temps: number[]): void {
  let total = 0;
  let maxTemp = Number.NEGATIVE_INFINITY;

  for (const temp of temps) {
    total += temp;
    if (temp > maxTemp) {
      maxTemp = temp;
    }
  }

  const average = total / temps.length;
  console.log(`평균 온도: ${average}`);
  console.log(`최고 온도: ${maxTemp}`);
}

 

After

 
function analyzeTemperatures(temps: number[]): void {
  let total = 0;
  for (const temp of temps) {
    total += temp;
  }

  let maxTemp = Number.NEGATIVE_INFINITY;
  for (const temp of temps) {
    if (temp > maxTemp) {
      maxTemp = temp;
    }
  }

  const average = total / temps.length;
  console.log(`평균 온도: ${average}`);
  console.log(`최고 온도: ${maxTemp}`);
}

 

반복문을 파이프라인으로 바꾸기 (Replace Loop with Pipeline)

배경

반복문을 사용하여 컬렉션을 처리할 때, 단계별로 연산을 수행하는 방식이 복잡하게 느껴질 수 있다.
반복문 내에서 여러 가지 작업이 이루어지면 코드를 이해하기 어려울 수 있는데, 이런 작업들을 파이프라인 형태로 바꾸면 코드 흐름이 훨씬 직관적으로 바뀐다.

컬렉션을 처리하는 연산을 map, filter, reduce와 같은 파이프라인 메서드로 바꾸면 코드의 가독성과 유지보수성이 크게 향상된다.
파이프라인은 데이터를 처리하는 일련의 연산들을 순차적으로 연결하는 방식이기 때문에 각 연산이 어떻게 수행되는지를 한 눈에 파악할 수 있다.

절차

  1. 반복문이 처리할 컬렉션을 가리키는 변수를 생성한다.
  2. 반복문 내의 각 단위 작업을 파이프라인 연산으로 변환한다.
    • map을 사용하여 요소를 변환하거나,
    • filter로 요소를 걸러내고,
    • reduce로 축약하거나.
    • 각 변환 후에는 테스트를 수행한다.
  3. 모든 반복문 동작을 파이프라인으로 변환했다면 반복문을 제거한다.

예시

Before

function processNumbers(nums: number[]): number {
  let total = 0;
  let maxNum = Number.NEGATIVE_INFINITY;

  for (const num of nums) {
    if (num > 10) {
      total += num;
    }
    if (num > maxNum) {
      maxNum = num;
    }
  }

  return total * maxNum;
}

 

After

function processNumbers(nums: number[]): number {
  const total = nums.filter(num => num > 10).reduce((sum, num) => sum + num, 0);
  const maxNum = Math.max(...nums);

  return total * maxNum;
}

 

죽은 코드 제거하기 (Remove Dead Code)

배경

소프트웨어 개발에서 사용되지 않는 코드는 프로그램의 이해를 방해하는 주요 원인 중 하나다.
죽은 코드(dead code)는 절대 호출되지 않거나 불필요한 로직을 포함하고 있기 때문에, 이를 그대로 두면 코드베이스가 점점 복잡해지고, 새로운 개발자가 코드를 이해하는 데 어려움을 겪게 된다.

죽은 코드는 기존의 기능을 유지하거나 확장하려는 노력을 방해할 뿐만 아니라, 불필요한 유지보수 비용을 초래할 수 있다. 그래서 사용되지 않는 코드는 과감히 제거해야 한다.
혹시라도 다시 필요해질까 봐 걱정할 수도 있지만, 불필요한 코드를 유지하는 것보다 실제로 필요한 코드를 추가하는 것이 더 효율적이다.

절차

  1. 죽은 코드가 참조되고 있는 곳이 있는지 확인한다.
    • 이를 위해 코드베이스를 정적 분석하거나 코드 커버리지 도구를 활용할 수 있다.
  2. 참조가 없다면, 해당 죽은 코드를 제거한다.

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

조건부 로직 간소화  (0) 2025.05.19
데이터 조직화  (0) 2025.05.15
캡슐화  (0) 2025.04.24
기본적인 리팩터링 (3)  (0) 2025.04.16
기본적인 리팩터링 (2)  (0) 2025.04.10
'Programming/Refactoring' 카테고리의 다른 글
  • 조건부 로직 간소화
  • 데이터 조직화
  • 캡슐화
  • 기본적인 리팩터링 (3)
Seung-o
Seung-o
공부한 것들을 정리하는 개발 블로그입니다.
  • Seung-o
    조그만 사람의 개발 주머니
    Seung-o
  • 전체
    오늘
    어제
    • 분류 전체보기 (52)
      • Database (20)
        • Real MySQL (4)
        • High Performance MySQL (14)
      • Programming (27)
        • Protocol (2)
        • Designing Data-Intensive Ap.. (5)
        • Unit Testing (4)
        • Refactoring (11)
        • Langchain (4)
      • Etc (5)
        • Thought (2)
        • Git (1)
        • Jira (1)
        • Experience (1)
  • 블로그 메뉴

    • 홈
    • 태그
  • 링크

    • Github
  • 인기 글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
Seung-o
기능 이동
상단으로

티스토리툴바