API 리팩터링

2025. 6. 4. 02:22·Programming/Refactoring

질의 함수와 변경 함수 분리하기

배경

우리는 외부에서 관찰할 수 있는 겉보기 부수효과(observable side effect) 가 없는 함수를 선호해야 한다.
이런 함수는 테스트가 쉽고, 호출 순서나 횟수에 구애받지 않으며, 다른 코드로 이동시키기도 편하다.

이 원칙을 잘 따르는 기준 중 하나가 "질의 함수는 부수효과가 없어야 한다"는 규칙이다.
즉, 값을 반환하는 함수는 내부 상태를 변경하지 않아야 한다.

반면 상태를 변경하는 함수는 아무 값도 반환하지 않도록 하여 의도를 명확히 해야 한다.
이렇게 역할을 명확히 나누면 코드를 더 쉽게 이해하고, 사용할 때 실수를 줄일 수 있다.

절차

  1. 기존 함수를 복사해, 반환값만 있는 질의 함수로 만든다.
  2. 질의 함수에서 부수효과 코드를 제거한다.
  3. 원래 함수를 호출하는 코드를 찾아서
    • 반환값을 사용하는 경우에는 질의 함수를 먼저 호출하고
    • 그 아래에 원래 함수를 호출하도록 순서를 분리한다.
  4. 원래 함수에서 반환값을 제거하고, 상태 변경만 수행하도록 수정한다.
  5. 중복된 로직이 있다면 정리한다.

예시

리팩터링 전

function alertForMiscreant(people) {
  for (const p of people) {
    if (p === "Don") {
      setOffAlarms();
      return "Don";
    }
    if (p === "John") {
      setOffAlarms();
      return "John";
    }
  }
  return "";
}

const found = alertForMiscreant(people);

위 코드는 "Don" 또는 "John"이 있으면 알람을 울리고 이름을 반환하는데,
이 함수는 값을 반환함과 동시에 부수효과(알람 울리기) 도 일으키므로 질의와 변경이 섞여 있다.

 

리팩터링 후

function findMiscreant(people) {
  for (const p of people) {
    if (p === "Don" || p === "John") return p;
  }
  return "";
}

function alertForMiscreant(people) {
  if (findMiscreant(people) !== "") {
    setOffAlarms();
  }
}

const found = findMiscreant(people);
alertForMiscreant(people);
  • findMiscreant: 오직 값을 반환하는 질의 함수
  • alertForMiscreant: 오직 알람을 울리는 변경 함수
  • 호출부에서는 두 함수를 명확히 분리하여 순서대로 사용한다

 

함수 매개변수화하기

배경

두 함수가 거의 동일한 로직을 가지며, 단지 리터럴 값만 다를 때 이 값들을 매개변수로 추출해 하나의 함수로 합칠 수 있다.
이렇게 하면 중복을 제거하고, 함수의 활용 범위를 넓힐 수 있다.

또한, 매개변수를 통해 함수 동작을 유연하게 조절할 수 있으므로 재사용성과 테스트 효율성도 높아진다.

절차

  1. 비슷한 함수 중 하나를 기준으로 삼는다.
  2. 함수 선언을 바꿔서 서로 다른 값을 매개변수로 전달받도록 수정한다.
  3. 함수 내부에서 리터럴을 매개변수로 대체한다.
  4. 나머지 비슷한 함수들은 해당 함수 호출로 대체한다.
  5. 테스트하면서 함수 통합이 잘 이뤄졌는지 확인한다.

예시

리팩터링 전

function tenPercentRaise(person) {
  person.salary = person.salary * 1.1;
}

function fivePercentRaise(person) {
  person.salary = person.salary * 1.05;
}

두 함수는 본질적으로 같은 일을 한다. 다만 인상률만 다를 뿐이다.

리팩터링 후

function raise(person, factor) {
  person.salary = person.salary * (1 + factor);
}

이제 인상률을 인자로 받아 처리하는 하나의 함수로 통합되었다.

raise(someEmployee, 0.10);  // 기존의 tenPercentRaise
raise(anotherEmployee, 0.05);  // 기존의 fivePercentRaise

 

플래그 인수 제거하기

배경

