해당 내용은 위의 책의 내용을 제가 이해한대로 정리해둔 내용입니다.
이번 포스팅에선 Chapter11 의 내용에 대해 다룹니다.
- Chapter11 API 리팩터링
11장에서 소개하는 주요 리팩터링 기법들을 요약하고 예제와 함께 정리했습니다.
Chapter11 API 리팩터링
1.질의 함수와 변경 함수 분리하기 |
2.함수 매개변수화하기 |
3.플래그 인수 제거하기 |
4.객체 통째로 넘기기 |
5.매개변수를 질의 함수로 바꾸기 |
6.질의 함수를 매개변수로 바꾸기 |
7.세터 제거하기 |
8.생성자를 팩터리 함수로 바꾸기 |
9.함수를 명령으로 바꾸기 |
10.명령을 함수로 바꾸기 |
11.수정된 값 반환하기 |
12.오류 코드를 예외로 바꾸기 |
13.예외를 사전확인으로 바꾸기 |
API 리팩터링의 효과
- API(Application Programming Interface)는 코드 사이의 경계를 형성하며, 이 경계를 잘 설계하는 것은 프로그램의 모듈성과 유지보수성에 큰 영향을 미칩니다.
- API 리팩터링의 주요 효과
- 인터페이스 명확성: 함수와 모듈의 목적과 책임이 명확해집니다
- 사용성 향상: 직관적이고 사용하기 쉬운 API는 개발 생산성을 높입니다
- 결합도 감소: 적절한 매개변수와 반환 값 설계로 모듈 간 결합도가 낮아집니다
- 확장성 개선: 잘 설계된 API는 기능 추가와 변경이 용이합니다
- 테스트 용이성: 명확한 책임 경계로 테스트 작성이 쉬워집니다
- 부수효과 감소: 함수의 역할이 명확해져 예측 가능성이 높아집니다
API 리팩터링 기법
1.질의 함수와 변경 함수 분리하기
- 값을 반환하는 함수(질의)와 상태를 변경하는 함수(명령)를 분리
- 함수의 책임 명확화, 사이드 이펙트 가시성 향상, 최적화와 테스트 용이성 개선
// Before
function getTotalAndAddItem(cart, item) {
cart.items.push(item);
return cart.items.reduce((total, i) => total + i.price, 0);
}
// After
// 변경 함수 - 반환값 없음
function addItem(cart, item) {
cart.items.push(item);
}
// 질의 함수 - 상태 변경 없음
function getTotalPrice(cart) {
return cart.items.reduce((total, item) => total + item.price, 0);
}
// 사용
addItem(cart, item);
const total = getTotalPrice(cart);
2.함수 매개변수화하기
- 유사한 함수들의 차이점을 매개변수로 통합
- 코드 중복 제거, 유연성 향상, 함수 수 감소로 인한 코드 간결화
// Before
function tenPercentRaise(person) {
person.salary = person.salary * 1.1;
}
function fivePercentRaise(person) {
person.salary = person.salary * 1.05;
}
// After
function raise(person, factor) {
person.salary = person.salary * factor;
}
// 사용
raise(employee, 1.1); // 10% 인상
raise(employee, 1.05); // 5% 인상
3.플래그 인수 제거하기
- 함수 동작을 전환하는 플래그 매개변수를 별도의 명시적 함수로 분리
- 함수 목적 명확화, 호출 코드 가독성 향상, API 단순화
// Before
function deliverPackage(package, isExpress) {
if (isExpress) {
// 특급 배송 처리
return `${package} will be delivered in 1 day`;
} else {
// 일반 배송 처리
return `${package} will be delivered in 3-5 days`;
}
}
// 사용
deliverPackage("Gift Box", true); // 불분명한 true의 의미
deliverPackage("Documents", false); // 불분명한 false의 의미
// After
function deliverExpressPackage(package) {
return `${package} will be delivered in 1 day`;
}
function deliverStandardPackage(package) {
return `${package} will be delivered in 3-5 days`;
}
// 사용 - 의도가 명확함
deliverExpressPackage("Gift Box");
deliverStandardPackage("Documents");
4.객체 통째로 넘기기
- 동일 객체에서 여러 값을 추출해 개별적으로 전달하는 대신 객체 전체를 전달
- 매개변수 목록 간소화, 객체 내 데이터 관계 유지, 함수 변경 용이성 향상
// Before
function calculateShipping(zipCode, weight, discountRate) {
const baseRate = getShippingRate(zipCode);
const weightFactor = weight * 0.1;
return baseRate * weightFactor * (1 - discountRate);
}
// 사용
const shippingCost = calculateShipping(order.zipCode, order.weight, order.discountRate);
// After
function calculateShipping(order) {
const baseRate = getShippingRate(order.zipCode);
const weightFactor = order.weight * 0.1;
return baseRate * weightFactor * (1 - order.discountRate);
}
// 사용
const shippingCost = calculateShipping(order);
5.매개변수를 질의 함수로 바꾸기
- 함수가 스스로 구할 수 있는 값을 매개변수로 받지 않도록 변경
- 함수 호출 단순화, 중복 제거, 의존성 명확화
// Before
function calculateTotal(order, discount) {
const baseTotal = order.items.reduce((total, item) => total + item.price, 0);
return baseTotal - discount;
}
// 사용
const discount = getOrderDiscount(order);
const total = calculateTotal(order, discount);
// After
function calculateTotal(order) {
const baseTotal = order.items.reduce((total, item) => total + item.price, 0);
return baseTotal - getOrderDiscount(order);
}
function getOrderDiscount(order) {
return order.isVIP ? baseTotal * 0.1 : 0;
}
// 사용
const total = calculateTotal(order);
6.질의 함수를 매개변수로 바꾸기
- 함수 내부에서 의존하는 값을 외부에서 매개변수로 전달받도록 변경
- 함수 순수성 향상, 부수효과 감소, 테스트 용이성 증가
// Before
class Order {
get finalPrice() {
const basePrice = this.quantity * this.itemPrice;
return this.discountedPrice(basePrice);
}
discountedPrice(basePrice) {
return basePrice * (1 - this.discountRate); // 객체 내부 상태에 의존
}
}
// After
class Order {
get finalPrice() {
const basePrice = this.quantity * this.itemPrice;
return this.discountedPrice(basePrice, this.discountRate);
}
discountedPrice(basePrice, discountRate) {
return basePrice * (1 - discountRate); // 매개변수로 명시적 전달
}
}
7.세터 제거하기
- 객체 생성 후 변경되지 않아야 하는 필드의 세터 메서드 제거
- 불변성 보장, 의도하지 않은 수정 방지, 객체 상태 일관성 유지
// Before
class Person {
constructor() {
this._id = null;
this._name = null;
}
get id() { return this._id; }
set id(value) { this._id = value; } // ID는 한 번 설정되면 변경되지 않아야 함
get name() { return this._name; }
set name(value) { this._name = value; }
}
// 사용
const person = new Person();
person.id = "12345"; // 생성 후 설정
person.name = "John";
// ... 나중에 실수로 ...
person.id = "67890"; // ID 변경 가능!
// After
class Person {
constructor(id) {
this._id = id;
this._name = null;
}
get id() { return this._id; }
// 세터 제거 - ID는 생성자에서만 설정 가능
get name() { return this._name; }
set name(value) { this._name = value; }
}
// 사용
const person = new Person("12345"); // 생성 시 ID 설정
person.name = "John";
// 나중에 ID 변경 시도
// person.id = "67890"; // 에러! 세터가 없음
8.생성자를 팩터리 함수로 바꾸기
- 객체 생성의 복잡성을 캡슐화하는 팩터리 함수로 생성자 대체
- 생성 로직 캡슐화, 구체적인 클래스 은닉, 생성 프로세스 유연성 향상
// Before
class Employee {
constructor(type) {
this._type = type;
// 타입에 따라 다른 초기화 로직
if (type === "engineer") {
this._bonus = 0.1;
} else if (type === "manager") {
this._bonus = 0.2;
} else {
throw new Error(`Invalid employee type: ${type}`);
}
}
get type() { return this._type; }
get bonus() { return this._bonus; }
}
// 사용
const engineer = new Employee("engineer");
// After
// 기본 직원 클래스
class Employee {
constructor(type, bonus) {
this._type = type;
this._bonus = bonus;
}
get type() { return this._type; }
get bonus() { return this._bonus; }
}
// 팩터리 함수들
function createEngineer() {
return new Employee("engineer", 0.1);
}
function createManager() {
return new Employee("manager", 0.2);
}
// 사용
const engineer = createEngineer();
9.함수를 명령으로 바꾸기
- 복잡한 함수를 전용 객체(명령 객체)로 캡슐화
- 복잡한 연산 캡슐화, 단계별 실행 가능, 실행 기록 및 취소 기능 구현 용이
// Before
function calculateInsurance(age, dependents, salary, risk) {
// 복잡한 보험료 계산 로직
let base = 0;
if (age < 30) base = salary * 0.2;
else if (age < 60) base = salary * 0.3;
else base = salary * 0.4;
let dependentFactor = 1 + (dependents * 0.1);
let riskFactor = risk === "high" ? 1.5 : 1;
return base * dependentFactor * riskFactor;
}
// After
class InsuranceCalculator {
constructor(age, dependents, salary, risk) {
this.age = age;
this.dependents = dependents;
this.salary = salary;
this.risk = risk;
}
calculate() {
return this.baseAmount() * this.dependentFactor() * this.riskFactor();
}
baseAmount() {
if (this.age < 30) return this.salary * 0.2;
else if (this.age < 60) return this.salary * 0.3;
else return this.salary * 0.4;
}
dependentFactor() {
return 1 + (this.dependents * 0.1);
}
riskFactor() {
return this.risk === "high" ? 1.5 : 1;
}
}
// 사용
const calculator = new InsuranceCalculator(40, 2, 80000, "low");
const premium = calculator.calculate();
10.명령을 함수로 바꾸기
- 간단한 작업을 수행하는 명령 객체를 일반 함수로 대체
- 불필요한 복잡성 제거, 간결한 코드, 명확한 의도 표현
// Before
class DiscountCalculator {
constructor(customer, order) {
this.customer = customer;
this.order = order;
}
calculate() {
if (this.customer.isPremium()) {
return this.order.totalPrice * 0.1;
}
return 0;
}
}
// 사용
const calculator = new DiscountCalculator(customer, order);
const discount = calculator.calculate();
// After
function calculateDiscount(customer, order) {
if (customer.isPremium()) {
return order.totalPrice * 0.1;
}
return 0;
}
// 사용
const discount = calculateDiscount(customer, order);
11.수정된 값 반환하기
- 객체를 직접 수정하는 대신 수정된 복사본을 반환
- 예상치 못한 부수효과 방지, 변경 지점 명확화, 불변 데이터 처리 용이
// Before
function addReservation(reservations, customer) {
reservations.push(customer);
// 반환값 없음, 원본 배열 직접 수정
}
// 사용
addReservation(reservations, customer);
// reservations가 변경됨 - 명시적이지 않음
// After
function addReservation(reservations, customer) {
// 새 배열 반환, 원본 변경 없음
return [...reservations, customer];
}
// 사용
const newReservations = addReservation(reservations, customer);
// 원본 reservations는 변경되지 않고, 새 배열이 반환됨
12.오류 코드를 예외로 바꾸기
- 특수 오류 코드 반환 대신 예외 throw로 오류 처리
- 오류 처리 로직과 정상 로직 분리, 오류 전파 자동화, 호출자의 오류 처리 강제
// Before
function withdrawFromAccount(account, amount) {
if (amount > account.balance) {
return -1; // 오류 코드 반환
}
account.balance -= amount;
return 0; // 성공 코드
}
// 사용
const result = withdrawFromAccount(account, 100);
if (result === -1) {
console.log("잔액 부족");
} else {
console.log("출금 성공");
}
// After
function withdrawFromAccount(account, amount) {
if (amount > account.balance) {
throw new InsufficientFundsError("잔액이 부족합니다");
}
account.balance -= amount;
}
// 사용
try {
withdrawFromAccount(account, 100);
console.log("출금 성공");
} catch (error) {
if (error instanceof InsufficientFundsError) {
console.log("잔액 부족");
} else {
throw error; // 다른 예외는 다시 던짐
}
}
// 오류 유형 정의
class InsufficientFundsError extends Error {
constructor(message) {
super(message);
this.name = "InsufficientFundsError";
}
}
13.예외를 사전확인으로 바꾸기
- 예외적 상황을 미리 검사하여 예외 발생을 방지
- 정상적인 제어 흐름 유지, 예외 처리 비용 감소, 코드 명확성 향상
// Before
function processPayment(payment) {
try {
const user = findUser(payment.userId);
chargeUser(user, payment.amount);
} catch (error) {
if (error instanceof UserNotFoundError) {
console.error("사용자를 찾을 수 없습니다");
} else {
throw error;
}
}
}
function findUser(userId) {
const user = userRepository.findById(userId);
if (!user) {
throw new UserNotFoundError(`ID ${userId}인 사용자가 없습니다`);
}
return user;
}
// After
function processPayment(payment) {
const user = userRepository.findById(payment.userId);
if (!user) {
console.error("사용자를 찾을 수 없습니다");
return; // 예외적 상황을 정상 흐름으로 처리
}
chargeUser(user, payment.amount);
}
// findUser 함수 제거 또는 변경
function findUser(userId) {
return userRepository.findById(userId); // 예외 던지지 않음
}
마무리
JavaScript의 유연한 특성으로 인해 객체 통째로 넘기기와 매개변수 추출의 균형을 맞추는 것이 항상 의구심으로 남아있었습니다. 객체를 통째로 넘기는 습관이 있었는데, 이럴 경우 함수의 의존성이 모호해지더라고요. 실제 프로젝트에서 다음과 같이 리팩터링했을 때 코드의 의도가 훨씬 명확해졌습니다
// Before - 객체 전체를 전달, 의존성이 모호함
function renderUserProfile(user) {
return `
<div class="profile">
<h2>${user.displayName}</h2>
<p>${user.email}</p>
<p>멤버십: ${user.subscription.level}</p>
</div>
`;
}
// After - 필요한 속성만 명시적으로 전달
function renderUserProfile({ displayName, email, subscriptionLevel }) {
return `
<div class="profile">
<h2>${displayName}</h2>
<p>${email}</p>
<p>멤버십: ${subscriptionLevel}</p>
</div>
`;
}
// 호출 시
renderUserProfile({
displayName: user.displayName,
email: user.email,
subscriptionLevel: user.subscription.level
});
최근에는 함수형 프로그래밍의 영향으로 Either 패턴이나 Result 객체를 반환하는 방식도 실험해보고 있습니다. 처음에는 좀 과하다고 생각했는데, 복잡한 비즈니스 로직에서는 이런 패턴이 오히려 코드를 단순화해주는것 같습니다.
개인적으로 API 리팩터링을 통해 얻은 가장 큰 교훈은, 좋은 API는 '사용하는 방법'이 아닌 '사용하는 목적'에 집중해야 한다는 점입니다. 함수명과 매개변수가 "무엇을 위한 것인지" 명확하게 드러내면, 다른 개발자들(본인 포함)이 그 코드를 이해하고 사용하는 데 훨씬 수월해집니다.
결국, API 리팩터링은 단순히 코드를 깔끔하게 만드는 것을 넘어 팀의 협업 방식과 제품의 장기적인 건강성에 직접 영향을 미치는 중요한 활동이고, 계속 건강하게 코드를 작성하고 수정해야겠다는 생각이 들었습니다.
'개발' 카테고리의 다른 글
[리팩터링 2판] JavaScript 리팩터링 도서학습 #8 (2) | 2025.05.29 |
---|---|
[리팩터링 2판] JavaScript 리팩터링 도서학습 #6 (6) | 2025.05.17 |
[리팩터링 2판] JavaScript 리팩터링 도서학습 #5 (7) | 2025.05.15 |
[리팩터링 2판] JavaScript 리팩터링 도서학습 #4 (4) | 2025.04.24 |
[리팩터링 2판] JavaScript 리팩터링 도서학습 #3 (5) | 2025.04.17 |
댓글