😀 책에서 기억하고 싶은 내용을 써보세요.

manager = aPerson.department.manager;
manager = aPerson.managr;

class Person {
	get manager() {return this.department.manager;}

 

배경

  • 모듈화 설계를 제대로 하는 핵심은 캡슐화다.
  • 캡슐화는 모듈들이 시스템의 다른 부분에 대해 알아야 할 내용을 줄여준다.
  • 캡슐화가 잘 되어 있다면 무언가를 변경해야 할 때 고려해야 할 모듈 수가 적어져서 코드를 변경하기가 훨씬 쉬워진다.
  • 객체 지향을 처음 배울 때는 캡슐화란 필드를 숨기는 것이라고 배운다. 그러다 경험이 쌓이면서 캡슐화의 역할이 그보다 많다는 사실을 깨닫는다.

서버 객체의 필드가 가리키는 객체(위임 객체)의 메서드를 호출하려면 클라이언트는 이 위임 객체를 알아야 한다. 위임 객체의 인터페이스가 바뀌면 이 인터페이스를 사용하는 모든 클라이언트가 코드를 수정해야 한다.

⇒ 이러한 의존성을 없애려면 서버 자체에 위임 메서드를 만들어서 위임 객체의 존재를 숨긴다.

그러면 위임 객체가 수정되도 서버 코드만 고치면 되며, 클라이언트는 아무런 영향을 받지 않는다.

 

 

 

절차

  1. 위임 객체의 각 메서드에 해당하는 위임 메서드를 서버에 생성한다.
  2. 클라이언트가 위임 객체 대신 서버를 호출하도록 수정한다. 하나씩 바꿀 때마다 테스트한다.
  3. 모두 수정했다면, 서버로부터 위임 객체를 얻는 접근자를 제거한다.
  4. 테스트한다.

 

 

예시

사람과 사람이 속한 부서를 다음처럼 정의했다.

// Person 클래스
constructor(name) {
	this._name = name;
}
get name()          {return this._name;}
get department()    {return this._department;}
set department(arg) {this._department = arg;}
// Department 클래스
get chargeCode()    {return this._chargeCode;}
set chargeCode(arg) {this._chargeCode = arg;}
get manager()       {return this._manager;}
set manager(arg)    {this._manager = arg;}

클라이언트에서 어떤 사람이 속한 부서의 관리자를 알고 싶은 상황

⇒ 그러기 위해서는 부서 객체로부터 얻어와야 한다.

 

// 클라이언트
manager = aPerson.department.manager;

부서 클래스가 관리자 정보를 제공하는 상황

 

1️⃣ 이러한 의존성을 줄이려면 클라이언트가 부서 클래스를 볼수 없게 숨기고, 대신 사람 클래스에 간단한 위임 메서드를 만들면 된다.

// Person 클래스
get manager() {return this._department.manager;}

 

2️⃣ 이제 모든 클라이언트가 이 메서드를 사용하도록 고친다.

// 클라이언트
manager = aPerson.manager;

3️⃣ 클라이언트 코드를 다 고쳤다면 사람 클래스의 department() 접근자를 삭제한다.

 

 

🤔 오늘 읽은 소감은? 떠오르는 생각을 가볍게 적어보세요.

  • 캡슐화를 단순히 이론으로만 배우는게 아니라, 예시와 설명이 담겨 있어서 좋았습니다.
    • 이론으로만 배우는데는 상당히 한계가 있는 개념이였습니다.

 

🔎 궁금한 내용이 있거나, 잘 이해되지 않는 내용이 있다면 적어보세요.

  •  
반응형

 

 

 

 

😀 책에서 기억하고 싶은 내용을 써보세요.

class Person {
  get officeAreaCode() {return this._telephoneNumber.areaCode;}
  get officeNumber()   {return this._telephoneNumber.number;}
}
class TelephoneNumber {
  get areaCode() {return this._areaCode;}
  get number()   {return this._number;}
class TelephoneNumber {
  get officeAreaCode() {return this._officeAreaCode;}
  get officeNumber()   {return this._officeNumber;}

 

 

배경

클래스 인라인하기는 클래스 추출하기를 거꾸로 돌리는 리팩터링이다.

  • 저자는 더 이상 제 역할을 못 해서 그대로 두면 안 되는 클래스는 인라인해버린다.
  • 역할을 옮기는 리팩터링을 하고나니 특정 클래스에 남은 역할이 거의 없을 때 이런 현상이 자주 생긴다. ⇒ 이럴 땐 이 불쌍한 클래스를 가장 많이 사용하는 클래스로 흡수시키자.

두 클래스의 기능을 지금과 다르게 배분하고 싶을 때도 클래스를 인라인한다.

  • 클래스를 인라인해서 하나로 합친 다음 새로운 클래스를 추출 하는 게 쉬울 수도 있기 때문 ⇒ 이는 코드를 재구성할 때 흔히 사용하는 방식이기도 하다.

 

 

절차

  1. 소스 클래스의 각 public 메서드에 대응하는 메서드들을 타깃 클래스에 생성한다. 이 메서드들은 단순히 작업을 소스 클래스로 위임해야 한다.
  2. 소스 클래스의 메서드를 사용하는 코드를 모두 타깃 클래스의 위임 메서드를 사용하도록 바꾼다. 하나씩 바꿀 때마다 테스트한다.
  3. 소스 클래스의 메서드와 필드를 모두 타깃 클래스로 옮긴다. 하나씩 옮길 때마다 테스트한다.
  4. 소스 클래스를 삭제하고 조의를 표한다.

 

예시

배송 추적 정보를 표현하는 TrackingInformation 클래스를 준비

class TrackingInformation {
  get shippingCompany()    {return this._shippingCompany;}  // 배송 회사
  set shippingCompany(arg) {this._shippingCompany = arg;}
  get trackingNumber()     {return this._trackingNumber;}   // 추적 번호
  set trackingNumber(arg)  {this._trackingNumber = arg;}
  get display() {
   return `${this.shippingCompany}: ${this.trackingNumber}`;
 }
}

이 클래스는 배송 클래스의 일부처럼 사용된다.

 

// Shipment 클래스
get trackingInfo() {
  return this._trackingInformation.display;
}
get trackingInformation()  {return this._trackingInformation;}
set trackingInformation(aTrackingInformation) {
  this._trackingInformation = aTrackingInformation;
}

TrackingInformation이 예전에는 유용했을지 몰라도 현재는 제 역할을 못 하고 있으니

Shipment 클래스로 인라인하려 한다.

 

먼저 TrackingInformation의 메서드를 호출하는 코드를 찾는다.

// 클라이언트
aShipment.trackingInformation.shippingCompany = request.vendor;

 

1️⃣ 이처럼 외부에서 직접 호출하는 TrackingInformation의 메서드들을 모조리 Shipment로 옮긴다.

그런데 보통 때의 함수 옮기기와는 약간 다르게 진행해보자. 먼저 Shipment에 위임 함수를 만들고

2️⃣ 클라이언트가 이를 호출하도록 수정하는 것이다.

// Shipment 클래스
set shippingCompany(arg) {this._trackingInformation.shippingCompany = arg;}
// 클라이언트
aShipment.shippingCompany = request.vendor;

 

3️⃣ 다 고쳤다면 TrackingInformation의 모든 요소를 Shipment로 옮긴다.

먼저 display() 메서드를 인라인한다.

// Shipment 클래스
get trackingInfo() {
  return `${this.shippingCompany}: ${this.trackingNumber}`;
}

 

다음은 배송 회사 필드 차례다.

// Shipment 클래스
get shippingCompany()    {return this._shippingCompany;}
set shippingCompany(arg) {this._shippingCompany = arg;}

 

여기서는 이동할 목적지인 Shipment에서 shippingCompany()만 참조하므로 필드 옮기기의 절차를 모두 수행하지 않아도 된다. 그래서 타깃을 참조하는 링크를 소스에 추가하는 단계는 생략한다.

이 과정을 반복하고, 4️⃣ 다 옮겼다면 TrackingInformation 클래스를 삭제한다.

// Shipment 클래스
get trackingInfo() {
  return `${this.shippingCompany}: ${this.trackingNumber}`;
}
get shippingCompany()     {return this._shippingCompany;}
set shippingCompany(arg)  {this._shippingCompany = arg;}
get trackingNumber()      {return this._trackingNumber;}
set trackingNumber(arg)   {this._tackingNumber = arg;}

 

 

 

🤔 오늘 읽은 소감은? 떠오르는 생각을 가볍게 적어보세요.

  • 결합도가 높고, 복잡하게 엉켜있던 클래스의 메서드를 옮겼던 것이 생각납니다.
    • 그에 비해 아무래도 예시의 코드는 간결하기 때문에 옮기기 쉬워 보인다고 느꼈습니다.

 

 

🔎 궁금한 내용이 있거나, 잘 이해되지 않는 내용이 있다면 적어보세요.