플래그 인수(flag argument) 는 호출하는 쪽에서 함수의 동작을 제어하기 위해 전달하는 인수다.
예를 들어 true나 "premium" 같은 값을 전달해 어떤 조건 분기를 실행할지 선택하는 방식이다.

이런 방식은 다음과 같은 문제를 만든다.

  • 함수 목록만 보고는 어떤 작업을 할 수 있는지 알기 어렵다.
  • true, false 같은 값만 보면 무슨 의미인지 파악하기 어렵다.
  • 문서나 내부 구현을 뜯어봐야 의미를 알 수 있어 가독성과 유지보수성이 떨어진다.

플래그 인수를 제거하면 코드가 더 명확해지고, 자동완성이나 정적 분석 도구의 도움도 더 잘 받을 수 있다.
플래그가 여러 개 들어가는 함수라면, 이는 그 함수가 너무 많은 일을 하고 있다는 신호일 수 있다.

절차

  1. 플래그 값마다 수행하는 동작이 명확히 나뉘는지 확인한다.
  2. 플래그 값별로 명시적인 함수를 만든다.
  3. 기존 함수를 각 명시적 함수에서 호출하거나, 분기 로직을 각 함수로 나눈다.
  4. 호출 코드를 새 함수로 교체한다.
  5. 기존 함수는 제거하거나, 내부에서만 사용되게 숨긴다.

예시

리팩터링 전

function deliveryDate(order, isRush) {
  if (isRush) {
    if (["MA", "CT"].includes(order.deliveryState)) return order.placedOn.plusDays(2);
    else return order.placedOn.plusDays(3);
  } else {
    if (["MA", "CT", "NY"].includes(order.deliveryState)) return order.placedOn.plusDays(4);
    else return order.placedOn.plusDays(5);
  }
}

// 호출부
deliveryDate(anOrder, true);   // 긴급 배송
deliveryDate(anOrder, false);  // 일반 배송
  • deliveryDate는 isRush 플래그에 따라 서로 다른 로직을 수행하고 있다.
  • true가 무슨 의미인지 한눈에 보이지 않으며, 함수 내부 분기가 복잡하다.

 

리팩터링 후

function rushDeliveryDate(order) {
  if (["MA", "CT"].includes(order.deliveryState)) return order.placedOn.plusDays(2);
  else return order.placedOn.plusDays(3);
}

function regularDeliveryDate(order) {
  if (["MA", "CT", "NY"].includes(order.deliveryState)) return order.placedOn.plusDays(4);
  else return order.placedOn.plusDays(5);
}

// 호출부
rushDeliveryDate(anOrder);
regularDeliveryDate(anOrder);
  • 플래그가 사라지고, 명확한 함수 이름으로 의도가 드러난다.
  • 각 함수는 하나의 목적만 수행하므로 단순하고 읽기 쉬워졌다.

 

객체 통째로 넘기기

배경

코드를 살펴보다 보면 어떤 함수가 객체에서 일부 값만 뽑아 인수로 전달받는 경우가 있다.
이럴 땐 필요한 값만 추출해서 넘기기보다, 객체 자체를 통째로 넘기는 게 더 나은 선택일 수 있다.

그 이유는 다음과 같다:

  • 나중에 더 많은 필드가 필요해져도 함수 시그니처를 바꿀 필요가 없다.
  • 매개변수 개수가 줄어들어 함수 호출이 더 깔끔해진다.
  • 관련된 데이터가 여러 함수에서 동일한 방식으로 처리될 경우, 중복 로직을 제거하기 쉬워진다.

하지만 이 리팩터링을 무조건 적용해서는 안 된다.

  • 호출 대상과 호출자가 서로 다른 모듈에 속해 있다면, 불필요한 의존성이 생긴다.
  • 어떤 객체에서 값만 가져와 다른 곳에서 처리하는 로직이라면, 그 로직 자체를 객체 안으로 옮겨야 할지도 모른다. (Feature Envy)

절차

  1. 기존 함수에 전체 객체를 인수로 받는 새로운 함수를 만든다.
  2. 기존 함수 내부에서는 새 함수로 위임하도록 구성한다.
  3. 호출 코드를 하나씩 새 함수로 변경한다.
  4. 더 이상 쓰이지 않는 기존 함수는 제거한다.
  5. 이름에서 prefix를 제거하고 정리한다.

예시

