Skip to main content

10. 조건부 로직 간소화

  • 조건문 분해하기, 중복 조건식 통합하기
    • 복잡한 조건문인 경우나 논리적 조합을 명확하게 다듬는 경우
  • 중첩 조건문을 보호 구문으로 바꾸기, 조건부 로직을 다형성으로 바꾸기
    • 핵심 로직 전 무언가를 검사하거나, 같은 분기 로직이 자주 등장하는 경우
  • 특이 케이스 추가하기(널 개체 추가하기), 어서션 추가하기
    • 특이 케이스 처리시
  • 제어 플래그를 탈출문으로 바꾸기
    • 제어 플래그를 이용해 코드 동작 흐름을 바꾸는 경우

10.1 조건문 분해하기#

// before
if (!aDate.isBefore(plan.summerStart) && !aDate.isAfter(plan.summerEnd))
change = quantity * plan.summerRate
else change = quantity * plan.regularRate + plan.regularServiceCharge
// after
if (summer()) charge = summerCharge()
else charge = regularCharge()

배경#

  • 복잡한 조건부 로직은 프로그램을 만들게 합니다.
  • 거대한 코드 블럭은 코드를 부위별로 분해한 다음 해체된 코드 덩어리들을 각 덩어리의 의도를 살인 이름의 함수 호출을 했습니다.

절차#

  1. 조건식과 그 조건식에 달린 조건절 각각을 함수로 추출했습니다.

예시#

  • 리팩터링 전 코드
if (!aDate.isBefore(plan.summerStart) && !aDate.isAfter(plan.summerEnd))
change = quantity _ plan.summerRate
else change = quantity _ plan.regularRate + plan.regularServiceCharge
  • 리팩터링 후 코드
charge = summer() ? summerCharge() : regularCharge()
function summer() {
return !aDate.isBefore(plan.summerStart) && !aDate.isAfter(plan.summerEnd)
}
function summerCharge() {
return quantity * plan.summerRate
}
function regularCharge() {
return quantity * plan.regularRate + plan.regularServiceCharge
}

10.2 조건식 통합하기#

// before
if (anEmployee.seniority < 2) return 0
if (anEmployee.monthsDisabled > 12) return 0
if (anEmployee.isPartTime) return 0
// after
if (isNotEligibleForDisability()) return 0
function isNotEligibleForDisability() {
return (
anEmployee.seniority < 2 ||
anEmployee.monthsDisabled > 12 ||
anEmployee.isPartTime
)
}

배경#

  • 비교하는 조건은 다르나 결과로 수행하는 동작이 똑같은 경우에 할 수 있습니다.
  • 조건부 코드를 통합하는 경우는 두가지 이유가 있습니다.
    • 여러 조각으로 나뉜 조건들을 하나로 통합함으로써 하는 일이 명확해집니다.
    • 함수 추출하기 까지 이어질 가능성이 높습니다.

절차#

  1. 해당 조건식을 모두에 보수효과가 없는지 확인합니다.
  2. 조건문 두 개를 선택하여 두 조건문의 조건식들을 논리 연산자로 결합합니다.
  3. 테스트합니다.
  4. 조건을 하나만 남을 때까지 2~3 과정을 반복합니다.
  5. 하나로 합쳐진 조건식을 함수로 추출할지 고민합니다.

예시#

앞의 예시 말고도 다음과 같이 사용할 수 있습니다.

// before
if (anEmployee.onVacation) if (anEmployee.seniority > 10) return 1
return 0.5
// after
if (anEmployee.onVacation && anEmployee.seniority > 10) return 1
else return 0

10.3 중첩 조건문을 보호 구문으로 바꾸기#

// before
function getPayAmount() {
let result
if (isDead) result = deadAmount()
else {
if (isSeparated) result = separatedAmount()
else {
if (isRetired) result = retiredAmount()
else result = normalPayAmount()
}
}
return result
}
// after
function getPayAmount() {
if (isDead) return deadAmount()
if (isSeparated) return separatedAmount()
if (isRetired) return retiredAmount()
return normalPayAmount()
}

배경#

  • 조건문은 주로 두가지 형태로 쓰입니다.
    • 참인 경로와 거짓인 경로 모두 정상 동작으로 이어지는 형태
    • 한쪽만 정상인 형태
  • 중첩 조건문을 보호 구문으로 바꾸기 리팩터팅의 핵심은 의도를 부각하는 데 의미가 있습니다.

