manager = aPerson.managr;
class Person {
get manager() {return this.department.manager;}
배경
모듈화 설계를 제대로 하는 핵심은 캡슐화다.
캡슐화는 모듈들이 시스템의 다른 부분에 대해 알아야 할 내용을 줄여준다.
캡슐화가 잘 되어 있다면 무언가를 변경해야 할 때 고려해야 할 모듈 수가 적어져서 코드를 변경하기가 훨씬 쉬워진다.
객체 지향을 처음 배울 때는 캡슐화란 필드를 숨기는 것이라고 배운다. 그러다 경험이 쌓이면서 캡슐화의 역할이 그보다 많다는 사실을 깨닫는다.
서버 객체의 필드가 가리키는 객체(위임 객체)의 메서드를 호출하려면 클라이언트는 이 위임 객체를 알아야 한다. 위임 객체의 인터페이스가 바뀌면 이 인터페이스를 사용하는 모든 클라이언트가 코드를 수정해야 한다.
⇒ 이러한 의존성을 없애려면 서버 자체에 위임 메서드를 만들어서 위임 객체의 존재를 숨긴다.
그러면 위임 객체가 수정되도 서버 코드만 고치면 되며, 클라이언트는 아무런 영향을 받지 않는다.
절차
위임 객체의 각 메서드에 해당하는 위임 메서드를 서버에 생성한다.
클라이언트가 위임 객체 대신 서버를 호출하도록 수정한다. 하나씩 바꿀 때마다 테스트한다.
모두 수정했다면, 서버로부터 위임 객체를 얻는 접근자를 제거한다.
테스트한다.
예시
사람과 사람이 속한 부서를 다음처럼 정의했다.
// 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;}
배경
클래스 인라인하기는 클래스 추출하기를 거꾸로 돌리는 리팩터링이다.
저자는 더 이상 제 역할을 못 해서 그대로 두면 안 되는 클래스는 인라인해버린다.
역할을 옮기는 리팩터링을 하고나니 특정 클래스에 남은 역할이 거의 없을 때 이런 현상이 자주 생긴다. ⇒ 이럴 땐 이 불쌍한 클래스를 가장 많이 사용하는 클래스로 흡수시키자.
두 클래스의 기능을 지금과 다르게 배분하고 싶을 때도 클래스를 인라인한다.
클래스를 인라인해서 하나로 합친 다음 새로운 클래스를 추출 하는 게 쉬울 수도 있기 때문 ⇒ 이는 코드를 재구성할 때 흔히 사용하는 방식이기도 하다.
절차
소스 클래스의 각 public 메서드에 대응하는 메서드들을 타깃 클래스에 생성한다. 이 메서드들은 단순히 작업을 소스 클래스로 위임해야 한다.
소스 클래스의 메서드를 사용하는 코드를 모두 타깃 클래스의 위임 메서드를 사용하도록 바꾼다. 하나씩 바꿀 때마다 테스트한다.
소스 클래스의 메서드와 필드를 모두 타깃 클래스로 옮긴다. 하나씩 옮길 때마다 테스트한다.
소스 클래스를 삭제하고 조의를 표한다.
예시
배송 추적 정보를 표현하는 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이 예전에는 유용했을지 몰라도 현재는 제 역할을 못 하고 있으니
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;}
}
배경
메서드와 데이터가 너무 많은 클래스는 이해하기가 쉽지 않으니 잘 살펴보고 적절히 분리하는 것이 좋다.
특히 일부 데이터와 메서드를 따로 묶을 수 있다면 어서 분리하라는 신호다.
함께 변경되는 일이 많거나 서로 의존하는 데이터들도 분리한다.
특정 데이터나 메서드 일부를 제거하면 어떤 일이 일어나는지 자문해보면 판단에 도움이 된다. ⇒ 제거해도 다른 필드나 메서드들이 논리적으로 문제가 없다면 분리할 수 있다는 뜻이다.
절차
클래스의 역할을 분리할 방법을 정한다.
분리된 역할을 담당할 클래스를 새로 만든다.
원래 클래스에 남은 역할과 클래스 이름이 어울리지 않는다면 적절히 바꾼다.
원래 클래스의 생성자에서 새로운 클래스의 인스턴스를 생성하여 필드에 저장해둔다.
분리될 역할에 필요한 필드들을 새 클래스로 옮긴다(필드 옮기기). 하나씩 옮길 때마다 테스트
메서드들도 새 클래스로 옮긴다(함수 옮기기). 이때 저수준 메서드, 즉 다른 메서드를 호출하기보다는 호출을 당하는 일이 많은 메서드부터 옮긴다. 하나씩 옮길 때마다 테스트한다.
양쪽 클래스의 인터페이스를 살펴보면서 불필요한 메서드를 제거하고, 이름도 새로운 환경에 맞게 바꾼다.
새 클래스를 외부로 노출할지 정한다. 노출하려거든 새 클래스에 참조를 값으로 바꾸기를 적용할지 고민해본다.
예시
단순한 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을 최소화 하면서 사용할려고 하고 있습니다.
🔎 궁금한 내용이 있거나, 잘 이해되지 않는 내용이 있다면 적어보세요.
규모가 점점 커질수록 클래스에 다양한 기능을 집어넣는 경우를 보았다. 회사에서 이런 코드를 보면 어떻게 해야 할까?
get basePrice() {this._quantity * this._itemPrice;}
...
if (this.basePrice > 1000)
return this.basePrice * 0.95;
else
return this.basePrice * 0.98;
배경
함수 안에서 어떤 코드의 결괏값을 뒤에서 다시 참조할 목적으로 임시 변수를 쓰기도 한다.
임시 변수를 사용하면 값을 계산하는 코드가 반복되는 걸 줄이고 (변수 이름을 통해) 값의 의미를 설명할 수도 있어서 유용하다.
그런데 한 걸음 더 나아가 아예 함수로 만들어 사용하는 편이 나을 때가 많다.
긴 함수의 한 부분을 함수로 추출하고자 할 때 먼저 변수들을 각각의 함수로 만들면 일이 수월해진다.
⇒ 추출한 함수에 변수를 따로 전달할 필요가 없어지기 때문
추출한 함수와 원래 함수의 경계가 더 분명해진다.
⇒ 부자연스러운 의존 관계나 부수효과를 찾고 제거하는 데 도움이 된다.
변수 대신 함수로 만들어두면 비슷한 계산을 수행하는 다른 함수에서도 사용할 수 있어 코드 중복이 줄어든다.
⇒ 여러 곳에서 똑같은 방식으로 계산되는 변수를 발견할 때마다 함수로 바꿀 수 있는지 확인
이번 리팩터링은 클래스 안에서 적용할 때 효과가 가장 크다.
클래스는 추출할 메서드들에 공유 컨텍스트를 제공하기 때문
클래스 바깥의 최상위 함수로 추출하면 매개변수가 너무 많아져서 함수를 사용하는 장점이 줄어든다.
중첩 함수를 사용하면 이런 문제는 없지만 관련 함수들과 로직을 널리 공유하는 데 한계가 있다.
임시 변수를 질의 함수로 바꾼다고 다 좋아지는건 아니다.
자고로 변수는 값을 한 번만 계산하고, 그 뒤로는 읽기만 해야 한다.
가장 단순한 예로, 변수에 값을 한 번 대입한 뒤 더 복잡한 코드 덩어리에서 여러 차례 다시 대입하는 경우는 모두 질의 함수로 추출해야 한다.
계산 로직은 변수가 다음번에 사용될 때 수행해도 똑같은 결과를 내야 한다. ⇒ 그래서 ‘옛날 주소’ 처럼 스냅숏 용도로 쓰이는 변수에는 이 리팩터링을 적용하면 안 된다.
절차
변수가 사용되기 전에 값이 확실히 결정되는지, 변수를 사용할 때마다 계산 로직이 매번 다른 결과를 내지는 않는지 확인한다.
읽기 전용으로 만들 수 있는 변수는 읽기 전용으로 만든다.
테스트한다.
변수 대입문을 함수로 추출한다.
변수와 함수가 같은 이름을 가질 수 없다면 함수 이름을 임시로 짓는다. 또한, 추출한 함수가 부수효과를 일으키지는 않는지 확인한다. 부수효과가 있다면 질의 함수와 변경 함수 분리하기로 대처한다.
테스트한다.
변수 인라인하기로 임시 변수를 제거한다.
예시
간단한 주문 클래스
// 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 접근자 메서드를 단순히 해당 값을 읽어오는 것으로 사용하는 게 아니라, 똑같은 방식으로 계산되어 같은 값이 나오는 것들을 묶어 줄 수 있다는 것을 알게 되었습니다(캡슐화). ⇒ 이 방법을 통해 유저 혹은 클라이언트는 내부 구조를 알 필요 없이 해당 함수를 가져다 사용할 수 있습니다. (유저의 편의성 증대)
🔎 궁금한 내용이 있거나, 잘 이해되지 않는 내용이 있다면 적어보세요.
캡슐화와 추상화의 차이는?
캡슐화와 추상화는 개발 비용을 낮춰주는 객체지향의 두 가지 특징이다.
캡슐화는 기능 구현을 외부로부터 감추고, 내부의 구현 병경이 외부로 전파되는 것을 막아줍니다.
추상화는 의존 대상을 추상 타입으로 간접 참조하고, 사용하고 있는 의존 대상의 변경이 사용하는 입장에는 영향을 주지 않습니다.
둘은 상호 보완적인 개념입니다. 추상화: 객체의 동작, 기능 자체에 중점을 둔다. 캡슐화: 객체 내부 상태에 대한 정보를 숨기는 방식으로 이루어진다.
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
가장 흔히 사용하는 방식은 컬렉션 데터를 제공하되 내부 컬렉션의 복제본을 반환하는 방식
복제본을 수정해도 캡슐화된 원본 컬렉션에는 아무런 영향을 주지 않는다.
컬렉션이 상당히 크다면 성능 문제가 발생 할 수 있지만 성능에 지장을 줄만큼 컬렉션이 큰 경우는 별로 없었다.
절차
아직 컬렉션을 캡슐화하지 않았다면 변수 캡슐화하기부터 한다.
컬렉션에 원소를 추가/제거하는 함수를 추가한다.
컬렉션 자체를 통째로 바꾸는 세터는 제거한다. 세터를 제거할 수 없다면 인수로 받은 컬렉션을 복제해 저장하도록 만든다.
정적 검사를 수행한다.
컬렉션을 참조하는 부분을 모두 찾는다. 컬렉션의 변경자를 호출하는 코드가 모두 앞에서 추가한 추가/제거 함수를 호출하도록 수정한다. 하나씩 수정할 때마다 테스트한다.
컬렉션 게터를 수정해서 원본 내용을 수정할 수 없는 읽기전용 프락시나 복제본을 반환.
테스트한다.
예시
수업 목록을 필드로 지니고 있는 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;}