Chapter 11

API 리팩터링

  • 11.1 질의 함수와 변경 함수 분리하기
  • 11.2 함수 매개변수화하기
  • 11.3 플래그 인수 제거하기
  • 11.4 객체 통째로 넘기기
  • 11.5 매개변수를 질의 함수로 바꾸기
  • 11.6 질의 함수를 매개변수로 바꾸기
  • 11.7 세터 제거하기
  • 11.8 생성자를 팩터리 함수로 바꾸기
  • 11.9 함수를 명령으로 바꾸기
  • 11.10 명령을 함수로 바꾸기
  • 11.11 수정된 값 반환하기
  • 11.12 오류 코드를 예외로 바꾸기
  • 11.13 예외를 사전확인으로 바꾸기

좋은 API는 데이터를 갱신하는 함수와 조회하는 함수를 구분하고, 필요한 것만 매개변수로 받으며, 호출하는 쪽이 편하게 설계되어 있어. 11장은 모듈과 모듈 사이의 경계, 즉 API를 다루는 리팩터링이야.

**질의 함수와 변경 함수 분리하기(Separate Query from Modifier)**가 첫 번째야. "부수 효과가 있는 함수와 값을 반환하는 함수를 분리하라." 이게 명령-질의 분리(CQS) 원칙이거든. 겉보기에는 값만 반환하는 것 같은데 내부에서 상태를 변경하고 있으면, 호출하는 쪽에서 예상하지 못한 부작용이 생겨. 질의 함수는 부수 효과가 없어야 해. 그래야 아무 걱정 없이 호출할 수 있고, 호출 순서를 바꾸거나 테스트하기도 쉬워지지.

**함수 매개변수화하기(Parameterize Function)**는 비슷한 로직의 함수가 여러 개 있는데 리터럴 값만 다른 경우, 그 값을 매개변수로 받는 하나의 함수로 합치는 거야.

// Before
function tenPercentRaise(aPerson) { aPerson.salary = aPerson.salary.multiply(1.1); }
function fivePercentRaise(aPerson) { aPerson.salary = aPerson.salary.multiply(1.05); }

// After
function raise(aPerson, factor) { aPerson.salary = aPerson.salary.multiply(1 + factor); }

**플래그 인수 제거하기(Remove Flag Argument)**는 불리언이나 열거형을 매개변수로 받아서 내부에서 분기하는 경우, 명시적인 함수로 분리하는 거야. 플래그 인수가 나쁜 이유는, 호출하는 쪽에서 bookConcert(customer, true)를 보고 true가 뭘 의미하는지 알 수 없기 때문이야. 별도 함수로 만드는 게 의도가 명확해. 다만 함수 내부 로직이 거의 동일하고 분기가 아주 작은 부분에서만 일어나면 굳이 쪼개지 않아도 돼.

**객체 통째로 넘기기(Preserve Whole Object)**는 객체에서 여러 값을 꺼내서 함수에 전달하는 대신 객체 자체를 전달하는 거야.

// Before
const low = aRoom.daysTempRange.low;
const high = aRoom.daysTempRange.high;
if (aPlan.withinRange(low, high)) ...

// After
if (aPlan.withinRange(aRoom.daysTempRange)) ...

매개변수 목록이 짧아지고, 나중에 함수가 다른 값도 필요해지면 시그니처를 바꾸지 않아도 돼. 다만 함수가 해당 객체에 의존하게 되니까, 의존성을 만들고 싶지 않다면 하지 않는 게 좋아.

**매개변수를 질의 함수로 바꾸기(Replace Parameter with Query)**는 매개변수로 전달되는 값을 함수 내부에서 직접 알아낼 수 있다면, 매개변수를 제거하는 거야. "매개변수가 있으면 호출할 때마다 적절한 값을 결정해야 하는 건 호출자다." 그 책임을 함수 안으로 옮기면 호출자가 편해져. 반대로 **질의 함수를 매개변수로 바꾸기(Replace Query with Parameter)**는 함수가 외부 의존성 없이 매개변수만으로 동작하게 만들고 싶을 때 적용해. "참조 투명한 함수는 다루기가 훨씬 쉽다." 이 둘은 **"모듈을 참조 투명하게 만들어서 얻는 장점"**과 "호출자가 복잡해지는 비용" 사이의 트레이드오프야.

**세터 제거하기(Remove Setting Method)**는 객체가 생성된 후에 값이 바뀌면 안 되는 필드에서 세터를 제거하는 거야. 세터가 있으면 **"이 필드는 바뀔 수 있다"**는 메시지를 보내거든. 바뀌면 안 되는 필드라면 생성자에서만 설정하고, 이후에는 읽기만 가능하게 만들어.

**생성자를 팩터리 함수로 바꾸기(Replace Constructor with Factory Function)**는 생성자의 제약(이름 변경 불가, 서브클래스 반환 불가, new 연산자 필수)을 피하고 싶을 때 적용해.

// Before
const leadEngineer = new Employee(document.leadEngineer, 'E');

// After
const leadEngineer = createEngineer(document.leadEngineer);

의미 있는 이름을 붙일 수 있고, 상황에 따라 다른 타입의 객체를 반환할 수 있지.

**함수를 명령으로 바꾸기(Replace Function with Command)**는 함수를 독자적인 명령 객체로 만드는 거야. 복잡한 함수를 여러 메서드로 쪼갤 수 있고, undo 기능을 구현할 수도 있어. 하지만 "함수로 충분한데 명령 객체를 만들면 과잉 엔지니어링이다." 반대로 명령을 함수로 바꾸기는 명령 객체가 하는 일이 단순하면 일반 함수로 되돌리는 거야.

**수정된 값 반환하기(Return Modified Value)**는 함수가 내부에서 매개변수를 변경하는 대신 변경된 값을 반환하게 바꾸는 거야. 데이터가 어디서 갱신되는지 추적하기 쉬워지고, const로 선언할 수도 있어.

// Before
let totalAscent = 0;
calculateAscent();  // totalAscent를 내부에서 변경

// After
const totalAscent = calculateAscent();  // 값을 반환

**오류 코드를 예외로 바꾸기(Replace Error Code with Exception)**는 오류 상황에서 특수한 값(-1, null 등)을 반환하는 대신 예외를 던지는 거야. **"정상 흐름과 오류 처리 흐름을 분리"**할 수 있거든. 다만 예외는 **"예상 밖의 동작"**에만 사용해야 해. 반대로 **예외를 사전확인으로 바꾸기(Replace Exception with Precheck)**는 예외가 호출 전에 검사할 수 있는 조건에 의해 발생한다면, 사전 검사로 바꾸는 거야. 정상적인 제어 흐름에 예외를 쓰면 코드를 읽는 사람이 오해하거든.


정리

11장 읽고 기억할 거 세 가지:

  1. "질의와 변경을 분리하라." 부수 효과 없는 질의 함수는 안심하고 호출할 수 있어
  2. "플래그 인수는 함수의 의도를 감춘다." 명시적인 함수로 분리하면 호출하는 쪽이 명확해져
  3. "예외는 예외적인 상황에만 써라." 예상 가능한 조건은 사전 검사로 처리해