  • 회사를 다니면서 위와 같이 클래스를 인라인 하는 일이 많을지 궁금합니다.
    • 개인적으로 많이 없을 것이라고 생각해서 위와 같은 질문이 나온 것 같습니다.

 

 

 

반응형

 

 

 

😀 책에서 기억하고 싶은 내용을 써보세요.

class Person {
	get officeAreaCode() {return this._officeAreaCode;}
	get officeNumber() {return this._officeNumber;}
}
class Person {
	get officeAreaCode() {return this._telephoneNumber.areaCode;}
	get officeNumber() {return this._telephoneNumber.number;}
}
class TelephoneNumber {
	get areaCode() {return this._areaCode;}
	get number() {return this._number;}
}

 

배경

  • 메서드와 데이터가 너무 많은 클래스는 이해하기가 쉽지 않으니 잘 살펴보고 적절히 분리하는 것이 좋다.
  • 특히 일부 데이터와 메서드를 따로 묶을 수 있다면 어서 분리하라는 신호다.
  • 함께 변경되는 일이 많거나 서로 의존하는 데이터들도 분리한다.
  • 특정 데이터나 메서드 일부를 제거하면 어떤 일이 일어나는지 자문해보면 판단에 도움이 된다. ⇒ 제거해도 다른 필드나 메서드들이 논리적으로 문제가 없다면 분리할 수 있다는 뜻이다.

 

절차

  1. 클래스의 역할을 분리할 방법을 정한다.
  2. 분리된 역할을 담당할 클래스를 새로 만든다.
    • 원래 클래스에 남은 역할과 클래스 이름이 어울리지 않는다면 적절히 바꾼다.
  3. 원래 클래스의 생성자에서 새로운 클래스의 인스턴스를 생성하여 필드에 저장해둔다.
  4. 분리될 역할에 필요한 필드들을 새 클래스로 옮긴다(필드 옮기기). 하나씩 옮길 때마다 테스트
  5. 메서드들도 새 클래스로 옮긴다(함수 옮기기). 이때 저수준 메서드, 즉 다른 메서드를 호출하기보다는 호출을 당하는 일이 많은 메서드부터 옮긴다. 하나씩 옮길 때마다 테스트한다.
  6. 양쪽 클래스의 인터페이스를 살펴보면서 불필요한 메서드를 제거하고, 이름도 새로운 환경에 맞게 바꾼다.
  7. 새 클래스를 외부로 노출할지 정한다. 노출하려거든 새 클래스에 참조를 값으로 바꾸기를 적용할지 고민해본다.

 

예시

단순한 Person 클래스

// Person 클래스
get name()              {return this._name;}
get name(arg)           {this._name = arg;}
get telephoneNumber()   {return `(${this.officeAreaCode}) ${this.officeNumber}`;}
get officeAreaCode()    {return this._officeAreaCode;}
set officeAreaCode(arg) {return this._officeAreaCode = arg;}
get officeNumber()      {return this._officeNumber;}
set officeNumber(arg)   {this._officeNumber = arg;}

 

1️⃣ 전화번호 관련 동작을 별도 클래스로 추출

2️⃣ 먼저 빈 전화번호를 표현하는 TelephoneNumber 클래스를 정의

class TelephoneNumber {
}

 

3️⃣ 다음으로 Person 클래스의 인스턴스를 생성할 때 전화번호 인스턴스도 함께 생성해 저장

// Person 클래스
constructor() {
	this._telephoneNumber = new TelephoneNumber();
}
// TelephoneNumber 클래스
get officeAreaCode()    {return this._officeAreaCode;}
set officeAreaCode(arg) {this._officeAreaCode = arg;}

 

4️⃣ 필드들을 하나씩 새 클래스로 옮긴다.

// Person 클래스
get officeAreaCode() {return this._telephoneNumber.officeAreaCode;}
set officeAreaCode() {this._telephoneNumber.officeAreaCode = arg;}

테스트해서 문제없으면 다음 필드로 넘어간다.

 

// TelephoneNumber 클래스
get officeNumber()    {return this._officeNumber;}
set officeNumber(arg) {this._officeNumber = arg;}
// Person 클래스
get officeNumber()    {return this._telephoneNumber.officeNumber;}
set officeNumber(arg) {this._telephoneNumber.officeNumber = arg;}

 

다시 테스트해보고, 5️⃣ 이어서 telephoneNumber() 메서드를 옮긴다.

// TelephoneNumber 클래스
get telephoneNumber() {return `(${this.officeAreaCode}) ${this.officeNumber}`;}
// Person 클래스
get telephoneNumber() {return this._telephoneNumber.telephoneNumber;}

 

6️⃣ 정리 단계

  • 새로 만든 클래스는 전화번호를 뜻하므로 사무실(office)이란 단어를 쓸 이유가 없다.
  • 마찬가지로 전화번호라는 뜻도 메서드 이름에서 다시 강조할 이유가 없다.

메서드들의 이름을 적절히 바꿔준다(함수 선언 바꾸기)

// TelephoneNumber 클래스
get areaCode()    {return this._areaCode;}
set areaCode(arg) {this._areaCode = arg;}
get number()      {return this._number;}
set number(arg)   {this._number = arg;}
// Person 클래스
get officeAreaCode()    {return this._telephoneNumber.areaCode;}
set officeAreaCode(arg) {this._telephoneNumber.areaCode = arg;}
get officeNumber()      {return this._telephoneNumber.number;}
set officeNumber(arg)   {this._telephoneNumber.number = arg;}

 

마지막으로 전화번호를 사람이 읽기 좋은 포맷으로 출력하는 역할도 전화번호 클래스에 맡긴다.

// TelephoneNumber 클래스
toString() {return `(${this.areaCode}) ${this.number}`;}
// Person 클래스
get telephoneNumber() {return this._telephoneNumber.toString();}

 

7️⃣ 전화번호는 여러모로 쓸모가 없으니 이 클래스는 클라이언트에게 공개하는 것이 좋겠다.

그러면 “office”로 시작하는 메서드들을 없애고 TelephoneNumber의 접근자를 바로 사용하도록 바꿀 수 있다. 그런데 기왕 이렇게 쓸 거라면 전화번호를 값 객체로 만드는게 나으니 참조를 값으로 바꾸기부터 적용한다.

 

 

🤔 오늘 읽은 소감은? 떠오르는 생각을 가볍게 적어보세요.

  • 다양한 역할을 같이 수행하는 클래스는 정말 코드를 이해하기 힘들다.
  • 공통된 업무를 하는 메서드를 모아놓은 클래스는 재사용성이 정말 좋았다.
  • static으로 정의된 메서드를 보면 서로 결합성이 높은 것을 많이 봤습니다. ⇒ 그래서 회사에서 코드를 작성하면 우선 static을 최소화 하면서 사용할려고 하고 있습니다.

 

🔎 궁금한 내용이 있거나, 잘 이해되지 않는 내용이 있다면 적어보세요.

  • 규모가 점점 커질수록 클래스에 다양한 기능을 집어넣는 경우를 보았다. 회사에서 이런 코드를 보면 어떻게 해야 할까?
    • 우선 리팩터링을 하기에는 회사에서의 승인을 받아야 하는 문제 발생
    • 회사에서 새기능을 구현해야 하는데도 시간이 부족한데 리팩터링을 할 수 있을지 고민

 

 

반응형

 

 

😀 책에서 기억하고 싶은 내용을 써보세요.

리팩터링 전

const basePrice = this._quantity * this._itemPrice;
if (basePrice > 1000)
	return basePrice * 0.95;
else
	return basePrice * 0.98;

 

리팩터링 후

get basePrice() {this._quantity * this._itemPrice;}
...
if (this.basePrice > 1000)
	return this.basePrice * 0.95;
else
	return this.basePrice * 0.98;

 

 

 

배경

  • 함수 안에서 어떤 코드의 결괏값을 뒤에서 다시 참조할 목적으로 임시 변수를 쓰기도 한다.
  • 임시 변수를 사용하면 값을 계산하는 코드가 반복되는 걸 줄이고 (변수 이름을 통해) 값의 의미를 설명할 수도 있어서 유용하다.

 

그런데 한 걸음 더 나아가 아예 함수로 만들어 사용하는 편이 나을 때가 많다.