리팩터링 전

// 호출부
const low = room.temperatureRange.low;
const high = room.temperatureRange.high;
if (!plan.withinRange(low, high)) {
  alerts.push("온도가 범위를 벗어남");
}

// HeatingPlan 클래스
withinRange(low, high) {
  return low >= this._range.low && high <= this._range.high;
}
  • withinRange는 두 개의 숫자 값을 받는다.
  • 호출부에서는 객체에서 값을 꺼내는 코드가 반복된다.

리팩터링 후

// HeatingPlan 클래스
withinRange(aRange) {
  return aRange.low >= this._range.low && aRange.high <= this._range.high;
}

// 호출부
if (!plan.withinRange(room.temperatureRange)) {
  alerts.push("온도가 범위를 벗어남");
}
  • 호출부에서 불필요한 중간 변수와 해체 작업이 사라졌다.
  • withinRange는 이제 하나의 객체를 받아, 필요한 정보를 내부에서 처리한다.

 

매개변수를 질의 함수로 바꾸기

배경

함수를 호출할 때, 피호출 함수가 스스로 계산할 수 있는 값을 굳이 매개변수로 전달하는 경우가 있다.
이런 방식은 호출자가 불필요한 결정을 떠안게 되고, 결과적으로 코드가 더 복잡해진다.

이럴 땐 매개변수를 함수 내부에서 직접 질의(query)하도록 변경하는 것이 좋다.
책임을 호출자에서 피호출 함수로 넘기는 방식이다.

하지만 이 리팩터링을 항상 적용하면 안 된다.

  • 질의하려는 값이 외부 모듈에 대한 의존성을 유발하거나
  • 글로벌 상태나 수용 객체(receiver object) 에 의존하게 된다면, 오히려 결합도가 높아질 수 있다.

즉, 이 리팩터링은 피호출 함수가 해당 정보를 자연스럽게 알 수 있을 때만 사용해야 한다.

절차

  1. 함수 내부에서 매개변수로 받았던 값을 직접 구하는 질의 코드를 작성한다.
  2. 기존 매개변수 대신 해당 질의 코드를 사용하도록 수정한다.
  3. 호출부에서 매개변수를 제거한다.
  4. 테스트하여 기존 동작이 잘 유지되는지 확인한다.

예시

리팩터링 전

class Order {
  finalPrice() {
    const basePrice = this.quantity * this.itemPrice;
    const discountLevel = this.quantity > 100 ? 2 : 1;
    return this.discountedPrice(basePrice, discountLevel);
  }

  discountedPrice(basePrice, discountLevel) {
    switch (discountLevel) {
      case 1: return basePrice * 0.95;
      case 2: return basePrice * 0.9;
    }
  }
}
  • discountedPrice 함수는 discountLevel을 매개변수로 받지만
    이 값은 Order 객체의 상태로부터 계산할 수 있다.
  • 따라서, 굳이 매개변수로 받을 필요가 없다.

리팩터링 후

class Order {
  finalPrice() {
    const basePrice = this.quantity * this.itemPrice;
    return this.discountedPrice(basePrice);
  }

  get discountLevel() {
    return this.quantity > 100 ? 2 : 1;
  }

  discountedPrice(basePrice) {
    switch (this.discountLevel) {
      case 1: return basePrice * 0.95;
      case 2: return basePrice * 0.9;
    }
  }
}
  • discountedPrice는 이제 스스로 discountLevel을 구한다.
  • 호출부가 더 간결해졌고, 로직의 책임도 Order 클래스 내부에 자연스럽게 위치하게 되었다.

 

질의 함수를 매개변수로 바꾸기

배경

함수 내부에서 전역 변수나 다른 모듈의 요소를 직접 참조하고 있다면, 해당 참조가 불편한 의존성으로 느껴질 수 있다.
이런 경우엔 참조를 함수 내부에 두는 대신, 필요한 값을 매개변수로 받아오도록 바꾸는 것이 좋다.

이렇게 하면 다음과 같은 이점이 생긴다:

  • 함수의 결합도를 낮추고, 독립성을 높일 수 있다.
  • 테스트가 쉬워지고, 모듈화가 더 쉬워진다.
  • 전체 프로그램의 의존성 그래프가 더 단순해진다.

