7. 캡슐화
- 모듈을 분리하는 가장 중요한 기준은 아마도 시스템에서 각 모듈이 자신을 제외한 다른 부분에 드러내지 않아야 할 비밀을 얼마나 잘 숨기느냐에 있다.
- 클래스는 내부 정보뿐 아니라 클래스 사이의 연결 관계를 숨기는 데도 유용하다.
- 가장 큰 캡슐화 단위는 클래스와 모듈이지만 함수도 구현을 캡슐화한다.
7.1 레코드 캡슐화하기
배경
- 레코드는 연관된 여러 데이터를 직관적인 방식으로 묶을 수 있어서 각각을 따로 취급할 때보다 훨씬 의미 있는 단위로 전달할 수 있게 해준다.
- 단점으로는 계산해서 얻을 수 있는 값과 그렇지 않은 값을 명확히 구분해 저장해야 하는 점이 번거롭다.
- 가변 데이터를 저장하는 용도로는 레코드보다 객체를 선호한다.
- 객체를 사용하면 어떻게 저장했는지를 숨긴 채 세 가지 값을 각각의 메서드로 제공할 수 있다.
- 사용자는 무엇이 저장된 값이고 무엇이 계산된 값인지 알 필요가 없다.
- 캡슐화하면 이름을 바꿀 때도 좋다.
- 가변 데이터일 때 객체를 선호하고 불변이면 레코드를 선호한다.
- 중첩된 리스트나 해시맵을 받아서 다른 포맷으로 직렬화할때, 캡슐화하면 나중에 포맷을 바꾸거나 추적하기 어려운 데이터를 수정하기 수월해진다.
절차
- 레코드를 담은 변수를 캡슐화한다.
- 레코드를 캡슐화하는 함수의 이름은 검색하기 쉽게 지어준다.
- 레코드를 감싼 단순한 클래스로 해당 변수의 내용을 교체한다. 이 클래스에 원본 레코드를 반환하는 접근자도 정의하고, 변수를 캡슐화하는 함수들이 이 접근자를 사용하도록 수정한다.
- 테스트한다.
- 원본 레코드 대신 새로 정의한 클래스 타입의 객체를 반환하는 함수들을 새로 만든다.
- 레코드를 반환하는 예전 함수를 사용하는 코드를 전 단계에서 만든 새 함수를 사용하도록 바꾼다. 필드에 접근할 때는 객체의 접근자를 사용한다. 적절한 접근자가 없다면 추가한다. 한 부분을 바꿀 때마다 테스트한다.
- 클라이언트가 데이터를 읽기만 한다면 데이터의 복제본이나 읽기전용 프록시를 반환할지 고려해본다.
- 클래스에서 원본 데이터를 반환하는 접근자와 1단계에서 레코드를 캡슐화하는 함수들을 제거한다.
- 테스트한다.
- 레코드의 필드도 데이터 구조인 중첩 구조라면 레코드 캡슐화하기와 컬렉션 캡슐화하기를 재귀적으로 적용한다.
예시
const organization = {name: "애크미", country: "GB"}
result += `${organization.name}`// 읽기
organization.name = newName // 쓰기
- 상수를 캡슐화한다(변수 캡슐화하기)
function getRawDataOfOrganization() {
return organization;
}
result += `${getRawDataOfOrganiztion().name}`// 읽기
getRawDataOfOrganiztion().name = newName // 쓰기
- 레코드를 클래스로 바꾸고, 새 클래스의 인스턴스를 반환하는 함수를 새로 만든다.
class Organization {
constructor(data) {
this._data = data;
}
}
const organization = new Organization({name: "애크미", country: "GB"});
function getRawDataOfOrganization() {return organization._data;}
function getOrganization() {return organization;}
- 레코드를 갱신하던 코드는 모두 세터를 사용하도록 고친다.
// Organization 클래스
set name(aString) {this._data.name = aString;}
// 클라이언트
getOrganization().name = newName;
- 임시 함수를 제거한다.
- 완성
class Organization {
constructor(data) {
this._name = data.name
this._country = data.country
}
get name() {return this._name;}
set name(aString) {this._name = aString;}
get country() {return this._country;}
set country(aCountryCode) {this._country = aCountryCode}
}
7.2 컬렉션 캡슐화하기
배경
- 컬렉션 게터가 원본 컬렉션을 반환하지 않게 만들어서 클라이언트가 실수로 컬렉션을 바꿀 가능성을 차단하는게 낫다.
두 가지 방식이 있다.
- 컬렉션에 접근하려면 컬렉션이 소속된 클래스의 적절한 메서드를 반드시 거치게 한다.
- 컬렉션을 읽기 전용으로 제공할 수 있다.
- 위의 방식 중 한 가지만 택해서 컬렉션 접근 함수의 동작 방식을 통일해야 한다.
절차
- 아직 컬렉션을 캡슐화하지 않았다면 변수 캡슐화하기부터 한다.
- 컬렉션에 원소를 추가/ 제거하는 함수를 추가한다.
- 컬렉션 자체를 통짜로 바꾸는 세터는 제거한다.
- 컴파일
- 컬렉션을 참조하는 부분을 모두 찾는다. 컬렉션의 변경자를 호출하는 코드가 모두 앞에서 추가한 추가/제거 함수를 호출하도록 수정한다.
- 컬렉션 게터를 수정해서 원본 내용을 수정할 수 없는 읽기전용 프록시나 복제본을 반환하게 한다.
- 테스트
예시
class Person {
get courses() {return this._courses;}
set courses(aList) {this._courses = aList;}
}
------------------------------------------------------------
class Person {
get courses() {return this._courses.slice();}
addCourse(aCourse) {...}
removeCourse(aCourse) {...}
}
7.3 기본형을 객체로 바꾸기
배경
- 단순한 출력 이상의 기능이 필요해지면, 데이터를 표현하는 전용 클래스를 정의하는게 낫다.
절차
- 변수를 캡슐화하지 않았다면 캡슐화한다.(게터, 세터를 만든다)
- 단순한 값 클래스를 만든다. 생성자는 기존 값을 인수로 받아서 저장하고, 이 값을 반환하는 게터를 추가한다.
- 값 클래스의 인스턴스를 새로 만들어서 필드에 저장하도록 1에서 만든 세터를 수정한다. 이미 있다면 필드의 타입을 적절히 변경한다.
- 새로 만든 클래스의 게터를 호출한 결과를 반환하도록 1에서 만든 게터를 수정한다.
- 함수 이름을 바꾸면 원본 접근자의 동작을 더 잘 드러낼 수 있는지 검토한다.
예시
class Order {
constructor(data) {
this.priority = data.priority;
...
}
}
// 클라이언트
highPriorityCount = orders.filter(o => "high" === o.priority
|| "rush" === o.priority)
.length
- 변수를 캡슐화한다.
- 이렇게 필드를 자가 캡슐화하면 이름을 바꿔도 클라이언트 코드는 유지할 수 있다.
class Order {
constructor(data) {
this.priority = data.priority;
...
}
get priority() {
return this._priority;
}
set priority(aString) {
this._priority = aString
}
}
- 우선순위 속성을 표현하는 값 클래스 Priority를 만든다.
class Priority {
constructor(value) {
this._value = value;
}
toString() {
return this._value;
}
}
- 전 단계에서 만들은 Priority 클래스를 사용하도록 접근자들을 수정한다.
class Order {
constructor(data) {
this.priority = data.priority;
...
}
get priority() {
return this._priority.toString();
}
set priority(aString) {
this._priority = new Priority(aString)
}
}
- get priority()를 의미에 맞게 함수 이름을 바꾼다.
class Order {
constructor(data) {
this.priority = data.priority;
...
}
get priorityString() {
return this._priority.toString();
}
set priority(aString) {
this._priority = new Priority(aString)
}
}
// 클라이언트
highPriorityCount = orders.filter(o => "high" === o.priorityString
|| "rush" === o.priorityString)
.length
7.4 임시 변수를 질의 함수로 바꾸기
배경
- 임시 변수는 아예 함수로 만들어 사용하는 편이 나을 때가 많다.
- 변수 대신 함수로 만들어두면 비슷한 계산을 수행하는 다른 함수에서도 사용할 수 있어 코드 중복이 줄어든다.
- 이 리팩토링 기법은 클래스 안에서 적용할 때 효과가 좋다
- 클래스는 추출할 메서들에 공유 컨텍스트를 제공하기 때문이다.
- 스냅숏 용도로 쓰이는 변수에는 이 리팩터링을 적용하면 안된다.
절차
- 변수가 사용되기 전에 값이 활실히 결정되는지, 변수를 사용할 때마다 계산 로직이 매번 다른 결과를 내지는 않는지 확인한다.
- 읽기전용으로 만들 수 있는 변수는 읽기전용으로 만든다.
- 변수 대입문을 함수로 추출한다.
- 변수 인라인하기로 임시 변수를 제거한다.
예시
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;
7.5 클래스 추출하기
배경
- 메서드와 데이터가 너무 많은 클래스는 이해하기가 쉽지 않으니 잘 살펴보고 적절히 분리하는 것이 좋다.
- 일부 데이터와 메서드를 따로 묶을 수 있다면 어서 분리하라는 신호다
- 함께 변경되는 일이 많거나 서로 의존하는 데이터들도 분리한다.
- 제거해도 다른 필드나 메서드 들이 논리적으로 문제가 없다면 분리할 수 있다는 뜻이다.
절차
- 클래스의 역할을 분리할 방법을 정한다.
- 분리될 역할을 담당할 클래스를 새로 만든다.
- 원래 클래스는 남은 역할에 따라 이름을 적절히 바꿔준다.
- 원래 클래스의 생성자에서 새로운 클래스의 인스턴스를 생성하여 필드에 저장해둔다.
- 분리될 역할에 필요한 필드들을 새 클래스로 옮긴다. 하나씩 옮길 때마다 테스트한다.
- 메서드들도 새 클래스로 옮긴다. 이때 저수준 메서드. 즉 호출을 당하는 일이 많은 메서드부터 옮긴다.
- 양쪽 클래스의 인터페이스를 살펴보면서 불필요한 메서드를 제거하고, 이름도 새로운 환경에 맞게 바꾼다.
- 새 클래스를 외부로 노출할지 정한다. 노출하려거든 새 클래스에 참조를 값으로 바꾸기를 적용할지 고민한다.
예시
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;
}
}
7.6 클래스 인라인하기
배경
- 클래스 추출하기를 거꾸로 돌리는 리팩터링이다.
- 제 역할을 못 해서 그대로 두면 안되는 클래스를 인라인한다.
- 두 클래스의 기능은 지금과 다르게 배분하고 싶을 때도 클래스를 인라인한다.
- 클래스를 인라인해서 합친 다음 추출하는게 더 쉬울때도 있기 때문
절차
- 소스 클래스의 각 public 메서드에 대응하는 메서드들을 타깃 클래스에 생성한다.
- 소스 클래스의 메서드를 사용하는 코드를 모두 타깃 클래스의 위임 메서드를 사용하도록 바꾼다.
- 소스 클래스의 메서드와 필드를 모두 타깃 클래스로 옮긴다.
예시
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 Person {
get officeAreaCode() {
return this._officeAreaCode;
}
get officeNumber() {
return this._officeNumber;
}
}
7.7 위임 숨기기
배경
- 모듈화 설계를 제대로 하는 핵심은 캡슐화다.
- 캡슐화는 모듈들이 시스템의 다른 부분에 대해 알아야 할 내용을 줄여준다.
- 서버 자체에 위임 메서드를 만들어서 위임 객체(서버 객체의 필드가 가리키는 객체)의 존재를 숨기면 클라이언트 - 위임 객체의 인터페이스 간 의존성이 사라진다.
절차
- 위임 객체의 각 메서드에 해당하는 위임 메서드를 서버에 생성된다.
- 클라이언트가 위임 객체 대신 서버를 호출하도록 수정한다.
- 모두 수정했다면, 서버로부터 위임 객체를 얻는 접근자를 제거한다.
예시
manager = aPerson.department.manager;
----------------------------------------
manager = aPerson.manager;
class Person {
get manager() {
return this.department.manager
}
}
7.8 중개자 제거하기
배경
- 클라이언트가 위임 객체의 또 다른 기능을 사용하고 싶을 때마다 서버에 위임 메서드를 추가해야 하는데, 이러다보면 단순히 전달만 하는 위임 메서드들이 많아 진다.
- 이럴때 서버 클래스는 중개자 역할로 전락하여, 클라이언트가 위임 객체를 직접 호출하는 것이 낫다.
절차
- 위임 객체를 얻는 게터를 만든다.
- 위임 메서드를 호출하는 클라이언트가 모두 이 게터를 거치도록 수정한다.
- 모두 소정했다면 위임 메서드를 삭제한다.
예시
manager = aPerson.manager;
class Person {
get manager() {
return this.department.manager
}
}
----------------------------------------
manager = aPerson.department.manager;
- 위임 숨기기나 중개자 제거하기를 적당히 섞어도 된다. 자주 쓰는 위임은 그대로 두는 편이 클라이언트 입장에서 편하다.
7.9 알고리즘 교체하기
배경
- 때로는 알고리즘 전체를 걷어내고 훨씬 간결한 알고리즘으로 바꿔야 할 때가 있다.
- 문제를 더 확실히 이해하고 훨씬 쉽게 해결하는 방법을 발견했을 때 이렇게 한다.
- 거대하고 복잡한 알고리즘을 교체하기란 상당히 어려우니 알고리즘을 간소화하는 작업부터 해야 교체가 쉬워진다.
절차
- 교체할 코드를 함수 하나에 모은다.
- 이 함수만을 이용해 동작을 검증하는 테스트를 마련한다.
- 대체할 알고리즘을 준비한다.
- 정적 검사를 수행한다.
- 기존 알고리즘과 새 알고리즘의 결과를 비교하는 테스트를 수행한다.
'핌팩토링' 카테고리의 다른 글
9. 데이터 조직화 (0) | 2022.04.28 |
---|---|
8. 기능 이동 (0) | 2022.04.27 |
6. 기본적인 리팩터링 (0) | 2022.03.26 |
4. 테스트 구축하기 (0) | 2022.03.12 |
3. 코드에서 나는 악취 (0) | 2022.02.28 |