해당 내용은 위의 책의 내용을 제가 이해한대로 정리해둔 내용입니다.
이번 포스팅에선 Chapter10 의 내용에 대해 다룹니다.
- Chapter10 조건부 로직 간소화
10장에서 소개하는 주요 리팩터링 기법들을 요약하고 예제와 함께 정리했습니다.
Chapter10 조건부 로직 간소화
1.조건문 분해하기 |
2.중복 조건식 통합하기 |
3.중첩 조건문을 보호 구문으로 바꾸기 |
4.조건부 로직을 다형성으로 바꾸기 |
5.특이 케이스 추가하기 |
6.어서션 추가하기 |
7.제어 플래그를 탈출문으로 바꾸기 |
조건부 로직 간소화의 효과
복잡한 조건부 로직은 코드 이해와 유지보수를 어렵게 합니다
조건부 로직을 간소화하면 얻을수있는 이점
- 가독성 향상: 코드의 의도가 명확해져 이해하기 쉬워집니다
- 유지보수성 개선: 간결한 조건문은 변경 사항 적용이 쉽습니다
- 버그 감소: 복잡한 조건문에서 발생하기 쉬운 논리적 오류를 줄입니다
- 확장성 향상: 더 명확한 구조로 기능 추가가 용이해집니다
- 테스트 용이성: 단순한 조건문은 테스트 케이스 작성이 쉽습니다
조건부 로직 간소화 기법
1. 조건문 분해하기
// Before
function calculateCharge(date, quantity, plan) {
let charge = 0;
if (!date.isBefore(plan.summerStart) && !date.isAfter(plan.summerEnd)) {
charge = quantity * plan.summerRate;
} else {
charge = quantity * plan.regularRate + plan.regularServiceCharge;
}
return charge;
}
// After
function calculateCharge(date, quantity, plan) {
if (isSummer(date, plan)) {
return summerCharge(quantity, plan);
} else {
return regularCharge(quantity, plan);
}
}
function isSummer(date, plan) {
return !date.isBefore(plan.summerStart) && !date.isAfter(plan.summerEnd);
}
function summerCharge(quantity, plan) {
return quantity * plan.summerRate;
}
function regularCharge(quantity, plan) {
return quantity * plan.regularRate + plan.regularServiceCharge;
}
2. 중복 조건식 통합하기
- 여러 조건문이 동일한 결과를 낼 때 하나의 조건식으로 통합
- 코드 중복 제거, 의도 명확화, 수정 시 발생할 수 있는 불일치 방지
// Before
function disabilityAmount(employee) {
if (employee.seniority < 2) return 0;
if (employee.monthsDisabled > 12) return 0;
if (employee.isPartTime) return 0;
// 장애 수당 계산 로직
return calculateDisabilityAmount(employee);
}
// After
function disabilityAmount(employee) {
if (isNotEligibleForDisability(employee)) return 0;
// 장애 수당 계산 로직
return calculateDisabilityAmount(employee);
}
function isNotEligibleForDisability(employee) {
return employee.seniority < 2
|| employee.monthsDisabled > 12
|| employee.isPartTime;
}
3. 중첩 조건문을 보호 구문으로 바꾸기
- 여러 조건문이 동일한 결과를 낼 때 하나의 조건식으로 통합
- 코드 중복 제거, 의도 명확화, 수정 시 발생할 수 있는 불일치 방지
// Before
function getPayAmount(employee) {
let result = 0;
if (employee.isSeparated) {
result = { amount: 0, reasonCode: "SEP" };
} else {
if (employee.isRetired) {
result = { amount: 0, reasonCode: "RET" };
} else {
// 급여 계산 로직
result = someFinalComputation();
}
}
return result;
}
// After
function getPayAmount(employee) {
// 보호 구문으로 예외 케이스 먼저 처리
if (employee.isSeparated) return { amount: 0, reasonCode: "SEP" };
if (employee.isRetired) return { amount: 0, reasonCode: "RET" };
// 주요 로직이 중첩 없이 명확하게 드러남
return someFinalComputation();
}
4. 조건부 로직을 다형성으로 바꾸기
- 타입에 따른 조건부 로직을 클래스와 다형성을 사용하여 대체
- 새로운 타입 추가가 용이해지고, 조건부 로직이 관련 클래스로 분산되어 응집도 향상
// Before
function plumages(birds) {
return birds.map(b => plumage(b));
}
function speeds(birds) {
return birds.map(b => airSpeedVelocity(b));
}
function plumage(bird) {
switch (bird.type) {
case 'EuropeanSwallow':
return "average";
case 'AfricanSwallow':
return (bird.numberOfCoconuts > 2) ? "tired" : "average";
case 'NorwegianBlueParrot':
return (bird.voltage > 100) ? "scorched" : "beautiful";
default:
return "unknown";
}
}
function airSpeedVelocity(bird) {
switch (bird.type) {
case 'EuropeanSwallow':
return 35;
case 'AfricanSwallow':
return 40 - 2 * bird.numberOfCoconuts;
case 'NorwegianBlueParrot':
return (bird.isNailed) ? 0 : 10 + bird.voltage / 10;
default:
return null;
}
}
// After
function plumages(birds) {
return birds.map(b => createBird(b).plumage);
}
function speeds(birds) {
return birds.map(b => createBird(b).airSpeedVelocity);
}
function createBird(bird) {
switch (bird.type) {
case 'EuropeanSwallow':
return new EuropeanSwallow(bird);
case 'AfricanSwallow':
return new AfricanSwallow(bird);
case 'NorwegianBlueParrot':
return new NorwegianBlueParrot(bird);
default:
return new Bird(bird);
}
}
class Bird {
constructor(birdObject) {
Object.assign(this, birdObject);
}
get plumage() {
return "unknown";
}
get airSpeedVelocity() {
return null;
}
}
class EuropeanSwallow extends Bird {
get plumage() {
return "average";
}
get airSpeedVelocity() {
return 35;
}
}
class AfricanSwallow extends Bird {
get plumage() {
return (this.numberOfCoconuts > 2) ? "tired" : "average";
}
get airSpeedVelocity() {
return 40 - 2 * this.numberOfCoconuts;
}
}
class NorwegianBlueParrot extends Bird {
get plumage() {
return (this.voltage > 100) ? "scorched" : "beautiful";
}
get airSpeedVelocity() {
return (this.isNailed) ? 0 : 10 + this.voltage / 10;
}
}
5. 특이 케이스 추가하기
- null이나 예외적인 값을 처리하는 반복적인 코드를 특이 케이스 객체로 대체
- 예외 처리 로직의 중복 제거, 코드 이해도 향상, 특수 상황 처리 일관성 확보
// Before
class Site {
constructor(customer) {
this._customer = customer;
}
get customer() { return this._customer; }
}
// 클라이언트 코드
const site = new Site(customer);
const customerName = site.customer ? site.customer.name : "occupant";
const plan = site.customer ? site.customer.plan : registry.basicPlan;
const weeksDelinquent = site.customer ? site.customer.paymentHistory.weeksDelinquentInLastYear : 0;
// After
// 특이 케이스 객체 생성
class UnknownCustomer {
get name() { return "occupant"; }
get plan() { return registry.basicPlan; }
get paymentHistory() { return new NullPaymentHistory(); }
// 특이 케이스 확인 메서드
get isUnknown() { return true; }
}
class NullPaymentHistory {
get weeksDelinquentInLastYear() { return 0; }
}
class Customer {
get isUnknown() { return false; }
// 기존 Customer 코드...
}
class Site {
constructor(customer) {
this._customer = (customer === "unknown") ? new UnknownCustomer() : customer;
}
get customer() { return this._customer; }
}
// 클라이언트 코드 단순화
const site = new Site(customer);
const customerName = site.customer.name;
const plan = site.customer.plan;
const weeksDelinquent = site.customer.paymentHistory.weeksDelinquentInLastYear;
// 더 명시적인 구분이 필요한 경우
if (site.customer.isUnknown) {
// 알 수 없는 고객에 대한 특별 처리
}
6. 어서션 추가하기
- 코드가 특정 조건을 만족한다고 가정하는 부분에 명시적 검사를 추가
- 의도와 가정을 명확히 표현, 오류를 빠르게 발견, 디버깅 용이성 향상
// Before
function applyDiscount(customer, amount) {
// 할인율이 양수라고 가정하지만 명시적 검사 없음
return amount - (amount * customer.discountRate);
}
// After
function applyDiscount(customer, amount) {
// 명시적 어서션으로 가정 검증
assert(customer.discountRate >= 0, "할인율은 음수가 될 수 없습니다");
return amount - (amount * customer.discountRate);
}
// 기본 어서션 함수
function assert(condition, message) {
if (!condition) {
throw new Error(message || "Assertion failed");
}
}
7. 제어 플래그를 탈출문으로 바꾸기
- 루프를 제어하는 플래그 변수를 break, continue, return 등의 탈출문으로 대체
- 코드 단순화, 가독성 향상, 논리 구조 명확화
// Before
function findPerson(people) {
let found = false;
for (let i = 0; i < people.length; i++) {
if (!found) {
if (people[i] === "Don") {
sendAlert();
found = true;
}
if (people[i] === "John") {
sendAlert();
found = true;
}
}
}
}
// After
function findPerson(people) {
for (let i = 0; i < people.length; i++) {
if (people[i] === "Don") {
sendAlert();
return;
}
if (people[i] === "John") {
sendAlert();
return;
}
}
}
// 더 단순하게 - 현대적 JavaScript 방식
function findPerson(people) {
const targetPeople = ["Don", "John"];
const found = people.find(p => targetPeople.includes(p));
if (found) sendAlert();
}
마무리
조건부 로직 간소화 기법들을 학습하면서 깨달은 가장 큰 점은, 복잡한 조건문이 그 자체로 코드의 '악취'라는 사실입니다. 프론트엔드 개발을 하며 가장 자주 마주했던 문제이기도했고, 특히 레거시 코드를 인수받거나 대규모 프로젝트에 투입되었을 때, 깊게 중첩된 if-else 구문과 거대한 switch 문은 코드 이해의 많은 문제가 되었습니다.
// Before
function processUserRegistration(user) {
if (user) {
if (user.age >= 18) {
if (user.email && validateEmail(user.email)) {
if (!isBlacklisted(user.email)) {
// 여기에 핵심 로직 - 50줄 이상의 복잡한 코드
return SUCCESS;
} else {
return ERROR_BLACKLISTED;
}
} else {
return ERROR_INVALID_EMAIL;
}
} else {
return ERROR_UNDERAGE;
}
} else {
return ERROR_NO_USER;
}
}
// After
function processUserRegistration(user) {
// 보호 구문으로 변환
if (!user) return ERROR_NO_USER;
if (user.age < 18) return ERROR_UNDERAGE;
if (!user.email || !validateEmail(user.email)) return ERROR_INVALID_EMAIL;
if (isBlacklisted(user.email)) return ERROR_BLACKLISTED;
// 여기서부터 핵심 로직에 집중할 수 있음
// 조건 체크에서 벗어나 비즈니스 로직이 명확하게 보임
return SUCCESS;
}
JavaScript의 컨텍스트에서는 함수형 프로그래밍 패러다임이 조건부 로직을 간소화하는 데 특히 효과적이라고 생각합니다. 조건들을 작은 함수로 분리하고 조합하는 방식은 가독성과 테스트 용이성을 크게 높여줍니다.
// 조건들을 명확한 의미를 가진 작은 함수로 분리
const meetsAgeRequirement = user => user.age >= 18;
const hasValidEmail = user => user.email && validateEmail(user.email);
const isAllowedToRegister = user => !isBlacklisted(user.email);
// 조건 함수들을 조합하여 사용
const canRegister = user =>
user &&
[meetsAgeRequirement, hasValidEmail, isAllowedToRegister]
.every(check => check(user));
// 메인 로직이 훨씬 명확해짐
function processUserRegistration(user) {
if (!canRegister(user)) {
return generateErrorCode(user); // 적절한 에러 코드를 생성하는 함수
}
// 핵심 비즈니스 로직
}
결국 조건부 로직 간소화는 단순히 코드를 "예쁘게" 만드는 것이 아니라,
비즈니스 로직의 본질을 더 명확하게 드러내고 변화에 유연하게 대응할 수 있는 코드 구조를 만드는 과정입니다.
복잡한 조건문 하나를 리팩터링하는 데 시간을 투자하는 것이, 장기적으로는 그 몇 배의 시간을 절약해 준다라고 하는데, 이러한 리팩터링을 통해서 많은 시간이 절약되는 경험을 빠른시일내에 마주할수있기를 바랍니다.
'개발' 카테고리의 다른 글
[리팩터링 2판] JavaScript 리팩터링 도서학습 #8 (2) | 2025.05.29 |
---|---|
[리팩터링 2판] JavaScript 리팩터링 도서학습 #7 (5) | 2025.05.19 |
[리팩터링 2판] JavaScript 리팩터링 도서학습 #5 (7) | 2025.05.15 |
[리팩터링 2판] JavaScript 리팩터링 도서학습 #4 (4) | 2025.04.24 |
[리팩터링 2판] JavaScript 리팩터링 도서학습 #3 (5) | 2025.04.17 |
댓글