이 리팩터링은 특히 참조 대상이 변경될 가능성이 높거나,
함수가 속한 모듈을 다른 곳으로 옮기고자 할 때 유용하다.

절차

  1. 함수 내에서 외부 값을 참조하는 질의 부분을 변수로 추출한다.
  2. 그 변수 값을 매개변수로 전달받도록 함수 시그니처를 수정한다.
  3. 호출부에서 해당 값을 미리 계산해 전달한다.
  4. 테스트하여 동일한 동작이 유지되는지 확인한다.

예시

리팩터링 전

// 전역 thermostat 객체를 참조
class HeatingPlan {
  get targetTemperature() {
    if (thermostat.selectedTemperature > this._max) return this._max;
    else if (thermostat.selectedTemperature < this._min) return this._min;
    else return thermostat.selectedTemperature;
  }
}
  • HeatingPlan은 전역 객체 thermostat에 직접 의존하고 있다.
  • 이는 테스트와 재사용에 큰 제약을 만든다.

리팩터링 후

class HeatingPlan {
  targetTemperature(selectedTemperature) {
    if (selectedTemperature > this._max) return this._max;
    else if (selectedTemperature < this._min) return this._min;
    else return selectedTemperature;
  }
}

// 호출부
const selectedTemp = thermostat.selectedTemperature;
plan.targetTemperature(selectedTemp);
  • 전역 참조를 제거하고, 값을 매개변수로 전달하게 바뀌었다.
  • HeatingPlan은 이제 thermostat 객체와 독립적인 코드가 되었다.

 

세터 제거하기

배경

객체의 필드에 대해 세터(setter) 를 제공한다는 것은 해당 값이 객체 생성 이후에도 바뀔 수 있음을 뜻한다.
하지만 변경되어서는 안 되는 값이라면, 굳이 세터를 제공할 필요가 없다.

이 리팩터링은 다음과 같은 상황에서 유용하다:

  1. 생성자 안에서조차 세터를 사용하는 경우
    • 습관적으로 모든 필드에 접근자 메서드를 정의하면,
      생성자 내부에서만 쓰이는 불필요한 세터가 생길 수 있다.
    • 이 경우엔 세터를 제거하고, 생성자에서 직접 필드를 초기화하는 것이 더 명확하다.
  2. 생성 스크립트 방식으로 객체를 초기화하는 경우
    • new로 객체를 만든 뒤 여러 세터로 값을 설정하는 방식이다.
    • 이런 값들이 이후에는 절대 변경되지 않기를 기대한다면, 세터를 제거하고 생성자에서 인자로 받도록 변경하자.
    • 객체의 불변성을 명확히 하여, 코드의 의도와 안정성을 높일 수 있다.

절차

  1. 해당 필드를 생성자에서 초기화하도록 변경한다.
  2. 세터를 사용하는 코드를 찾아 생성자 인수로 변경한다.
  3. 세터 메서드를 제거하고 필드를 불변으로 선언한다 (가능한 경우).
  4. 테스트를 수행해 정상 동작을 확인한다.

예시

리팩터링 전

class Person {
  set name(arg) { this._name = arg; }
  get name() { return this._name; }

  set id(arg) { this._id = arg; }
  get id() { return this._id; }
}

// 생성 스크립트
const person = new Person();
person.name = "Alice";
person.id = "1234";
  • id는 한 번 정해지면 변경되어선 안 되는 값이다.
  • 그러나 세터가 열려 있으므로, 외부에서 마음대로 바꿀 수 있다.

리팩터링 후

class Person {
  constructor(id) {
    this._id = id;
  }

  set name(arg) { this._name = arg; }
  get name() { return this._name; }

  get id() { return this._id; }
}

// 생성 방식 변경
const person = new Person("1234");
person.name = "Alice";
  • id는 생성자에서만 설정되며, 그 외엔 절대 변경할 수 없다.
  • 세터를 제거함으로써 의도가 명확히 드러난다: 이 값은 고정되어야 한다.

 

생성자를 팩터리 함수로 바꾸기

배경

생성자는 객체를 만들 때 사용하는 기본적인 방법이지만, 제약사항이 많다.