절차#

  1. 교체해야 할 조건 중 가장 바깥 것을 선택하여 보호 구문으로 바꿉니다.
  2. 테스트합니다.
  3. 1~2 과정을 필요한 만큼 반복합니다.
  4. 모든 보호 구문이 같은 결과를 반환한다면 보호 구문들의 조건식을 통합합니다.

예시#

function getPayAmount(employee) {
let result
if (employee.isSeparated) result = { amount: 0, reasonCode: 'SEP' }
else {
if (employee.isRetired) result = { amount: 0, reasonCode: 'RET' }
else {
// 급여 계산 로직
lorem.ipsum(dolor.sitAet)
consectetur(adipiscing).elict()
sed.do.eiusmod = tempor.incididunt.ut(labore) && dolore(magna.aliqua)
ut.enim.ad(minim.veniam)
result = someFinalComputation()
}
}
return result
}
  • 최상위 조건부터 보호 구문으로 바꿉니다.
function getPayAmount(employee) {
let result
if (employee.isSeparated) return { amount: 0, reasonCode: 'SEP' }
if (employee.isRetired) result = { amount: 0, reasonCode: 'RET' }
else {
// 급여 계산 로직
lorem.ipsum(dolor.sitAet)
consectetur(adipiscing).elict()
sed.do.eiusmod = tempor.incididunt.ut(labore) && dolore(magna.aliqua)
ut.enim.ad(minim.veniam)
result = someFinalComputation()
}
return result
}
  • 테스트 후 다음 조건으로 넘깁니다.
function getPayAmount(employee) {
if (employee.isSeparated) return { amount: 0, reasonCode: 'SEP' }
if (employee.isRetired) return { amount: 0, reasonCode: 'RET' }
else {
// 급여 계산 로직
lorem.ipsum(dolor.sitAet)
consectetur(adipiscing).elict()
sed.do.eiusmod = tempor.incididunt.ut(labore) && dolore(magna.aliqua)
ut.enim.ad(minim.veniam)
return someFinalComputation()
}
}

조건 반대로 만들기#

function adjustedCapital(anInstrument) {
let result = 0
if (anInstrument.capital > 0) {
if (anInstrument.interestRate > 0 && anInstrument.duration > 0) {
result =
(anInstrument.income / anInstrument.duration) *
anInstrument.adjustmentFactor
}
}
return result
}
  • 조건을 역으로 바꾸면서 보호구문을 추가합니다.
function adjustedCapital(anInstrument) {
let result = 0
if (anInstrument.capital <= 0) return result
if (anInstrument.interestRate > 0 && anInstrument.duration > 0) {
result =
(anInstrument.income / anInstrument.duration) *
anInstrument.adjustmentFactor
}
return result
}
  • 조건을 간소화하고, 조건식을 통합합니다.
function adjustedCapital(anInstrument) {
if (
anInstrument.capital <= 0 ||
anInstrument.interestRate <= 0 ||
anInstrument.duration <= 0
)
return result
return (
(anInstrument.income / anInstrument.duration) *
anInstrument.adjustmentFactor
)
}

10.4 조건부 로직을 다형성으로 바꾸기#

// before
switch (bird.type) {
case '유럽 제비':
return '보통이다'
case '아프리카 제비':
return bird.numberOfCounts > 2 ? '지쳤다' : '보통이다'
case '노르웨이 파랑 앵무':
return bird.voltage > 100 ? '그을렸다' : '예쁘다'
default:
return '알 수 없다'
}
// after
class EuropeanSwallow {
get plumage() {
return '보통이다'
}
// ...
}
class AfricanSwallow {
get plumage() {
return bird.numberOfCounts > 2 ? '지쳤다' : '보통이다'
}
// ...
}
class NorwegianBlueParrot {
get plumage() {
return bird.voltage > 100 ? '그을렸다' : '예쁘다'
}
// ...
}

배경#

  • 복잡한 조건부 로직은 클래스와 다형성을 이용해서 더 확실하게 분리할 수도 있습니다.

