# 03-16. 임시 필드
# ✋ Intro
- 간혹 특정 상황에서만 값이 설정되는 필드를 가지는 클래스가 있다.
- 하지만 객체를 가져올 때는 당연히 모든 필드가 채워져 있으리라 기대하는게 보통이지만 쓰이지 않는 것처럼 보이는 임시 필드가 존재한다.
- 이러한 필드들을 발견하면 '클래스 추출하기'로 제 살 곳을 찾아주고 그 후 '함수 옮기기'로 임시 필드들과 관련된 코드를 모조리 새 클래스에 몰아넣는다.
- 또한, 임시 필드들이 유효한지 확인한 후 동작하는 조건부 로직이 있을 수 있는데, '특이 케이스 추가하기'로 필드들이 유효하지 않을 때를 위한 대안 클래스를 만들어서 제거할 수 있다.
- 책에서는 클래스를 예시로 설명하고 있는데 객체인 경우에도 해당 경우를 생각해볼 수 있을 것 같다.
const obj = {
id: 123,
name: 'wally',
age: 28,
};
if (office === 'abc.com') {
obj.member_access_unique_id = `abc_${obj.id}`
} else if (office === 'def.go.kr') {
obj.gov_auth_key = `${getAuthKey(obj)}_def`
}
// 하략
- 위 코드와 같이 특정 케이스에 대해서만 고려되는 필드들을 추가하고 관리하는 코드를 종종 볼 수 있다.
- 변수명이라도 시멘틱하게 잘 지으면 코드를 이어받아서 다른 사람이 작업할 때 그나마 머리를 싸맬 필요가 없을 수 있다.
- 하지만
obj.a
,obj.temp_key
와 같이 의미를 파악하기 어려운 네이밍으로 변수명을 지으면 어떤 의미인지 파악하기 어려워 작업하는데 해맬 수도 있다.
- 그래서 특정 상황에서만 쓰이는 key 값을 무한으로 증식해서 관리하지 않고 공통으로 사용할 수 있는 변수로 묶어서 관리하고 위 코드의
if
문 처럼 조건식에 걸리지 않는 경우 empty string이나null
값으로 기본값을 설정하는 방식이 좋을 것 같다.
# (1) 특이 케이스 추가하기
- 특수한 경우의 공통 동작은 요소 하나에 모아서 사용하는 특이 케이스 패턴이라는 것이 있는데, 이 때 적용하면 좋은 리팩토링 기법 중 하나이다.
- 특이 케이스 객체에서 단순히 데이터를 읽기만 한다면 반환할 값들을 담은 리터럴 객체 형태로 준비하면 된다.
- 만약 그 이상의 어떤 동작을 수행한다면 필요한 메서드를 담은 객체를 생성하면 된다.
- 특이 케이스 객체는 이를 캡슐화한 클래스가 반환하도록 만들 수도 있고, 변환을 거쳐 데이터 구조에 추가시키는 형태도 될 수 있다.
- 가장 흔한 특이 케이스 패턴으로 널 객체 패턴(Null Object Pattern)이 있다.
# ➕ 널 객체 패턴(Null Object Pattern)
인터페이스는 구현하지만 아무 일도 하지 않는 객체
- Null 객체 패턴은 GoF의 디자인 패턴 목록에는 없는 패턴이다.
- 하지만 코딩을 하다 보면 자연스럽게 터득해 사용하게 되는 기법이기도 하다.
- 구현의 심플함에 비해 유용하고, 나름의 중요한 의미가 있는 패턴이다.
- 자바에서 흔히 마주칠 수 있는 에러가
NullPointerException
이다.- 이를 방지하기 위해
if(object!=null)
등의 코드를 활용하게 된다. - 특정 객체가 존재하지 않는다는 것을
null
이아닌 Null Object를 반환하여NullPointerException
을 방지하는 기법이 널 오브젝트 패턴이다.
- 이를 방지하기 위해
- 주의
- 이 패턴을 잘못 도입하면 예외나 에러를 탐지하기 어려워지는 경우가 있다.
- 도입했을 때 클래스와 코드가 마구 늘어난다면 이 패턴이 적절하지 않은 상황이거나 잘못 구현한 것이다.
📄 Reference
- https://johngrib.github.io/wiki/pattern/null-object/
- https://bb-library.tistory.com/207
- 리팩토링 적용 예시
class Site {
get customer() {
return this._customer;
}
}
class Customer {
get name() {
// 고객 이름
}
get billingPlan() {
// 요금제
}
set billingPlan(arg) {
// 요금제 setter
}
get paymentHistory() {
// 납부 이력
}
}
// 미확인 고객을 처리해야 하는 클라이언트가 아래와 같이 여러 개 있다고 가정하자.
// 고객 이름으로는 '거주자'를 사용하고, 기본 요금제를 청구하고, 연체 기간은 0주로 분류한다.
// 클라이언트1
const aCustomer = site.custoner;
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 weekDelinquent = (aCustomer === '미확인 고객') ? 0 : aCustomer.paymentHistory.weeksDelinquentInLastYear;
- 예시1) 클래스 생성 방식
// 미확인 고객인지를 나타내는 메서드를 고객 클래스에 추가
class Customer {
get isUnknown() {
return false;
}
}
// 그 후 미확인 고객 전용 클래스를 만듬
class UnknwonCustomer {
get isUnknown() {
return true;
}
}
// 여러 곳에서 똑같이 수정해야만 하는 코드를 별도 함수로 추출하여 한데 모음
// 지금 예에서는 특이 케이스인지 확인하는 코드가 추출 대상임
function isUnknown(arg) {
if (!((arg instancof Customer) || (arg === '미확인 고객'))) {
throw new Error(`잘못된 값과 비교: <${arg}>`);
}
return (arg === '미확인 고객');
}
// 추출한 함수를 클라이언트 코드에 적용
// 클라이언트1
const aCustomer = site.custoner;
let customerName;
if (isUnknown(aCustomer)) customerName = '거주자';
else customerName = aCustomer.name;
// 클라이언트2
const plan = isUnknown(aCustomer) ? registry.billingPlans.basic : aCustomer.billingPlan;
// 클라이언트3
if (!isUnknown(aCustomer)) aCustomer.billingPlan = newPlan;
// 클라이언트4
const weekDelinquent = isUnknown(aCustomer) ? 0 : aCustomer.paymentHistory.weeksDelinquentInLastYear;
// 특이 케이스일 때 Site 클래스가 UnknownCustomer 객체를 반환하도록 수정
class Site {
get customer() {
return (this._customer === '미확인 고객') ? new UnknownCustomer() : this._customer;
}
}
// isUnknown 함수 수정
function isUnknown(arg) {
if (!(arg instancof Customer || arg instanceof UnknownCustomer)) {
throw new Error(`잘못된 값과 비교: <${arg}>`);
}
return arg.isUnknown;
}
// 각 클라이언트에서 수행하는 특이 케이스 검사를 일반적인 기본값으로 대체할 수 있다면 이 검사 코드에서 여러 함수를 클래스로 묶기를 적용할 수 있다.
// 그러면 조건부 로직이 필요없게 된다.
class UnknwonCustomer {
get isUnknown() {
return true;
}
get name() {
return '거주자';
}
get billingPlan() {
return registry.billingPlans.basic;
}
set billingPlan(arg) {
// 우선 무시
}
get paymentHistory() {
return new NullPaymentHistory();
}
}
class NullPaymentHistory() {
get weeksDelinquentInLastYear() {
return 0;
}
}
// 클라이언트1
const aCustomer = site.customer;
const customerName = aCustomer.name;
// 클라이언트2 (읽는 경우)
const plan = aCustomer.billingPlan;
// 클라이언트3
aCustomer.billingPlan = newPlan;
// 클라이언트4
const weekDelinquent = aCustomer.paymentHistory.weeksDelinquentInLastYear;
// 특이 케이스로부터 다른 클라이언트와는 다른 무언가를 원하는 독특한 클라이언트가 있을 수 있다.
// ex) 미확인 고객의 이름으로 '거주자'를 사용하는 경우
// 이런 경우엔 원래의 특이 케이스 검사 코드를 유지해야 한다.
// isUnknown() 함수를 인라인 하고 모든 클라이언트를 수정했다면, 호출하는 곳이 없어진 전역 isUnknown() 함수를 죽은 코드 제거하기로 없애면 된다.
const name = aCustomer.isUnknown ? '미확인 거주자' : aCustomer.name;
- 예시2) 객체 리터럴 방식
- 만약 데이터 구조를 읽기만 한다면 클래스 대신 리터럴 객체를 사용해도 된다.
- 리터럴을 아래와 같이 사용하려면 불변으로 만들어야 한다.(
freeze()
메서드 이용)
function isUnknown(arg) {
return arg.isUnknown;
}
function createUnknownCustomer() {
return {
isUnknown: true,
name: '거주자',
billingPlan: registry.billingPlans.basic,
paymentHistory: {
weeksDelinquentInLastYear: 0,
}
}
}
class Customer {
get isUnknown() {
return false;
}
}
class Site {
get customer() {
return (this._customer === '미확인 고객') ? createUnknownCustomer() : this._customer;
}
}
// 클라이언트1
const aCustomer = site.customer;
const customerName = aCustomer.name;
// 클라이언트2
const plan = aCustomer.billingPlan;
// 클라이언트3
const weekDelinquent = aCustomer.paymentHistory.weeksDelinquentInLastYear;
- 예시3) 변환 함수 방식
- 변환 단계를 추가하면 같은 아이디어를 레코드에도 적용할 수 있다.
const rawSite = acquireSiteData();
const site = enrichSite(rawSite);
function isUnknown(aCustomer) {
if (aCustomer === '미확인 고객') {
return true;
} else {
return aCustomer.isUnknown;
}
}
function enrichSite(aSite) {
const result = _.cloneDeep(aSite);
const unknownCustomer = {
isUnknown: true,
name: '거주자',
billingPlan: registry.billingPlans.basic,
paymentHistory: {
weekDelinquentInLastYear: 0,
}
};
if (isUnknown(result.customer)) {
result.customer = unknownCustomer;
} else {
result.customer.isUnknown = false;
}
return result;
}
// 클라이언트1
const aCustomer = site.customer;
// 수 많은 코드 ...
const customerName = aCustomer.name;
// 클라이언트2
const plan = aCustomer.billingPlan;
// 클라이언트3
const weeksDelinquent = aCustomer.paymentHistory.weeksDelinquentInLastYear;