예를 들어 Java에서 생성자는 반드시 자기 자신 클래스의 인스턴스를 반환해야 한다.
또는 JavaScript에서는 생성자를 호출할 때 반드시 new 키워드를 사용해야 하고,
생성자 이름은 클래스 이름과 동일해야 하기 때문에 표현력을 제한받는다.

팩터리 함수(factory function) 는 이런 제약을 벗어난, 더 유연하고 강력한 객체 생성 방식이다.

  • 명확한 함수명으로 의도를 드러낼 수 있다. (createEngineer, newRegisteredUser, ...)
  • 서브클래스를 반환하거나 프록시 객체, 캐시된 인스턴스 등을 반환하는 로직도 포함할 수 있다.
  • 조건에 따라 다양한 객체를 리턴할 수 있어 테스트 더블 주입이나 전략 패턴 구현에도 유리하다.

절차

  1. 생성자와 동일한 인자를 받는 팩터리 함수를 만든다.
  2. 팩터리 함수 안에서 생성자를 호출해 객체를 만든다.
  3. 기존 생성자 호출을 팩터리 함수 호출로 교체한다.
  4. 모든 호출이 바뀌면 생성자의 외부 노출을 제한하거나 제거한다.
  5. 필요하다면 팩터리 함수 내부에서 다른 객체를 반환하거나 생성 로직을 커스터마이즈한다.

예시

리팩터링 전

class Employee {
  constructor(name, typeCode) {
    this._name = name;
    this._typeCode = typeCode;
  }

  get name() { return this._name; }
  get type() {
    return Employee.legalTypeCodes[this._typeCode];
  }

  static get legalTypeCodes() {
    return { "E": "Engineer", "M": "Manager", "S": "Sales" };
  }
}

// 호출부
const leadEng = new Employee("Jane", "E");

 

리팩터링 후

function createEmployee(name, typeCode) {
  return new Employee(name, typeCode);
}

// 기존 생성자 호출 → 팩터리 함수로 교체
const leadEng = createEmployee("Jane", "E");

 

더 발전된 활용 (조건 분기 예시)

function createEmployee(name, typeCode) {
  switch (typeCode) {
    case "E": return new Engineer(name);
    case "M": return new Manager(name);
    default: return new Employee(name, typeCode);
  }
}

 

함수를 명령으로 바꾸기

배경

특정 함수가 너무 복잡하거나,
그 함수의 실행을 더 유연하게 제어하거나 확장할 필요가 있을 때가 있다.

이럴 땐 함수를 명령(Command) 객체로 바꾸는 것이 좋은 해결책이 된다.

  • 명령은 보통 객체 하나에 execute() 같은 단일 메서드로 구성된다.
  • 하지만 필요하다면 상태 저장, 보조 메서드 추가, 로깅, 취소/재시도 등도 가능하다.
  • 특히 데이터가 많고, 처리 로직도 여러 단계로 복잡한 경우, 명령 객체는 캡슐화에 유리하다.

다만, 단순한 함수를 괜히 명령으로 만들면 오히려 복잡성만 증가하니
함수의 구조가 커지거나, 커질 가능성이 있는 경우에만 적용하는 것이 바람직하다.

절차

  1. 기존 함수를 위한 새 클래스를 만든다.
  2. 원래 함수의 매개변수들을 필드로 캡슐화한다.
  3. 원래 함수의 본문을 execute() 메서드로 옮긴다.
  4. 호출부에서 함수 호출을 명령 객체 생성 + execute() 호출로 바꾼다.
  5. 테스트하여 기존과 동일하게 동작하는지 확인한다.

예시

리팩터링 전

function score(candidate, medicalExam, scoringGuide) {
  let result = 0;
  if (medicalExam.isSmoker) result -= 5;
  let certificationGrade = scoringGuide.certification(candidate);
  if (certificationGrade === "A") result += 10;
  return result;
}
  • 간단해 보이지만, 로직이 더 복잡해지면 관리가 어려워질 수 있다.
  • 예를 들어 로그 기록, 추가 필드 계산, 동적 조건 분기가 생긴다면…?

리팩터링 후

class Scorer {
  constructor(candidate, medicalExam, scoringGuide) {
    this.candidate = candidate;
    this.medicalExam = medicalExam;
    this.scoringGuide = scoringGuide;
  }