절차#

  1. 다형적 동작을 표현하는 클래스들이 아직 없다면 만들어줍니다. 이왕이면 적합한 인스턴스를 알아서 만들어 반환하는 팩터리 함수도 만듭니다.
  2. 호출하는 코드에서 팩터리 함수를 사용하게 합니다.
  3. 조건부 로직 함수를 슈퍼클래스로 옮깁니다.
  4. 서브 클래스 중 하나를 선택합니다. 서브 클래스에서 슈퍼클래스의 조건부 로직 메서드를 오버라이드합니다. 조건부 문장 중 선택된 서브클래스에 해당하는 조건절을 서브 클래스 메서드로 복사한 다음 적절히 수정합니다.
  5. 같은 방식으로 각 조건절을 해당 서브클래스에서 메서드로 구현합니다.
  6. 슈퍼클래스 메서드에서는 기본 동작 부분만 남깁니다. 혹은 슈퍼클래스가 추상 클래스여야 한다면, 이 메서드를 추상으로 선언하거나 서브클래스에서 처리해야 함을 알리는 에러를 던집니다.

예시#

function plumages(birds) {
return new Map(birds.map((b) => [b.name, plumage(b)]))
}
function speeds(birds) {
return new Map(birds.map((b) => [b.name, airSpeedVelocity(b)]))
}
function plumage(bird) {
switch (bird.type) {
case '유럽 제비':
return '보통이다'
case '아프리카 제비':
return bird.numberOfCounts > 2 ? '지쳤다' : '보통이다'
case '노르웨이 파랑 앵무':
return bird.voltage > 100 ? '그을렸다' : '예쁘다'
default:
return '알 수 없다'
}
}
function airSpeedVelocity(bird) {
switch (bird.type) {
case '유럽 제비':
return 35
case '아프리카 제비':
return 40 - 2 * bird.numberOfCoconuts
case '노르웨이 파랑 앵무':
return bird.isNailed ? 0 : 10 + bird.voltage / 10
default:
return null
}
}
  • Bird 클래스로 묶습니다.
function plumages(birds) {
return new Bird(bird).plumages
}
function speeds(birds) {
return new Bird(bird).airSpeedVelocity
}
class Bird {
constructor(birdObject) {
Object.assign(this, birdObject)
}
get plumage() {
switch (this.type) {
case '유럽 제비':
return '보통이다'
case '아프리카 제비':
return this.numberOfCounts > 2 ? '지쳤다' : '보통이다'
case '노르웨이 파랑 앵무':
return this.voltage > 100 ? '그을렸다' : '예쁘다'
default:
return '알 수 없다'
}
}
get airSpeedVelocity() {
switch (this.type) {
case '유럽 제비':
return 35
case '아프리카 제비':
return 40 - 2 * this.numberOfCoconuts
case '노르웨이 파랑 앵무':
return this.isNailed ? 0 : 10 + this.voltage / 10
default:
return null
}
}
}
  • 종별 서브클래스를 만듭니다.
function plumages(birds) {
return createBird(bird).plumages
}
function speeds(birds) {
return createBird(bird).airSpeedVelocity
}
function createBird(bird) {
switch (this.type) {
case '유럽 제비':
return new EuropeanSwallow(bird)
case '아프리카 제비':
return new AfricanSwallow(bird)
case '노르웨이 파랑 앵무':
return new NorwegianBirdParrot(bird)
default:
return new Bird(bird)
}
}
class Bird {
// 생략
}
class EuropeanSwallow extends Bird {}
class AfricanSwallow extends Bird {}
class NorwegianBirdParrot extends Bird {}
  • 코드가 길기 때문에 생략하고, 기존에 switch-case로 되어있는 부분을 오버라이딩해서 수정합니다.

10.5 특이 케이스 추가하기#

// before
if (aCustomer === '미확인 고객') customerName = '거주자'
// after
class UnknownCustomer {
get name() {
return '거주자'
}
}

배경#

  • 특수한 경우의 공통 동작을 요소 하나에 모아서 사용하는 특이 케이스 패턴이 있는데 이때 적용하면 좋습니다.
  • 대표적인 예시로 null이 있습니다.

