Recent Posts
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
Tags
- taint
- 개성국밥
- JanusGateway
- 티스토리챌린지
- preemption #
- 달인막창
- tolerated
- python
- 겨울 부산
- 자원부족
- 헥사고날아키텍처 #육각형아키텍처 #유스케이스
- JanusWebRTCGateway
- PytestPluginManager
- pytest
- Value too long for column
- 코루틴 컨텍스트
- kotlin
- Spring Batch
- table not found
- k8s #kubernetes #쿠버네티스
- JanusWebRTC
- 깡돼후
- terminal
- 코루틴 빌더
- JanusWebRTCServer
- vfr video
- PersistenceContext
- VARCHAR (1)
- 오블완
- mp4fpsmod
Archives
너와 나의 스토리
[리팩터링] 특이 케이스 추가하기 Introduce Special Case 본문
반응형
배경
- 고객 리스트가 있다고 할 때, 이름과 같은 정보가 적혀있지 않은 미확인 고객이 있을 수 있다. 이런 특이 케이스를 검사하는 코드가 있다고 생각하자.
- 데이터 구조의 특정 값을 확인한 후 똑같은 동작을 수행하는 코드가 곳곳에 등장하는 경우가 더러 있다.
- 흔히 볼 수 있는 중복 코드 중 하나다.
- 특수한 경우의 공통 동작을 요소 하나에 모암서 사용하는 특이 케이스 패턴(Special Case Pattern)이라는 것이 있다.
- 위와 같은 경우에 적용하면 좋을 메커니즘이다.
- 이 패턴을 활용하면 특이 케이스를 확인하는 코드 대부분을 단순한 함수 호출로 바꿀 수 있다.
- 널(null)은 특이 케이스로 처리해야 할 때가 많다. 그래서 이 패턴을 널 객체 패턴(Null Object Pattern)이라고도 한다.
절차
- 컨테이너에 특이 케이스인지를 검사하는 속성을 추가하고, false를 반환하게 한다.
- 특이 케이스 객체를 만든다. 이 객체는 특이 케이스인지를 검사하는 속성만 포함하며, 이 속성은 true를 반환하게 한다.
- 클라이언트에서 특이 케이스인지를 검사하는 코드를 함수로 추출한다. 모든 클라이언트가 값을 직접 비교하는 대신 방금 추출한 함수를 사용하도록 고친다.
- 코드에 새로운 특이 케이스 대상을 추가한다. 함수의 반환 값으로 받거나 변환 함수를 적용하면 된다.
- 특이 케이스를 검사하는 함수 본문을 수정하여 특이 케이스 객체의 속성을 사용하도록 한다.
- 테스트한다.
- 여러 함수를 클래스로 묶기나 여러 함수를 변환 함수로 묶기를 적용하여 특이 케이스를 처리하는 공통 동작을 새로운 요소로 옮긴다.
- 아직도 특이 케이스 검사 함수를 이용하는 곳이 남아 있다면 검사 함수를 인라인 한다.
예시 1
- 전력 회사는 전력이 필요한 현장(site)에 인프라를 설치해 서비스를 제공한다.
-
class Site { get customer() { return this._customer; } } class Customer { get name() {...} get billingPlan() {...} set billingPlan(arg) {...} get paymentHistory() {...} }
- 기존의 누군가 이사 가고 새로운 누군가(아직 누구인지 모름)가 이사 온 경우, 데이터 레코드의 고객 필드를 "미확인 고객"이라는 문자열로 채운다.
- 이런 상황을 감안하여 Site 클래스를 사용하는 클라이언트 코드들은 알려지지 않은 미확인 고객도 처리할 수 있어야 한다.
- 이런 코드의 예는 다음과 같다.
-
// 클라이언트 1 const aCustomer = site.customer; ... let customerName; if (aCustomer === "미확인 고객") customerName = "거주자"; else customerName = aCustomer.name; // 클라이언트 2 const plan = (aCustomer === "미확인 고객") ? CustomElementRegistry.billingPlans.basic : acustomer.billingPlan; // 클라이언트 3 if (aCustomer === "미확인 고객") aCustomer.billingPlan = newPlan; // 클라이언트 4 const weeksDelinquent = (aCustomer === "미확인 고객") ? 0 : aCustomer.paymentHistory.weeksDelinquentInLastyear;
- * delinquent: 채무를 이행하지 않은
- 코드 베이스를 훑어보니 미확인 고객을 처리해야 하는 클라이언트가 여러 개 발견됐고, 그 대부분에서 똑같은 방식으로 처리했다.
- 미확인 고객
- 고객 이름: "거주자"
- 기본 요금제(billing plan) 청구
- 많은 곳에서 이뤄지는 이 특이 케이스 검사와 공통된 반응이 우리에게 특이 케이스 객체를 도입할 때임을 말해준다.
- Step 1: 미확인 고객인지를 나타내는 메서드를 고객 클래스에 추가한다.
-
class Customer { ... get isUnknown() { return false; } }
-
- Step 2: 미확인 고객 전용 클래스를 만든다.
-
class UnknownCustomer { get isUnknown() { return true; } }
-
- Step 3: 클라이언트에서 특이 케이스(UnknownCustomer)인지를 검사하는 코드를 함수로 추출하고 검사하는 곳에서 이를 사용하도록 고쳐야 한다.
-
function isUnknown(arg) { if (!((arg instanceof Customer || (arg == "미확인 고객")))) throw new Error(`잘못된 값과 비교: <${arg}>`); return (arg === "미확인 고객"); } // 클라이언트 1 const aCustomer = site.customer; ... let customerName; if(!isUnknown(aCustomer)) customerName = "거주자"; else customerName = aCustomer.name; // 클라이언트 2 const plan = isUnknown(aCustomer) ? CustomElementRegistry.billingPlans.basic : acustomer.billingPlan; // 클라이언트 3 if(!isUnknown(aCustomer)) aCustomer.billingPlan = newPlan; // 클라이언트 4 const weeksDelinquent = isUnknown(aCustomer) ? 0 : aCustomer.paymentHistory.weeksDelinquentInLastyear;
-
- Step 4: 특이 케이스일 때(미확인 고객일 때) Site 클래스가 UnknownCustomer 객체를 반환하도록 수정하자.
-
class Site { // Before: get customer() { return this._customer; } get customer() { return (this._customer === "미확인 고객") ? new UnknownCustomer() : this._customer; } }
-
- Step 5: isUnknown() 함수를 수정하여 고객 객체의 속성을 사용하도록 하면 "미확인 고객" 문자열을 사용하던 코드는 완전히 사라진다.
-
function isUnknown(arg) { if (!((arg instanceof Customer || arg instanceof UnknownCustomer))) throw new Error(`잘못된 값과 비교: <${arg}>`); return arg.isUnknown; }
-
- Step 6: 모든 기능이 잘 동작하는지 테스트
- Step 7: 각 클라이언트에서 수행하는 특이 케이스 검사를 일반적인 기본값으로 대체할 수 있다면 이 검사 코드에 여러 함수를 클래스로 묶기를 적용할 수 있다.
- 지금 예에서는 미확인 고객의 이름으로 "거주자"를 사용하는 코드가 많다.
-
// 클라이언트 1 let customerName; if (aCustomer === "미확인 고객") customerName = "거주자"; else customerName = aCustomer.name;
- 다음과 같이 적절한 메서드를 Unknowncustomer 클래스에 추가하자.
-
class UnknownCustomer { ... get name() { return "거주자"; } }
- 그러면 조건부 코드는 지워도 된다.
-
const customerName = aCustomer.name;
- 요금제 속성을 처리하는 부분(클라이언트 1, 2)도 위 과정을 반복하면 된다.
-
// 클라이언트 2 const plan = isUnknown(aCustomer) ? CustomElementRegistry.billingPlans.basic : aCustomer.billingPlan; // 클라이언트 3 if(!isUnknown(aCustomer)) aCustomer.billingPlan = newPlan;
- 현재 코드에서는 미확인 고객에 대해서는 세터를 호출하지 않지만 겉보기 동작을 똑같게 만들기 위해 세터를 만들어 준다.
-
class UnknownCustomer { ... get billingPlan() { return CustomElementRegistry.billingPlans.basic; } set billingPlan(arg) {/* 무시 */ } } // 클라이언트 2 const plan = aCustomer.billingPlan; // 클라이언트 3 aCustomer.billingPlan = newPlan;
- 특이 케이스 객체는 값 객체다. 따라서 항상 불변이어야 한다. 대체하려는 값이 가변이더라도 마찬가지이다.
- 마지막 상황은 좀 더 복잡하다. 특이 케이스가 자신만의 속성을 갖는 또 다른 객체(Payment History)를 반환해야 하기 때문이다.
-
// 클라이언트 4 const weeksDelinquent = isUnknown(aCustomer) ? 0 : aCustomer.paymentHistory.weeksDelinquentInLastyear;
- 특이 케이스 객체가 다른 객체를 반환해야 한다면 그 객체 역시 특이 케이스여야 하는 것이 일반적이다.
- 그래서 NullPaymentHistory를 만들기로 했다.
-
class UnknownCustomer { ... get paymentHistory() { return new NullPaymentHistory(); } } class NullPaymentHistory { get weeksDelinquentInLastyear() { return 0; } } // 클라이언트 const weeksDelinquent = aCustomer.paymentHistory.weeksDelinquentInLastyear;
- 이런 식으로 계속해서 특이 케이스 검사 함수를 이용하는 곳이 남아 있다면 검사 함수를 인라인 해준다.
- 특이 케이스로부터 다른 클라이언트들과는 다른 무언가를 원하는 독특한 클라이언트가 있을 수 있다.
-
// 튀는 클라이언트 const name = !isUnknown(aCustomer) ? aCustomer.name : "미확인 거주자";
- isUnknown() 함수를 인라인 해주면 된다.
-
const name = aCustomer.isUnknown ? "미확인 거주자" : aCustomer.name;
예시 2: 객체 리터럴 이용하기
- 앞의 예처럼 단순한 값을 위해 클래스까지 동원하는 건 좀 과한 감이 있다.
- 하지만 고객 정보가 갱신될 수 있어서 클래스가 꼭 필요했다.
-
class Site { get customer() { 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 === "미확인 고객") ? CustomElementRegistry.billingPlans.basic : aCustomer.billingPlan; // 클라이언트 3 const weeksDelinquent = isUnknown(aCustomer) ? 0 : aCustomer.paymentHistory.weeksDelinquentInLastyear;
- [Step 1] 앞의 예와 같이, 먼저 Customer에 isUnknown() 속성을 추가하고 [Step 2] 이 필드를 포함하는 특이 케이스 객체를 생성한다.
- 차이점이라면 이번에는 특이 케이스가 리터럴이다.
-
class Customer { ... get isUnknown() { return false; } } function createUnknownCustomer() { return { isUnknown: true, }; }
- [Step 3] 특이 테이스 조건 검사 부분을 함수로 추출한다.
-
function isUnknown(arg) { if (arg === "미확인 고객"); } // 클라이언트 1 let customerName; if (isUnknown(aCustomer)) customerName = "거주자"; else customerName = aCustomer.name; // 클라이언트 2 const plan = isUnknown(aCustomer) ? CustomElementRegistry.billingPlans.basic : aCustomer.billingPlan; // 클라이언트 3 const weeksDelinquent = isUnknown(aCustomer) ? 0 : aCustomer.paymentHistory.weeksDelinquentInLastyear;
- [Step 4] 조건을 검사하는 코드와 Site 클래스에서 이 특이 케이스를 이용하도록 수정한다.
-
class Site { get customer() { return (this._customer === "미확인 고객") ? createUnknownCustomer() : this._customer; } } function isUnknown(arg) { return arg.isUnknown; }
- [Step 7] 각각의 표준 응답을 적절한 리터럴 값으로 대체한다.
-
function createUnknownCustomer() { return { isUnknown: true, name: "거주자", billingPlan: CustomElementRegistry.billingPlans.basic, paymentHistory: { weeksDelinquentInLastYear: 0, }, }; } // 클라이언트 1 const customerName = acustomer.name; // 이름 // 클라이언트 2 const plan = aCustomer.billingPlan; // 요금제 // 클라이언트 3 const weeksDelinquent = aCustomer.paymentHistory.weeksDelinquentInLastyear; // 납부 이력
예시 3: 변환 함수 이용하기
- 앞의 두 예는 모두 클래스와 관련 있지만, 변환 단계를 추가하면 같은 아이디어를 레코드에도 적용할 수 있다.
- 위에 설명된 과정과 비슷하므로 간단하게만 설명하겠다.
- 자세한 내용은 책을 참고해 주세요.
-
{ "name": "애크미 보턴", "location": "Malden MA", "customer": { "name": "애크미 산업", "billingPlan": "plan-451", "paymentHistory": { "weeksDelinquentInLastYear": 7 } }
- 고객이 알려지지 않은 경우도 있을 텐데, 앞서와 똑같이 "미확인 고객"으로 표시하자
-
{ "name": "애크미 보턴", "location": "Malden MA", "customer": "미확인 고객" }
- 클라이언트는 다음과 같다.
- Json 데이터를 읽어서 customer 객체를 가져와서 이름을 확인한다.
-
// 클라이언트 const rawSite = acquireSiteData(); const site = enrichSite(rawSite); // 깊은 복사 const aCustomer = site.customer; ... let customerName; if (aCustomer === "미확인 고객") customerName = "거주자"; else customerName = aCustomer.name;
- 이런 상황에서 우리는 위 과정을 거쳐서 enrichSite 함수를 다음과 같이 정의할 수 있다.
-
function enrichSite(aSite) { const result = _.cloneDeep(aSite) // 깊은 복사 const unknownCustomer = { isUnknown: true, name: "거주자", billingPlan: CustomElementRegistry.billingPlans.basic, }; if (isUnknown(result.customer)) result.customer = unknownCustomer; else result.customer.isUnknown = false; return result; }
- 그러면 위 클라이언트 부분을 다음과 같이 수정할 수 있다.
-
// 클라이언트 const rawSite = acquireSiteData(); const site = enrichSite(rawSite); // 깊은 복사 const aCustomer = site.customer; ... const customerName = aCustomer.name;
출처:
- [리팩터링 2판]
반응형
'개발 > Refactoring' 카테고리의 다른 글
[리팩터링] 11.8 생성자를 팩터리 함수로 바꾸기 Replace Constructor with Factory Function (0) | 2022.01.25 |
---|---|
[리팩터링] 11.5 매개변수를 질의 함수로 바꾸기 Replace parameter with Query (0) | 2022.01.18 |
[리팩토링] Replace Inline Code with Function Call 인라인 코드를 함수 호출로 바꾸기 (0) | 2021.11.20 |
[리팩터링] Move Field 필드 옮기기 (0) | 2021.11.20 |
[리팩토링] Substitute Algorithm 알고리즘 교체하기 (0) | 2021.11.08 |
Comments