  • 긴 함수의 한 부분을 함수로 추출하고자 할 때 먼저 변수들을 각각의 함수로 만들면 일이 수월해진다.
  • ⇒ 추출한 함수에 변수를 따로 전달할 필요가 없어지기 때문
  • 추출한 함수와 원래 함수의 경계가 더 분명해진다.
  • ⇒ 부자연스러운 의존 관계나 부수효과를 찾고 제거하는 데 도움이 된다.
  • 변수 대신 함수로 만들어두면 비슷한 계산을 수행하는 다른 함수에서도 사용할 수 있어 코드 중복이 줄어든다.
  • ⇒ 여러 곳에서 똑같은 방식으로 계산되는 변수를 발견할 때마다 함수로 바꿀 수 있는지 확인

 

이번 리팩터링은 클래스 안에서 적용할 때 효과가 가장 크다.

  • 클래스는 추출할 메서드들에 공유 컨텍스트를 제공하기 때문
  • 클래스 바깥의 최상위 함수로 추출하면 매개변수가 너무 많아져서 함수를 사용하는 장점이 줄어든다.
  • 중첩 함수를 사용하면 이런 문제는 없지만 관련 함수들과 로직을 널리 공유하는 데 한계가 있다.

 

임시 변수를 질의 함수로 바꾼다고 다 좋아지는건 아니다.

  • 자고로 변수는 값을 한 번만 계산하고, 그 뒤로는 읽기만 해야 한다.
  • 가장 단순한 예로, 변수에 값을 한 번 대입한 뒤 더 복잡한 코드 덩어리에서 여러 차례 다시 대입하는 경우는 모두 질의 함수로 추출해야 한다.
  • 계산 로직은 변수가 다음번에 사용될 때 수행해도 똑같은 결과를 내야 한다. ⇒ 그래서 ‘옛날 주소’ 처럼 스냅숏 용도로 쓰이는 변수에는 이 리팩터링을 적용하면 안 된다.

 

절차

  1. 변수가 사용되기 전에 값이 확실히 결정되는지, 변수를 사용할 때마다 계산 로직이 매번 다른 결과를 내지는 않는지 확인한다.
  2. 읽기 전용으로 만들 수 있는 변수는 읽기 전용으로 만든다.
  3. 테스트한다.
  4. 변수 대입문을 함수로 추출한다.
    • 변수와 함수가 같은 이름을 가질 수 없다면 함수 이름을 임시로 짓는다. 또한, 추출한 함수가 부수효과를 일으키지는 않는지 확인한다. 부수효과가 있다면 질의 함수와 변경 함수 분리하기로 대처한다.
  5. 테스트한다.
  6. 변수 인라인하기로 임시 변수를 제거한다.

 

예시

간단한 주문 클래스

// Order 클래스
constructor(quantity, item) {
	this._quantity = quantity;
	this._item = item;
}

get price() {
	var basePrice = this._quantity * this._item.price;
	var discountFactor = 0.98;

	if (basePrcie > 1000) discountFactor -= 0.03;
	return basePrice * discountFactor;
}

 

여기서 임시 변수인 basePrice와 discountFactor를 메서드로 바꿔보자.

2️⃣ 먼저 basePrice에 const를 붙여 읽기 전용으로 만들고 3️⃣ 테스트해본다.

  • 못 보고 지나친 재대입 코드를 찾을 수 있다.
  • ⇒ 컴파일 에러 발생
// Order 클래스
constructor(quantity, item) {
	this._quantity = quantity;
	this._item = item;
}

get price() {
	const basePrice = this._quantity * this._item.price;
	var discountFactor = 0.98;
	if (basePrice > 1000) discountFactor -= 0.03;
	return basePrice * discountFacotr;
}

 

4️⃣ 그런 다음 대입문의 우변을 게터로 추출한다.

// Order 클래스...
	const basePrice = this.basePrice;
	var discountFactor = 0.98;
	if (basePrice > 1000) discountFacotr -= 0.03;
	return basePrice * discountFactor;
}

get basePrice() {
	return this._quantity * this._item.price;
}

 

5️⃣ 테스트한 다음 6️⃣ 변수를 인라인 한다.

// Order 클래스...
get price() {
	var discountFactor = 0.98;
	if (this.basePrice > 1000) discountFactor -= 0.03;
	return this.basePrice * discountFactor;	
}

 

discountFactor 변수도 같은 순서로 처리한다. 4️⃣ 먼저 함수 추출하기다.

// Order 클래스...
get price() {
	const discountFactor = this.discountFactor;
	return this.basePrice * discountFactor;
}

get discountFactor() {
	var discountFactor = 0.98;
	if (this.basePrice > 1000) discountFactor -= 0.03;
	return discountFactor;
}

 

이번에는 discountFactor에 값을 대입하는 문장이 둘인데, 모두 추출한 함수에 넣어야 한다.

2️⃣ 원본 변수는 마찬가지로 const로 만든다.

6️⃣ 마지막으로 변수 인라인 차례다.

// Order 클래스...
get price() {
	return this.basePrice * this.discountFactor;
}

 

 

🤔 오늘 읽은 소감은? 떠오르는 생각을 가볍게 적어보세요.

  • getter 접근자 메서드를 단순히 해당 값을 읽어오는 것으로 사용하는 게 아니라, 똑같은 방식으로 계산되어 같은 값이 나오는 것들을 묶어 줄 수 있다는 것을 알게 되었습니다(캡슐화). ⇒ 이 방법을 통해 유저 혹은 클라이언트는 내부 구조를 알 필요 없이 해당 함수를 가져다 사용할 수 있습니다. (유저의 편의성 증대)

 

 

🔎 궁금한 내용이 있거나, 잘 이해되지 않는 내용이 있다면 적어보세요.

  • 캡슐화와 추상화의 차이는?
    • 캡슐화와 추상화는 개발 비용을 낮춰주는 객체지향의 두 가지 특징이다.
    • 캡슐화는 기능 구현을 외부로부터 감추고, 내부의 구현 병경이 외부로 전파되는 것을 막아줍니다.
    • 추상화는 의존 대상을 추상 타입으로 간접 참조하고, 사용하고 있는 의존 대상의 변경이 사용하는 입장에는 영향을 주지 않습니다.
    • 둘은 상호 보완적인 개념입니다. 추상화: 객체의 동작, 기능 자체에 중점을 둔다. 캡슐화: 객체 내부 상태에 대한 정보를 숨기는 방식으로 이루어진다.

 

 

 

반응형

 

 

😀 책에서 기억하고 싶은 내용을 써보세요.

orders.filter(o => "high" === o.priority
                || "rush" === o.priority);
orders.filter(o => o.priority.higherThan(new Priority("normal")))

 

 

배경

단순한 출력 이상의 기능이 필요해지는 순간 그 데이터를 표현하는 전용 클래스를 정의한다.

시작은 기본형 데이터를 단순히 감싼 것과 큰 차이가 없을 것이라 효과가 미미하다.

하지만 나중에 특별한 동작이 필요해지면 이 클래스에 추가하면 되니 프로그램이 커질수록 점점 유용한 도구가 된다.

 

 

 

절차