절차#

  1. 컨테이너에 특이 케이스인지 검사하는 속성을 추가하고 false를 반환하게 합니다.
  2. 특이 케이스 객체를 만듭니다. 이 객체는 특이 케이스인지를 검사하는 속성만 포함하여, 이 속성은 true를 반환합니다.
  3. 클라이언트에서 특이 케이스인지를 검사하는 코드를 함수로 추출합니다. 모든 클라이언트가 값을 직접 비교하는 대신 방금 추출한 함수를 사용하도록 고칩니다.
  4. 코드에 새로운 특이 케이스 대상을 추가합니다. 함수의 반환 값으로 받거나 변환 함수를 적용하면 됩니다.
  5. 특이 케이스를 검사하는 함수 본문을 수정하여 특이케이스 객체의 속성을 사용하도록 합니다.
  6. 테스트합니다.
  7. 여러 함수를 클래스로 묶기나 여러 함수를 변환 함수로 묶기를 적용하여 특이 케이스를 처리하는 공통 동작을 새로운 요소로옮깁니다.
  8. 아직도 특이 케이스 검사 함수를 이용하는 곳이 남아 있다면 검사 함수를 인라인합니다.

예시#

class Site {
get cumstomer() {
return this._customer
}
}
class Customer {
get name() {...}
get billingPlan() {...}
set billingPlan(arg) {...}
get paymentHistory() {...}
}
// 클라이언트 1
const aCustomer = site.customer
let customerName
if (aCustomer === '미확인 고객') customerName = '거주자'
else customerName = aCustomer.name
// 클라이언트 2
const plan =
aCustomer === '미확인 고객'
? registry.billingPlans.basic
: aCustomer.billingPlan
// 클라이언트 3
if (aCustomer !== '미확인 고객') aCustomer.billingPlan = newPlan
// 클라이언트 4
const weeksDelinquent =
aCustomer === '미확인 고객'
? 0
: aCustomer.paymentHistory.weeksDelinquentInLastYear
  • 리팩터링 후 코드
class UnknownCustomer {
get name() {
return '거주자'
}
get paymentHistory() {
return new NullPaymentHistory()
}
}
class NullPaymentHistory {
get weeksDelinquentInLastYear() {
return 0
}
}
// 클라이언트
const weeksDelinquent = aCustomer.paymentHistory.weeksDelinquentInLastYear
// 튀는 클라이언트
const name = aCustomer.isUnknown ? '미확인 거주자' : aCustomer.name
  • 다른 코드도 생략합니다.

10.6 어서션 추가하기#

// before
if (this.discountRate) base = base - this.discountRate * base
// after
assert(this.discountRate >= 0)
if (this.discountRate) base = base - this.discountRate * base

배경#

  • 특정 조건이 참일 때만 정상적으로 동작하는 코드 영역이 있는데, 이 경우를 명시적으로 작성하면 이해하기 좋아집니다.
  • 어셔션을 통해서 제대로 개발하게 되고, 소통 등의 장점을 가집니다.

절차#

  • 참이라고 가정하는 조건이 보이면 그 조건을 명시하는 어서션을 추가합니다.

예시#

class Customer {
applyDiscount(aNumber) {
return this.discountRate ? aNumber - this.discountRate * aNumber : aNumber
}
}
  • if-then으로 수정합니다.
class Customer {
applyDiscount(aNumber) {
if (!this.discountRate) return aNumber
else return aNumber - this.discountRate * aNumber
}
}
  • 어서션을 추가합니다.
class Customer {
applyDiscount(aNumber) {
if (!this.discountRate) return aNumber
else {
assert(this.discountRate > 0)
return aNumber - this.discountRate * aNumber
}
}
}
  • 이는 목적을 위해 setter 메서드에 추가합니다.
class Customer {
set discountRate(aNumber) {
assert(null === aNumber || aNumber >= 0)
this._discountRate = aNumber
}
}

10.7 제어 플래크를 탈출문으로 바꾸기#

// before
for (const p of people) {
if (!found) {
if (p === '조커') {
sendAlert()
found = true
}
// ...
}
}
// after
for (const p of people) {
if (p === '조커') {
sendAlert()
break
}
// ...
}

배경#

  • 제어 플래그란 코드의 동작을 변경하는데 사용되는 변수입니다.
  • 반복문안에서 좀 보기 안좋은 코드는 break, continue, return 등으로 처리합니다.

절차#

  1. 제어 플래그를 사용하는 코드를 함수로 추출할지 고민합니다.
  2. 제어 플래그를 갱신하는 코드 각각을 적절한 제어문으로 바꿉니다. 하나 바꿀때마다 테스트합니다.
  3. 모두 수정했다면 제어 플래그를 제거합니다.

예시#

  • 코드 생략
Last updated on