  execute() {
    let result = 0;
    if (this.medicalExam.isSmoker) result -= 5;
    let grade = this.scoringGuide.certification(this.candidate);
    if (grade === "A") result += 10;
    return result;
  }
}

// 호출부
const score = new Scorer(candidate, medicalExam, scoringGuide).execute();
  • 함수는 Scorer라는 명령 객체로 대체되었다.
  • 이제 이 객체는 필요하다면 다음도 수행할 수 있다:
    • logResult(), buildReport(), validate() 같은 보조 메서드 추가
    • 내부 상태 관리
    • 테스트 더블 삽입 등 유연한 확장

 

명령을 함수로 바꾸기

배경

명령(Command) 객체는 복잡한 프로세스를 분리하거나, 다양한 상태를 다뤄야 하는 상황에서 유용하게 쓰인다. 하지만, 단순히 데이터를 받아 정해진 작업을 수행하고 결과만 반환하는 경우라면, 명령 객체를 굳이 쓸 필요는 없다.

명령 객체는 캡슐화된 구조와 상태를 갖추는 데에 비용이 들며, 코드 구조를 더 복잡하게 만들 수 있다. 로직이 간단하고 상태를 다룰 필요가 없다면, 해당 명령을 일반 함수로 대체하는 것이 낫다.

절차

  1. 명령 객체의 execute() 또는 run() 메서드가 하는 일을 파악한다.
  2. 명령 객체의 필드를 함수의 매개변수로 옮긴다.
  3. 메서드 본문을 새 함수로 추출하고, 그 함수를 명령 객체가 사용되던 자리에서 호출한다.
  4. 기존 명령 클래스를 제거한다.

예시

다음은 배송비를 계산하는 명령 객체이다.

class ChargeCalculator {
  constructor(customer, usage, provider) {
    this._customer = customer;
    this._usage = usage;
    this._provider = provider;
  }

  execute() {
    const baseCharge = this._customer.rate * this._usage;
    const providerCharge = this._provider.connectionCharge;
    return baseCharge + providerCharge;
  }
}

간단한 계산만 수행하는 이 객체는 상태를 저장하거나 재사용할 필요가 없다. 함수로 바꾸면 다음과 같이 단순화된다.

function charge(customer, usage, provider) {
  const baseCharge = customer.rate * usage;
  const providerCharge = provider.connectionCharge;
  return baseCharge + providerCharge;
}

 

이제는 다음처럼 간단히 사용할 수 있다.

const total = charge(aCustomer, usage, provider);

 

수정된 값 반환하기

배경

코드에서 데이터가 언제, 어떻게 수정되는지를 파악하는 일은 많은 개발자에게 어려움을 준다. 특히 함수가 내부에서 값을 변경해버리고 아무것도 반환하지 않는다면, 호출자는 코드 흐름을 추적해야만 해당 값이 바뀌었는지를 알 수 있다.

이럴 때 변경된 값을 명시적으로 반환하게 만들면, 변수의 갱신이 코드에 드러나며 추론이 쉬워진다. 호출자도 값을 명시적으로 다시 할당해야 하기 때문에, 그 순간 변수가 갱신될 것이라는 점을 인식하게 된다.

즉, 함수는 명확하게 "값을 바꾼다"는 신호를 반환값으로 전달해야 한다.

절차

  1. 변수의 값을 수정하는 함수가 있다면, 해당 함수에서 수정된 값을 반환하게 만든다.
  2. 호출자 쪽 코드를 찾아, 해당 반환값을 명시적으로 변수에 다시 할당하도록 수정한다.
  3. 테스트를 수행해 기존 동작이 유지되는지 확인한다.

예시

다음은 고객 포인트를 누적하는 함수이다. 이 함수는 고객 객체의 내부 상태만 바꾸고, 반환값이 없다.

function accumulatePoints(customer, points) {
  customer.points += points;
}

 

호출자는 이렇게 사용한다.

accumulatePoints(aCustomer, 100);

 

이제 함수를 수정하여 변경된 값을 반환하게 만든다.

function accumulatePoints(customer, points) {
  customer.points += points;
  return customer.points;
}

 

그리고 호출자에서는 반환값을 명시적으로 할당하도록 바꾼다.

aCustomer.points = accumulatePoints(aCustomer, 100);

 

이제 이 코드를 읽는 사람은 aCustomer.points가 갱신됨을 명확히 알 수 있다.

 