  1. 아직 변수를 캡슐화하지 않았다면 캡슐화한다.
  2. 단순한 값 클래스를 만든다. 생성자는 기존 값을 인수로 받아서 저장하고, 이 값을 반환하는 게터를 추가한다.
  3. 정적 검사를 수행한다.
  4. 값 클래스의 인스턴스를 새로 만들어서 필드에 저장하도록 세터를 수정한다. 이미 있다면 필드의 타입을 적절히 변경한다.
  5. 새로 만든 클래스의 게터를 호출한 결과를 반환하도록 게터를 수정한다.
  6. 테스트한다.
  7. 함수 이름을 바꾸면 원본 접근자의 동작을 더 잘 드러낼 수 있는지 검토한다.
    • 참조를 값으로 바꾸거나 값을 참조로 바꾸면 새로 만든 객체의 역할(값 또는 참조 객체)이 더 잘 드러나는지 검토한다.

 

 

예시

레코드 구조에서 데이터를 읽어 들이는 단순한 주문 클래스를 살펴보자.

// Order 클래스
constructor(data) {
	this.priority = data.priority;
	// 나머지 초기화 코드 생략

클라이언트에서는 이 코드를 다음처럼 사용한다.

 

// 클라이언트
highPriorityCount = orders.filter(o => "high" === o.priority
                                    || "rush" === o.priority)
                          .length;

 

1️⃣ 데이터 값을 다루기 전에 항상 변수부터 캡슐화한다.

// Order 클래스
get priority() {return this._priority;}
set priority(aString) {this._priority = aString;}

이제 우선순위 속성을 초기화하는 생성자에서 방금 정의한 세터를 사용할 수 있다.

⇒ 이렇게 필드를 자가 캡슐화하면 필드 이름을 바꿔도 클라이언트 코드는 유지할 수 있다.

 

2️⃣ 우선순위 속성을 표현하는 값 클래스 Priority를 만든다.

⇒ 이 클래스는 표현할 값을 받는 생성자와 그 값을 문자열로 반환하는 변환 함수로 구성된다.

class Priority {
	constructor(value) {this._value = value;}
	toString() {return this._value;}
}

 

4️⃣ 5️⃣ 그런 다음 방금 만든 Priority 클래스를 사용하도록 접근자들을 수정한다.

// Order 클래스
get priority() {return this._priority.toString();}
set priority(aString) {this._priority = new Priority(aString);}

 

7️⃣ 이렇게 Priority 클래스를 만들고 나면 Order 클래스의 게터가 이상해진다. 이 게터가 반환하는 값은 우선순위 자체가 아니라 우선순위를 표현하는 문자열이다. 그러니 즉시 함수 이름을 바꿔준다.

// Order 클래스
get priorityString() {return this._priority.toString();}
set priority(aString) {this._priority = new Priority(aString);}
// 클라이언트
highPriorityCount = orders.filter(o => "high" === o.priorityString
                                    || "rust" === o.priorityString)
                          .length;
  • 지금처럼 매개변수 이름만으로 세터가 받는 데이터의 유형을 쉽게 알 수 있다면 세터의 이름을 그대로 둬도 좋다.

 

 

🤔 오늘 읽은 소감은? 떠오르는 생각을 가볍게 적어보세요.

  • 변수 캡슐화를 먼저 진행하고 그 다음에 컬렉션 캡슐화 진행
    • getter, setter 접근자 메서드 캡슐화를 진행
    • 여기서 한번 더 고려할 것은 세터를 통해 클라이언트 누구든 수정을 가능하게 할 것인지에 대한 것
    • ⇒ 데이터를 수정하거나 추가하는 부분은 한번 더 생각해보고 신중하게 결정

 

🔎 궁금한 내용이 있거나, 잘 이해되지 않는 내용이 있다면 적어보세요.

  • 캡슐화의 개념을 간결하게 설명한 글은 없을지 찾아보자
    • 캡슐화란 관련 있는 자료, 동작들을 하나로 묶어 요약하고 사용자에게는 배우적인 접근을 허용하지 않는 대신에 사용의 편의성을 제공해주는 것이다.
    • 여러 자료 및 처리과정을 하나의 모듈(부품)처럼 사용하므로 객체간의 이식성이 높아진다.
    • 참고한 링크 ⇒ 링크

 

 

반응형

 

😀 책에서 기억하고 싶은 내용을 써보세요.

class Person {
	get courses() {return this._courses;}
	set courses() {this._courses = aList;}
}
class Person {
	get courses() {return this._courses.slice();}
	addCourse(aCourse) { ... }
	removeCourse(aCourse) { ... }

 

 

배경

  • 저자는 가변 데이터를 모두 캡슐화하는 편이다.
    • 데이터 구조가 언제 어떻게 수정되는지 파악하기 쉬워서 필요한 시점에 데이터 구조를 변경하기도 쉬워진다.
    • 객체 지향 개발자들은 캡슐화를 적극 권장한다.

 

컬렉션을 다룰 때 하는 실수

  • 컬렉션 변수로의 접근을 캡슐화하면서 게터가 컬렉션 자체를 반환하도록 하면, 그 컬렉션을 감싼 클래스가 눈치채지 못하는 상태에서 컬렉션의 원소들이 바뀌어버린다
    • 이런 문제를 방지하기 위해 컬렉션을 감싼 클래스에 add()와 remove()라는 이름의 컬렉션 변경자 메서드를 만든다. ⇒ 항상 컬렉션을 소유한 클래스를 통해서만 원소를 변경하도록 한다.

 

 

내부 컬렉션을 직접 수정하지 못하게 막는 방법1

  • 절대로 컬렉션을 반환하지 않게 하는 방법
  • 컬렉션에 접근하려면 컬렉션이 소속된 클래스의 적절한 메서드를 반드시 거치게 하는 것
  • ex) aCustomer.orders.size() 처럼 접근하는 코드를 aCustomer.numberOfOrders()로 바꾸는 것
  • 저자가 선호하지 않는 방법

 

 

내부 컬렉션을 직접 수정하지 못하게 막는 방법2

  • 컬렉션을 읽기전용으로 제공하기
  • 자바에서는 컬렉션의 읽기전용 프락시를 반환하게 만들기가 쉽다.
    • 프락시가 내부 컬렉션을 읽는 연산은 그대로 전달하고, 쓰기는 모두 막는 것.
  • 이터레이터(iterator)나 열거형 객체를 기반으로 컬렉션을 조합하는 라이브러리들도 비슷한 방식을 쓴다.

 

 

내부 컬렉션을 직접 수정하지 못하게 막는 방법3

  • 가장 흔히 사용하는 방식은 컬렉션 데터를 제공하되 내부 컬렉션의 복제본을 반환하는 방식
  • 복제본을 수정해도 캡슐화된 원본 컬렉션에는 아무런 영향을 주지 않는다.
  • 컬렉션이 상당히 크다면 성능 문제가 발생 할 수 있지만 성능에 지장을 줄만큼 컬렉션이 큰 경우는 별로 없었다.

 

 

절차

  1. 아직 컬렉션을 캡슐화하지 않았다면 변수 캡슐화하기부터 한다.
  2. 컬렉션에 원소를 추가/제거하는 함수를 추가한다.
    • 컬렉션 자체를 통째로 바꾸는 세터는 제거한다. 세터를 제거할 수 없다면 인수로 받은 컬렉션을 복제해 저장하도록 만든다.
  3. 정적 검사를 수행한다.
  4. 컬렉션을 참조하는 부분을 모두 찾는다. 컬렉션의 변경자를 호출하는 코드가 모두 앞에서 추가한 추가/제거 함수를 호출하도록 수정한다. 하나씩 수정할 때마다 테스트한다.
  5. 컬렉션 게터를 수정해서 원본 내용을 수정할 수 없는 읽기전용 프락시나 복제본을 반환.
  6. 테스트한다.

 

 

예시

수업 목록을 필드로 지니고 있는 Person 클래스를 예로 살펴보자.

// Person 클래스
constructor(name) {
	this._name = name;
	this._courses = [];
}

get name() {return this._name;}
get courses() {return this._courses;}
get courses(aList) {this._courses = aList;}
// Course 클래스
constructor(name, isAdvanced) {
	this._name = name;
	this._isAdvanced = isAdvanced;
}
get name() {return this._name;}
get isAdvanced() {return this._isAdvanced;}

 

클라이언트는 Person이 제공하는 수업 컬렉션에서 수업 정보를 얻는다.

numAdvancedCourses = aPerson.courses
		.filter(c => c.isAdvanced)
		.length
;

 

모든 필드가 접근자 메서드로 보호받고 있으니 안이한 개발자는 이렇게만 해도 데이터를 제대로 캡슐화 했다고 생각하기 쉽다. 하지만 허점이 있다. 세터를 이용해 수업 컬렉션을 통째로 설정한 클라이언트는 누구든 이 컬렉션을 마음대로 수정할 수 있기 때문이다.

// 클라이언트
const basicCourseNames = readBaiscCourseNames(filename);
aPerson.courses = basicCourseNames.map(name => new Course(name, false));

 

 

클라이언트 입장에서는 다음처럼 수업 목록을 직접 수정하는 것이 훨씬 편할 수 있다.

// 클라이언트
for(const name of readBasicCourseNames(filename) {
	aPerson.courses.push(new Course(name, false));
}
  • 위와 같은 방식은 Person 클래스가 더는 컬렉션을 제어할 수 없어서 캡슐화가 깨진다.
  • 필드를 참조하는 과정한 캡슐화했을 뿐 필드에 담긴 내용은 캡슐화화지 않은게 원인이다.

 

 

2️⃣ 제대로 캡슐화하기 위해 먼저 클라이언트가 수업을 하나씩 추가하고 제거하는 메서드를 Person 에 추가해보자.

// Person 클래스
addCourse(aCourse) {
	this._courses.push(aCourse);
}
removeCourse(aCourse, fnIfAbsent = () => {throw new RangeError();}) {
	const index = this._courses.indexOf(aCourse);
	if (index === -1) fnIfAbsent();
	else this._courses.splice(index, 1);
}

제거 메서드에서는 클라이언트가 컬렉션에 없는 원소를 제거하려 할 때의 대응 방식을 정해야 한다.

그냥 무시하거나 에러를 던질 수도 있다. 여기서는 기본적으로 에러를 던지되, 호출자가 원하는 방식으로 처리할 여지도 남겨뒀다.

 

 

4️⃣ 그런 다음 컬렉션의 변경자를 직접 호출하던 코드를 모두 찾아서 방금 추가한 메서드를 사용

// 클라이언트
for (const name of readBasicCourseNames(filename)) {
	aPerson.addCourse(new Course(name, false));
}

 

2️⃣ 이렇게 개별 원소를 추가하고 제거하는 메서드를 제공하기 때문에 setCourses()를 사용할 일이 없어졌으니 제거한다(세터 제거하기). 세터를 제공해야 할 특별한 이유가 있다면 인수로 받은 컬렉션의 복제본을 필드에 저장하게 한다.

// Person 클래스
set courses(aList) {this._courses = aList.slice();}

이렇게 하면 클라이언트는 용도에 맞는 변경 메서드를 사용하도록 할 수 있다.

 

5️⃣ 하지만 저자는 이 메서드들을 사용하지 않고서는 아무도 목록을 변경할 수 없게 만드는 방식을 선호한다. 다음과 같이 본제본을 제공하면 된다.

// Person 클래스
get courses() {return this._courses.slice();}
  • 저자의 경험에 따르면 컬렉션에 대해서는 어느 정도 강박증을 갖고 복제본을 만드는 편이 예상치 못한 수정이 촉발한 오류를 디버깅하는 것보다 낫다.

 

 

🤔 오늘 읽은 소감은? 떠오르는 생각을 가볍게 적어보세요.

  • 기존에 게터를 사용해서 객체를 그대로 반환하고 세터로 수정하는 방식을 사용했었습니다.
    • 복제본을 제공해서 원본을 수정하지 못하게 막는 방식은 처음 봤습니다.
    • 책에서는 접근자 메서드로만으로 캡슐화 했다고 생각하면 안이한 개발자라고 한다.
  • 습관이 아닌 코드로 하지 말아야 할 것들을 미리 방지

 

 

 

🔎 궁금한 내용이 있거나, 잘 이해되지 않는 내용이 있다면 적어보세요.

  • 컬렉션이란?
    • 객체의 모음, 그룹
    • 자바에서는 컬렉션 인터페이스(java.util.Collection)로 대표적으로 List오 Set으로 나눠진다. List ⇒ 순서가 있는 저장 공간 Set ⇒ 집합적인 저장 공간

 

 

반응형

 

 

😀 책에서 기억하고 싶은 내용을 써보세요.

  • 모듈을 분리하는 가장 중요한 기준은 아마도 시스템에서 각 모듈이 자신을 제외한 다른 부분에 드러내지 않아야 할 비밀을 얼마나 잘 숨기느냐에 있을 것이다.
    • 이러한 비밀 중 대표적인 데이터 구조는 레코드 캡슐화하기와 컬렉션 캡슐화하기로 숨길 수 있다.
  • 심지어 기본형 데이터도 기본형을 객체로 바꾸기로 캡슐화할 수 있다.
  • 클래스는 본래 정보를 숨기는 용도로 설계되었다.
organization = {name: "애크미 구스베리", country: "GB"};
class Organization {
	constructor(data) {
		this._name = data.name;
		this._country = data.country;
	}
	get name() {return this._name;}
	get name(arg) {this._name = arg;}
	get country() {return this._country;}
	get country(arg) {this._country = arg;}
}

 

 

배경

대부분의 프로그래밍 언어는 데이터 레코드를 표현하는 구조를 제공한다.

  • 레코드는 연관된 여러 데이터를 직관적인 방식으로 묶을 수 있어서 각각을 따로 취급할 때보다 훨씬 의미 있는 단위로 전달할 수 있게 해준다.

하지만 단순한 레코드에는 단점이 있다.

  • 계산해서 얻을 수 있는 값과 그렇지 않은 값을 명확히 구분해 저장해야 하는 번거로움

 

바로 이 때문에 저자는 가변 데이터를 저장하는 용도로는 레코드보다 객체를 선호하는 편이다.

  • 객체를 사용하면 어떻게 저장했는지를 숨긴 채 세 가지 값을 각각의 메서드로 제공할 수 있다.
  • 사용자는 무엇이 저장된 값이고 무엇이 계산된 값인지 알 필요가 없다.
  • 캡슐화하면 이름을 바꿀 때도 좋다.
  • 필드 이름을 바꿔도 기존 이름과 새 이름 모두를 각각의 메서드로 제공할 수 있어서 사용자 모두가 새로운 메서드로 옮겨갈 때까지 점진적으로 수정할 수 있다.

 

‘가변’ 데이터 일 때 객체를 선호

  • 값이 불변이면 단순히 ‘시작'과 ‘끝'과 ‘길이'를 모두 구해서 레코드에 저장한다.
  • 이름을 바꿀 때는 그저 필드를 복제한다. 그러면 앞서 객체를 활용해 수정 전후의 두 메서드를 동시에 제공한 방식과 비슷하게 점진적으로 수정할 수 있다.

 

절차

  1. 레코드를 담은 변수를 캡슐화한다.
    • 레코드를 캡슐화하는 함수의 이름은 검색하기 쉽게 지어준다.
  2. 레코드를 감싼 단순한 클래스로 해당 변수의 내용을 교체한다. 이 클래스에 원본 레코드를 반환하는 접근자도 정의하고, 변수를 캡슐화하는 함수들이 이 접근자를 사용하도록 수정한다.
  3. 테스트한다.
  4. 원본 레코드 대신 새로 정의한 클래스 타입의 객체를 반환하는 함수들을 새로 만든다.
  5. 레코드를 반환하는 예전 함수를 사용하는 코드를 4에서 만든 새 함수를 사용하도록 바꾼다. 필드에 접근할 때는 객체의 접근자를 사용한다. 적절한 접근자가 없다면 추가한다. 한 부분을 바꿀 때마다 테스트한다.
    • 중첩된 구조처럼 복잡한 레코드라면, 먼저 데이터를 갱신하는 클라이언트들에 주의해서 살펴본다. 클라이언트가 데이터를 읽기만 한다면 데이터의 복제본이나 읽기전용 프락시를 반환할지 고려해보자.
  6. 클래스에서 원본 데이터를 반환하는 접근자와 (1에서 검색하기 쉬운 이름을 붙여둔) 원본 레코드를 반환하는 함수들을 제거한다.
  7. 테스트한다.
  8. 레코드의 필드도 데이터 구조인 중첩 구조라면 레코드 캡슐화하기와 컬렉션 캡슐화하기를 재귀적으로 적용.

 

예시: 간단한 레코드 캡슐화하기

프로그램 전체에서 널리 사용되는 상수를 예로 살펴보자.

const organization = {name: "애크미 구스베리", country: "GB"};

위 상수는 프로그램 곳곳에서 레코드 구조로 사용하는 자바스크립트 객체로, 아래와 같이 읽고 쓴다.

 

result += `<h1>${organization.name}</h1>`; // 읽기 예
organization.name = newName; // 쓰기 예

1️⃣가장 먼저 이 상수를 캡슐화한다(변수 캡슐화하기)

 

function getRawDataOfOrganization() {return organization;}

그러면 읽고 쓰는 코드는 다음처럼 바뀐다.

 

result += `<h1>${getRawDataOfOrganization().name}<h1>`; // 읽기 예
getRawDataOfOrganization().name = newName; // 쓰기 예
  • 레코드를 캡슐화하는 목적은 변수 자체는 물론 그 내용을 조작하는 방식도 통제하기 위해서다.
  • 2️⃣이렇게 하려면 레코드를 클래스로 바꾸고, 4️⃣새 클래스의 인스턴스를 반환하는 함수를 새로 만든다.

 

// Organization 클래스
class Organization {
	constructor(data) {
		this._data = data;
	}
}

// 최상위...
const organization = new Organization({name: "애크미 구스베리", country: "GB"});
function getRawDataOfOrganization() {return organization._data;}
function getOrganization() {return organization;}

 

객체로 만드는 작업이 끝났으니 5️⃣레코드를 사용하던 코드를 살펴보자.

레코드를 갱신하던 코드는 모두 세터를 이용하도록 고친다.

 

// Organization 클래스...
set name(aString) {this._data.name = aString;}

// 클라이언트
getOrganization().name = newName;

마찬가지로, 레코드를 읽는 코드는 모두 게터를 사용하게 바꾼다.

 

 

// Organization 클래스
get name() {return this._data.name;}

// 클라이언트
result += `<h1>${getOrganization().name}</h1>`;

6️⃣다 바꿨다면 앞에서 이상한 이름으로 지었던 임시 함수를 제거한다.

 

function getRawDataOfOrganization() {return organization._data;} // <--- 제거
function getOrganization() {return organization;}

마지막으로 _data의 필드들을 객체 안에 바로 펼쳐놓으면 더 깔끔할 것 같다.

 

 

class Organization {
	constructor(data) {
		this._name = data.name;
		this._country = data.country;
	}
	get name() {return this._name;}
	get name(aString) {this._name = aString;}
	get country() {return this._country;}
	get country(aCountryCode) {this._country = aCountryCode;}
}
  • 이렇게 하면 입력 데이터 레코드와의 연결을 끊어준다는 이점이 생긴다.
    • 특히 이 레코드를 참조하여 캡슐화를 깰 우려가 있는 코드가 많을 때 좋다.
  • 게터는 데이터 구조를 깊이 탐색하게 만들되 원본 데이터를 그대로 반환하지 말고 객체로 감싸서 반환하는게 효과적일 수 있다.
    • [Refactoring Code to Load Document] 참고 ⇒ 링크

 

 

🤔 오늘 읽은 소감은? 떠오르는 생각을 가볍게 적어보세요.

  • 이 책에서 레코드라고 불리는 데이터의 형식은 JSON과 상당히 비슷하다고 느꼈습니다.
    • 레코드는 좀 더 직관적인 방식이고 객체는 개발자에게 더 친화적이라고 생각했습니다.
  • 자바, 스프링을 사용하면서 기본적으로 객체를 통해 데이터를 주고 받는 코딩을 하고 있었습니다. 이런 방식이 저절로 캡슐화를 지켜서 코딩을 하게 도와주고 있었다는 것을 알게 되었습니다.
  • 유명한 프로그래머(ex. 마틴파울러)의 코드와 생각이 정리된 글을 보고 배우자!

 

 

🔎 궁금한 내용이 있거나, 잘 이해되지 않는 내용이 있다면 적어보세요.

  • 레코드 캡슐화를 재귀적으로 하는 방법은?

 

 

 

반응형

 

 

😀 책에서 기억하고 싶은 내용을 써보세요.

const orderData = orderString.split(/\\s+/);
const productPrice = priceList[orderData[0].split("-")[1]];
const orderPrice = parseInt(orderData[1]) * productPrice;
const orderRecord = parseOrder(order);
const orderPrice = price(orderRecord, priceList);

function parseOrder(aString) {
	const values = aString.split(/\\s+/);
	return ({
		productID: values[0].split("-")[1],
		quantity: parseInt(values[1]),
	});
}
function price(order, priceList) {
	return order.quantity * priceList[order.productID];
}

 

배경

저자는 서로 다른 두 대상을 한꺼번에 다루는 코드를 발견하면 각각을 별개 모듈로 나누는 방법을 모색한다고 한다.

  • 코드를 수정해야 할 때 두 대상을 동시에 생각할 필요 없이 하나에만 집중하기 위해.
  • 모듈이 잘 분리되어 있다면 다른 모듈의 상세 내용은 전혀 기억하지 못해도 원하는 대로 수정을 끝마칠 수도 있다.

 

대표적인 예: 컴파일러

  • 컴파일러는 기본적으로 어떤 텍스트(프로그래밍 언어로 작성된 코드)를 입력받아서 실행 가능한 형태(예컨대 특정 하드웨어에 맞는 목적 코드)로 변환한다.
  • 컴파일러의 역사가 오래되다 보니 사람들은 컴파일 작업을 여러 단계가 순차적으로 연결된 형태로 분리하면 좋다는 사실을 깨달았다.
    1. 텍스트를 토큰화
    2. 토큰을 파싱해서 구문 트리로 만들기
    3. (최적화 등) 구문 트리를 변환하는 다양한 단계를 거친다.
    4. 마지막으로 목적 코드(object code)를 생성

각 단계는 자신만의 문제에 집중하기 때문에 나머지 단계에 관해서는 자세히 몰라도 이해할 수 있다.

이렇게 단계를 쪼개는 기법은 주로 덩치 큰 소프트웨어에 적용된다.

 

 

절차

  1. 두 번째 단계에 해당하는 코드를 독립 함수로 추출한다.
  2. 테스트한다.
  3. 중간 데이터 구조를 만들어서 앞에서 추출한 함수의 인수로 추가한다.
  4. 테스트한다.
  5. 추출한 두 번째 단계 함수의 매개변수를 하나씩 검토한다. 그중 첫 번째 단계에서 사용되는 것은 중간 데이터 구조로 옮긴다. 하나씩 옮길 때마다 테스트한다.
    • 간혹 두 번째 단계에서 사용하면 안 되는 매개변수가 있다. 이럴 때는 각 매개변수를 사용한 결과를 중간 데이터 구조의 필드로 추출하고, 이 필드의 값을 설정하는 문장을 호출한 곳으로 옮긴다.
  6. 첫 번째 단계 코드를 함수로 추출하면서 중간 데이터 구조를 반환하도록 만든다.
    • 이때 첫 번째 단계를 변환기 객체로 추출해도 좋다.

 

예시

상품의 결제 금액을 계산하는 코드

function priceOrder(product, quantity, shippingMethod) {
	const basePrice = product.basePrice * quantity;
	const discount = Math.max(quantity - product.discountThreshold, 0)
					* product.basePrice * product.discountRate;
	const shippingPerCase = (basePrice > shippingMethod.discountThreshold)
					? shippingMethod.discountedFee : shippingMethod.feePerCase;
	const price = basePrice - discount + shippingCost;
	return price;
}

위 코드를 두 단계로 나눈다.

  • 앞의 몇 줄은 상품 정보를 이용해서 결제 금액 중 상품 가격을 계산한다.
  • 뒤의 코드는 배송 정보를 이용하여 결제 금액 중 배송비를 계산한다.

 

  1. 먼저 배송비 계산 부분을 함수로 추출한다.
function priceOrder(product, quantity, shippingMethod) {
	const basePrice = product.basePrice * quantity;
	const discount = Math.max(quantity - product.discountThreshold, 0)
					* product.basePrice * product.discountRate;
	const price = applyShipping(basePrice, shippingMethod, quantity, discount);
	return price;
}

function applyShipping(basePrice, shippingMethod, quantity, discount) {
	const shippingPerCase = (basePrice > shippingMethod.discountThreshold)
					? shippingMethod.discountedFee : shippingMethod.feePerCase;
	const shippingCost = quantity * shippingPerCase;
	const price = basePrice - discount + shippingCost;
	return price;
}

두 번째 단계에 필요한 데이터를 모두 개별 매개변수로 전달했다.

 

2. 다음으로 첫 번째 단계와 두 번째 단계가 주고받을 중간 데이터 구조를 만든다.

function priceOrder(product, quantity, shippingMethod) {
	const basePrice = product.basePrice * quantity;
	const discount = Math.max(quantity - product.discountThreshold, 0)
					* product.basePrice * product.discountRate;
	const priceData = {};  // <-- 중간 데이터 구조
	const price = applyShipping(priceData, basePrice, shippingMethod, quantity, discount);
	return price;
}

function applyShipping(priceData, basePrice, shippingMethod, quantity, discount) {
	const shippingPerCase = (basePrice > shippingMethod.discountThreshold)
					? shippingMethod.discountedFee : shippingMethod.feePerCase;
	const shippingCost = quantity * shippingPerCase;
	const price = basePrice - discount + shippingCost;
	return price;
}

3. 이제 applyShipping()에 전달되는 다양한 매개변수를 살펴보자.

 

이중 basePrice는 첫 번째 단계를 수행하는 코드에서 생성된다.

따라서 중간 데이터 구조로 옮기고 매개변수 목록에서 제거한다.

function priceOrder(product, quantity, shippingMethod) {
	const basePrice = product.basePrice * quantity;
	const discount = Math.max(quantity - product.discountThreshold, 0)
				* product.basePrice * product.discountRate;
	const priceData = {basePrice: basePrice};
	const price = applyShipping(priceData, shippingMethod, quantity, discount); // <-- basePrice 제거
	return price;
}

function applyShipping(priceData, shippingMethod, quantity, discount) { // <-- basePrice 제거
	const shippingPerCase = (priceData.basePrice > shippingMethod.discountThreshold)
					? shippingMethod.discountedFee : shippingMethod.feePerCase;
	const shippingCost = quantity * shippingPerCase;
	const price = priceData.basePrice - discount + shippingCost;
	return price;
}

quantity와 discount도 위와 같은 방법으로 저자가 선호하는 중간 데이터 구조에 가존 매개변수 값을 담는 방식을 진행

(코드 생략)

 

 

매개변수들을 모두 처리하면 중간 데이터 구조가 완성된다.

 

4. 이제 첫 번째 단계 코드를 함수로 추출하고 이 데이터 구조를 반환하게 한다.

⇒ 저자는 여기서 최종 결과를 담는 상수들(price)도 갈끔하게 정리했다.

function priceOrder(product, quantity, shippingMethod) {
	const priceData = calculatePricingData(product, quantity);
	return applyShipping(priceData, shippingMethod);
}

function calculatePricingData(product, quantity) {
	const basePrice = product.basePrice * quantity;
	const discount = Math.max(quantity- product.discountThreshold, 0)
				* product.basePrice * product.discountRate;
	return {basePrice: basePrice, quantity: quantity, discount:discount};
}

function applyShipping(priceData, shippingMethod) {
	const shippingPerCase = (priceData.basePrice > shippingMethod.discountThreshold)
				? shippingMethod.discountedFee : shippingMethod.feePerCase;
	const shippingCost = priceData.quantity * shippingPerCase;
	return priceData.basePrice - priceData.discount + shippingCost;
}

 

 

🤔 오늘 읽은 소감은? 떠오르는 생각을 가볍게 적어보세요.

  • 자주 테스트해야 할 복잡한 동작을 분리함으로써 테스트를 더 쉽게 수행하게 만든다.
  • *험블 객체 패턴(Humble Object Pattern)
  • 단계를 쪼개면서 단순히 쪼개는 것이 아닌 이전 단계에서 배운 리팩터링 방법을 적용하게 되지 않을까 생각했습니다.
    • 단계를 쪼개고 작은 단위의 함수로 나누면서 코드를 전반적으로 수정
  • 매개변수의 값을 객체에 데이터 구조에 담아서 전송하는게 상당히 인상깊었습니다.

 

 

🔎 궁금한 내용이 있거나, 잘 이해되지 않는 내용이 있다면 적어보세요.

  • 없음

 

 

반응형

 

 

😀 책에서 기억하고 싶은 내용을 써보세요.

function base(aReading) {...}
function taxableCharge(aReading) {...}
function enrichReading(argReading) {
	const aReading = _.cloneDeep(argReading);
	aReading.baseCharge = base(aReading);
	aReading.taxableCharge = taxableCharge(aReading);
	return aReading;
}

 

배경

소프트웨어는 데이터를 입력받아서 여러 가지 정보를 도출하곤 한다.

이렇게 도출된 정보는 여러 곳에서 사용될 수 있는데, 그러다 보면 이 정보가 사용되는 곳마다 같은 도출 로직이 반복되기도 한다.

  • 저자는 이런 도출작업들을 한데로 모아두길 좋아한다고 한다.
    • 모아두면 검색과 갱신을 일관된 장소에서 처리할 수 있다.
    • 로직 중복을 막을 수 있다.

 

 

이렇게 하기 위한 방법으로 변환 함수를 사용한다.

 

변환 함수란?

변환 함수는 원본 데이터를 입력 받아서 필요한 정보를 모두 도출한 뒤, 각각을 출력 데이터의 필드에 넣어 반환한다.

이렇게 해두면 도출 과정을 검토할 일이 생겼을 때 변환 함수만 살펴보면 된다.

 

절차

  1. 변환할 레코드를 입력받아서 값을 그대로 반환하는 변환 함수를 만든다.
    • 이 작업은 대체로 깊은 복사로 처리해야 한다. 변환 함수가 원본 레코드를 바꾸지 않는지 검사하는 테스트를 마련해두면 도움될 때가 많다.
  2. 묶을 함수 중 함수 하나를 골라서 본문 코드를 변환 함수로 옮기고, 처리 결과를 레코드에 새 필드로 기록한다. 그런 다음 클라이언트 코드가 이 필드를 사용하도록 수정한다.
    • 로직이 복잡하면 함수 추출하기부터 한다.
  3. 테스트한다.
  4. 나머지 관련 함수도 위 과정에 따라 처리한다.

 

예시

매달 사용자가 마신 차의 양을 측정하는 예시

reading = {customer: "ivan", quantity: 10, month: 5, year: 2017};

사용자에게 요금을 부과하기 위해 기본요금을 계산하는 코드

 

// 클라이언트 1
const aReading = acquireReading();
const baseCharge = baseRate(aReading.month, aReading.year) * aReading.quantity;

세금을 부과할 소비량을 계산하는 코드

 

// 클라이언트 2
const aReading = acquireReading();
const base = (baseRate(aReading.month, aReading.year) * aReading.quantity);
const taxableCharge = Math.max(0, base - taxThreshold(aReading.year));

이 코드에는 이와 같은 계산 코드가 여러 곳에 반복된다고 가정

  • 중복 코드는 나중에 로직을 수정할 때 골치를 썩인다.
  • 중복 코드라면 함수 추출하기로 처리할 수도 있지만, 추출한 함수들이 프로그램 곳곳에 흩어져서 나중에 프로그래머가 그런 함수가 있는지조차 모르게 될 가능성이 있다.

 

// 클라이언트 3
const aReading = acquireReading();
const basicChargeAmount = calculateBaseCharge(aReading);

function calculateBaseCharge(aReading) {  // <-- 다른 곳에서 이미 함수로 만들어둠
	return baseRate(aReading.month, aReading.year) * aReading.quantity;
}

이를 해결하는 방법으로, 다양한 파생 정보 계산 로직을 모두 하나의 변환 단계로 모을 수 있다.

변환 단계에서 미가공 측정값을 입력받아서 다양한 가공 정보를 덧붙여 반환하는 것이다.

 

  1. 우선 입력 객체를 그대로 복사해 반환하는 변환 함수를 만든다.
function enrichReading(original) {
	const result = _.cloneDeep(original);
	return result;
}

 

 

2.이제 변경하려는 계산 로직 중 하나를 고른다.

먼저 이 계산 로직에 측정값을 전달하기 전에 부가 정보를 덧붙이도록 수정한다.

// 클라이언트 3
const rawReading = acquireReading();
const aReading = enrichReading(rawReading);
const basicChargeAmount = aReading.baseCharge;

calculateBaseCharge()를 부가 정보를 덧붙이는 코드 근처로 옮긴다(함수 옮기기)

 

 

function enrichReading(original) {
	const result = _.cloneDeep(original);
	result.baseCharge = calculateBaseCharge(result);  // <-- 미가공 측정값에 기본 소비량을 부가 정보로 덧붙임
	return result;
}

변환 함수 안에서는 결과 객체를 매번 복제할 필요 없이 마음껏 변경해도 된다.

이어서 이 함수를 사용하던 클라이언트가 부가 정보를 담은 필드를 사용하도록 수정한다.

 

 

// 클라이언트 3
const rawReading = acquireReading();
const aReading = enrichReading(rawReading);
const basicChargeAmount = aReading.baseCharge;

calculateBaseCharge()를 호출하는 코드를 모두 수정했다면, 이 함수를 enrichReading() 안에 중첩시킬 수 있다. 그러면 ‘기본요금을 이용하는 클라이언트는 변환된 레코드를 사용해야 한다’는 의도를 명확히 표현할 수 있다.

 

 

주의할 점

enrichReading()처럼 정보를 추가해 반환할 때 원본 측정값 레코드는 변경하지 않아야 한다. ⇒ 따라서 이를 확인하는 테스트를 작성해두는 것이 좋다.

if('check reading unchanged', function() {
	const baseReading = {customer: "ivan", quantity: 15, month: 5, year: 2017};
	const oracle = _.cloneDeep(baseReading);
	enrichReading(baseReading);
	assert.deepEqual(baseReading, oracle);
});

그런 다음 클라이언트 1도 이 필드를 사용하도록 수정

 

// 클라이언트 1
const rawReading = acquireReading();
const aReading = enrichReading(rawReading);
const baseCharge = aReading.baseCharge;

 

 

 

3. 이제 세금을 부과할 소비량 계산으로 넘어간다. 가장 먼저 변환 함수부터 끼워 넣는다.

const rawReading = acquireReading();
const aReading = enrichReading(rawReading);
const base = (baseRate(aReading.month, aReading.year) * aReading.quantity);
const taxableCharge = Math.max(0, base - taxThreshold(aReading.year));

여기서 기본요금을 계산하는 부분을 앞에서 새로 만든 필드로 교체할 수 있다.

계산이 복잡하다면 함수 추출하기부터 하겠으나, 여기서는 복잡하지 않으니 한 번에 처리하겠다.

 

 

const rawReading = acquireReading();
const aReading = enrichReading(rawReading);
const base = aReading.baseCharge;
const taxableCharge = Math.max(0, base - taxThreshold(aReading.year));

 

테스트해서 문제가 없다면 base 변수를 인라인한다.

const rawReading = acquireReading();
const aReading = enrichReading(rawReading);
const taxableCharge = Math.max(0, aReading.baseCharge - taxThreshold(aReading.year));

 

그런 다음 계산 코드를 변환 함수로 옮긴다.

function enrichReading(original) {
	const result = _.cloneDeep(original);
	result.baseCharge = calculateBaseCharge(result);
	result.taxableCharge = Math.max(0, result.baseCharge - taxThreshold(result.year));
	return result;
}

 

이제 새로 만든 필드를 사용하도록 원본 코드를 수정한다.

const rawReading = acquireReading();
const aReading = enrichReading(rawReading);
const taxableCharge = aReading.taxableCharge;

테스트에 성공하면 taxableCharge 변수를 인라인한다.

 

 

 

🤔 오늘 읽은 소감은? 떠오르는 생각을 가볍게 적어보세요.

  • 변환함수를 작성 할 때도 함수명이 상당히 중요하다고 느꼈습니다.
  • 좀 더 추상화 기법에 근접한 내용 같다고 생각했습니다.
  • 지금까지 작성한 코드를 보면서 예시처럼 작성된 코드를 상당히 많이 봤습니다.

 

 

🔎 궁금한 내용이 있거나, 잘 이해되지 않는 내용이 있다면 적어보세요.

  • lodash 라이브러리란?
    • 자바스크립트의 인기 라이브러리 중 하나
    • 편리하게 코드를 작성하거나 웹 표준에 효율적으로 맞출 수 있게 해주는 Node.js 패키지

 

 

반응형

 

 

 

😀 책에서 기억하고 싶은 내용을 써보세요.

리팩터링 전

function base(aReading) {...}
function taxableCharge(aReading) {...}
function calculateBaseCharge(aReading) {...}

리팩터링 후

class Reading {
	base() {...}
	taxableCharge() {...}
	calculateBaseCharge() {...}
}

 

 

배경

  • 공통 데이터를 중심으로 긴밀하게 엮여 작동하는 함수 무리를 발견하면 클래스 하나로 묶는다
    • 클래스로 묶으면 이 함수들이 공유하는 공통 환경을 더 명확하게 표현할 수 있다.
    • 각 함수에 전달되는 인수를 줄여서 객체 안에서의 함수 호출을 간결하게 만들 수 있다.
    • 이런 객체를 시스템의 다른 부분에 전달하기 위한 참조를 제공할 수 있다.

이 리팩터링은 이미 만들어진 함수들을 재구성할 때는 물론, 새로 만든 클래스와 관련하여 놓친 연산을 찾아서 새 클래스의 메서드로 뽑아내는 데도 좋다.

클래스를 지원하지 않는 언어를 사용할 때는 같은 기능을 ‘함수를 객체처럼(Function As Object)패턴을 이용해 구현하기도 한다.

 

 

절차

  1. 함수들이 공유하는 공통 데이터 레코드를 캡슐화한다.
    • 공통 데이터가 레코드 구조로 묶여 있지 않다면 먼저 매개변수 객체 만들기로 데이터를 하나로 묶는 레코드를 만든다.
  2. 공통 레코드를 사용하는 함수 각각을 새 클래스로 옮긴다(함수 옮기기)
    • 공통 레코드의 멤버는 함수 호출문의 인수 목록에서 제거한다.
  3. 데이터를 조작하는 로직들은 함수로 추출해서 새 클래스로 옮긴다.

 

 

예시

사람들은 매달 차 계량기를 읽어서 측정값을 다음과 같이 기록한다고 하자.

reading = {customer: "ivan", quantity: 10, month: 5, year: 2017};

이런 레코드를 처리하는 코드를 훑어보니 이 데이터를 비슷한 연산을 수행하는 부분이 상당히 많았다. 그래서 기본요금을 계산하는 코드를 찾아봤다.

 

// 클라이언트 1
const aReading = acquireReading();
const baseCharge = baseRate(aReading.month, aReading.year) * aReading.quantity;

기본적인 차 소비량만큼은 면세가 되도록 한다.

 

// 클라이언트 2
const aReading = acquireReading();
const base = (baseRate(aReading.month, aReading.year) * aReading.quantity);
const taxableCharge = Math.max(0, base - taxThreshold(aReading.year));

여기서도 기본요금 계산 공식이 똑같이 등장하는 것을 발견했다.

저자와 성향이 같다면 곧바로 함수로 추출 하려 시도할 것이다.

 

// 클라이언트 3
const aReading = acquireReading();
const basicChargeAmount = calculateBaseCharge(aReading);

function calculateBaseCharge(aReading) {   // <--- 기본 요금 계산 함수
	return baseRate(aReading.month, aReading.year) * aReading.quantity;
}

이런 코드를 보면 저자는 본능적으로 앞의 두 클라이언트(클라이언트 1, 2)도 이 함수를 사용하도록 고친다고 한다. 하지만 이렇게 최상위 함수로 두면 못 보고 지나치기 쉽다는 문제가 있다.

저자라면 이런 함수를 데이터 처리 코드 가까이에 둔다고 한다, 그러기 위한 좋은 방법으로, 데이터를 클래스로 만든다.

 

 

  1. 먼저 레코드를 클래스로 변환하기 위해 레코드를 캡슐화한다.
class Reading {
	constructor(data) {
		this._customer = data.customer;
		this._quantity = data.quantity;
		this._month = data.month;
		this._year = data.year;
	}
	get customer() {return this._custoemr;}
	get quantity() {return this._quantity;}
	get month()    {return this._month;}
	get year()     {return this._year;}
}
  1. 이미 만들어져 있는 calculateBaseCharge()부터 옮기자.

 

  • 새 클래스를 사용하려면 데이터를 얻자마자 객체로 만들어야 한다.
// 클라이언트 3
const rawReading = acquireReading();
const aReading = new Reading(rawReading);
const basicChargeAmount = calculateBaseCharge(aReading);

 

그런 다음 calculateBaseCharge()를 새로 만든 클래스로 옮긴다(함수 옮기기).

// Reading 클래스
get calculateBaseCharge() {
	return baseRate(this.month, this.year) * this.quantity;
}
// 클라이언트 3
const rawReading = acquireReading();
const aReading = new Reading(rawReading);
const basicChargeAmount = aReading.calculateBaseCharge;

 

이 과정에서 메서드 이름을 원하는대로 바꾼다.(함수 이름 바꾸기)

get baseCharge() {
	return baseRate(this.month, this.year) * this.quantity;
}
// 클라이언트 3
const rawReading = acquireRading();
const aReading = new Reading(rawReading);
const basicChargeAmount = aReading.baseCharge;

이렇게 이름을 바꾸고 나면 Reading 클래스의 클라이언트는 baseCharge가 필드인지,

계산된 값(함수 호출)인지 구분할 수 없다.

이는 단일 접근 원칙(Uniform Access Principle)을 따르므로 권장하는 방식이다.

이제 첫 번째 클라이언트에서 중복된 계산 코드를 고쳐 앞의 메서드를 호출하게 한다.

 

// 클라이언트 1
const rawReading = acquireReading();
const aReading = new Reading(rawReading);
const baseCharge = aReading.baseCharge;

 

새로 만든 기본요금 메서드를 사용하도록 수정한다.

// 클라이언트 2
const rawReading = acquireReading();
const aReading = new Reading(rawReading);
const taxableCharge = Math.max(0, aReading.baseCharge - taxThreshold(aReading.year));

 

 

  1. 이어서 세금을 부과할 소비량을 계산하는 코드를 함수로 추출한다.
function taxableChargeFn(aReading) {
	return Math.max(0, aReading.baseCharge - taxThreshold(aReading.year));
}
// 클라이언트 3
const rawReading = acquireReading();
const aReading = new Reading(rawReading);
const taxableCharge = taxableChargeFn(aReading);

 

그런 다음 방금 추출한 함수를 Reading 클래스로 옮긴다(함수 옮기기)

// Reading 클래스
get taxableCharge() {
	return Math.max(0, this.baseCharge - taxThreshold(this.year));
}
// 클라이언트 3
const rawReading = acquireReading();
const aReading = new Reading(rawReading);
const taxableCharge = aReading.taxableCharge;
  • 파생 데이터 모두를 필요한 시점에 계산되게 만들었으니 저장된 데이터를 갱신하더라도 문제가 생길 일이 없다.
  • 프로그램의 다른 부분에서 데이터를 갱신할 가능성이 꽤 있을 때는 클래스로 묶어두면 큰 도움이 된다.

 

 

🤔 오늘 읽은 소감은? 떠오르는 생각을 가볍게 적어보세요.

  • 클래스와 캡슐화가 긴밀한 관계라는 이유를 알 것같습니다.
    • 변수와 함수를 클래스로 묶어서 공통 관리한다.
    • 함수들을 서로 공유하면서 더 명확하게 의도를 표현하게된다.

 

 

🔎 궁금한 내용이 있거나, 잘 이해되지 않는 내용이 있다면 적어보세요.

  • 함수를 객체처럼 패턴은 어떤 패턴인지?
    • 함수형 프로그래밍
    • 프로그래밍에서 함수를 값으로 다룰 수 있는 것(함수 스스로 객체 취급) 즉, 함수를 변수에 담아 원할 때 평가(함수 호출)하는 것

 

 

 

반응형

+ Recent posts