오류 코드를 예외로 바꾸기

배경

과거에는 오류 상황을 숫자 코드나 특수한 리턴 값으로 나타내고, 호출자가 그 값을 일일이 확인하는 방식이 일반적이었다. 하지만 이러한 방식은 실수하기 쉽고, 코드의 명확성을 떨어뜨린다.

반면 예외는 언어 차원에서 제공하는 정형화된 오류 처리 메커니즘으로, 예외 상황을 호출자에게 자연스럽게 전달할 수 있다. 예외는 정상 흐름에서 벗어나는 상황에만 사용해야 하며, 일반적인 조건 분기에는 쓰지 않는 것이 원칙이다.

정상적인 처리 흐름에서는 오류 코드를 사용하지 말고, 예외를 통해 의미 있는 오류 시점을 명확히 전달하도록 리팩터링하는 것이 좋다.

절차

  1. 오류를 반환하는 코드를 찾아 예외를 던지도록 수정한다.
  2. 오류 코드를 반환받아 분기 처리하던 호출자 코드를 try-catch로 감싼다.
  3. catch 블록에서 기존 오류 코드에 따른 처리 로직을 옮긴다.
  4. 테스트를 수행하여 정상 동작을 검증한다.

예시

다음 함수는 재고 수량이 부족한 경우 -1을 반환한다.

function withdrawInventory(product, quantity) {
  if (product.stock < quantity) return -1;
  product.stock -= quantity;
  return 0;
}

 

호출자에서는 오류 코드를 직접 검사하고 있다.

const result = withdrawInventory(item, 5);
if (result === -1) {
  console.log("재고 부족");
}

 

이제 오류 코드를 예외로 바꾼다.

function withdrawInventory(product, quantity) {
  if (product.stock < quantity) {
    throw new Error("재고 부족");
  }
  product.stock -= quantity;
}

 

호출자는 try-catch 문으로 예외를 처리한다.

try {
  withdrawInventory(item, 5);
} catch (e) {
  console.log(e.message); // "재고 부족"
}
 

 

예외를 사전확인으로 바꾸기 

배경

예외는 정상적인 흐름을 방해하면서 오류 상황을 처리하는 강력한 도구다. 하지만 모든 오류를 예외로 처리하는 것은 바람직하지 않다. 특히, 호출 전에 충분히 사전 조건을 검사할 수 있는 경우라면, 예외를 던지기보다는 조건 검사를 통해 예방하는 것이 좋다.

예외는 예상치 못한 상황을 다룰 때 사용해야 하며, 예측 가능한 조건에 대해서는 if 문과 같은 사전확인(precheck)으로 처리하는 것이 코드의 가독성과 안정성 면에서 더 낫다.

예외를 줄이면 코드의 흐름이 단순해지고, 성능상 이점도 얻을 수 있다.

절차

  1. 예외를 던지는 함수에서, 예외 발생 조건을 파악한다.
  2. 해당 조건을 호출자 쪽에서 사전 확인할 수 있는지 판단한다.
  3. 호출자 코드에 사전확인 로직을 추가하고, 예외 발생을 방지한다.
  4. 더 이상 필요 없는 예외 던지기를 제거하거나, 최소한으로 유지한다.
  5. 테스트하여 정상 동작을 확인한다.

예시

아래 함수는 0으로 나누는 경우 예외를 던진다.

function divide(a, b) {
  if (b === 0) throw new Error("0으로 나눌 수 없습니다.");
  return a / b;
}

 

호출자는 예외를 감싸고 있다.

try {
  const result = divide(10, userInput);
  console.log(result);
} catch (e) {
  console.log(e.message);
}

 

예외 대신, 사전에 확인할 수 있는 조건이므로 호출자에서 검사하는 방식으로 수정한다.

if (userInput !== 0) {
  const result = divide(10, userInput);
  console.log(result);
} else {
  console.log("0으로 나눌 수 없습니다.");
}

 

이때 divide 함수는 더 이상 예외를 던질 필요가 없다.

function divide(a, b) {
  return a / b;
}

 

 

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

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

    • 홈
    • 태그
  • 링크

    • Github
  • 인기 글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
Seung-o
API 리팩터링
상단으로

티스토리툴바