캡슐화
모듈을 분리하는 가장 중요한 기준은 아마도 시스템에서 각 모듈이 자신을 제외한 다른 부분에 드러내지 않아야할 비밀을 얼마나 잘 숨기느냐에 있다.
이러한 비밀 중 대표적인 형태인 데이터 구조는 레코드 캡슐화하기 와 컬렉션 캡슐화하기로 캡슐화해서 숨길 수 있다.
심지어 기본형 데이터도 기본형을 객체로 바꾸기로 캡슐화할 수 있다.
리팩터링할 때 임시 변수가 자주 걸리적 거리는데, 정확한 순서대로 계산해야하고 리팩터링 후에도 그 값을 사용하는 코드에서 접근할 수 있어야 하기 때문이다. <= 이때는 임시 변수를 질의함수로 바꾸기가 사용된다. (특히 길이가 너무 긴 함수를 쪼갤 때 유용)
클래스는 원래 정보를 숨기는 용도로 설계되었다.
그 뿐만 아니라 클래스는 내부 정보 와 클래스 사이의 연결 관계를 숨기는 데에도 유용하다.
그래도 너무 많이 숨기면 인퍼페이스가 비대해질 수 있으니 유의하자. (중개자 제거하기 기법이 사용됨)
레코드 캡슐화하기
배경
거의 모든 프로그래밍 언어는 데이터를 레코드 형태로 제공한다.
레코드가 연관된 여러 데이터를 묶어서 의미있는 단위로 전달할 수 있게 해주기 때문이다.
그런데 단순 레코드는 단점이 있다.
=> 계산해서 얻을 수 있는 값과, 그렇지 않은 값을 명확히 구분해서 저장해야하는 것이 번거롭다.
레코드는 2가지로 구분된다.
- 필드 이름을 노출하는 레코드
- 필드 이름을 외부로 부터 숨겨서 내가 원하는 이름을 쓸 수 있게하는 레코드
=> 해시, 맵, 해시맵, 딕셔너리, 연관 배열 등
많은 프로그래밍 언어가 해시맵을 쉽게 만드는 문법을 제공하고, 작업에도 유용할 수 있으나, 필드를 명확히 알려주지 않는다는것이 단점이 될 수 있다.
리팩터링 절차
- 레코드를 담을 변수를 캡슐화 한다.
- 레코드를 감싼 단순한 클래스로 해당 변수의 내용을 교체
- 테스트
- 원본 레코드 대신에 새로 정의한 클래스 타입의 객체를 반환하는 함수를 새로 생성
- 클래스에서 원본데이터를 반환하는 접근자와 원본 레코드를 반환하는 함수 제거
간단한 예시
const organization = {name: "하루", country: "KO"}
위 상수는 프로그램 곳곳에서 레코드 구조로 사용되는 js 객체이고,
코드 베이스 상에서 다음과 같이 reac/write된다고 가정하자.
result += `<h1>${organization.name}</h1>`; // read
organization.name = newName; // write
- 레코드를 담을 변수를 캡슐화 한다.
function getRawDataOfOrganization(){return organization._data;}
그러면 read/write 코드는 다음과 같이 바뀐다.
result += `<h1>${getRawDataOfOrganization().name}</h1>`; // read
getRawDataOfOrganization().name = newName; // write
- 레코드를 클래스로 바꾼다.
class Organization {
constructor(data){
this._data = data;
}
}
어디선가 테스팅
새 클래스의 인스턴스를 반환하는 함수를 만든다.
const organization = new Organization({
name: "도비",
country: "US"
});
function getRawDataOfOrganization(){return organization._data;}
function getOrganization(){return organization;}
- 레코드를 사용하던 코드를 살펴보고, 레코드를 갱신하던 코드를 모두 setter를 사용하도록 교체하고, 레코드를 읽는 코드는 getter를 사용하도록 교체한다.
set name(aString) {this._data.name = aString;}
getOrganization().name = newName;
get name() {return this._data.name;}
result += `<h1>${getOrganization().name}</h1>`;
- 다 바꿨으면 아까 이상한 이름으로 지었던 임시함수
getRawDataOfOrganization를 제거한다.
완성본은 다음과 같다.
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;}
}
복잡한 예시 (중첩된 레코드 캡슐화 하기)
다음과 같은 레코드가 있다고 가정해보자
"1920": {
name: "심바",
id: "1920",
usages: {
"2016": {
"1": 50,
"2": 55,
// 나머지 month 생략
},
"2015": {
"1": 70,
"2": 63,
// 나머지 month 생략
}
}
},
"38673": {
name: "다른 사람 이름",
id: "38673",
... // 다른 고객 정보도 똑같이 작성됨.
}
겪어 본 사람들은 알겠지만, 중첩 정도가 심할수록, 읽거나 쓸 때 데이터 구조 안으로 더 깊숙히 들어가야 합니다.
이렇게요
// read
function compareUsage(customerID, laterYear, month){
const later = customerData[customerID].usages[laterYear][month];
const earlier = customerData[cusromerID].usages[laterYear - 1][month];
return {
laterAmount: later,
chage: later - earlier
};
}
// write
customerData[customerID].usages[year][month] = amount;
크으.. 굉장히 사용하기 더럽습니다.
이런 중첩된 레코드도 앞에서와 마찬가지로 변수 캡슐화부터 해줍니다. (이름은 대충 지어도됨 어짜피 없앨거라;)
function getRawDataOfCustomers() {return customerData;}
function setRawDataOfCustomers(arg) {customerData = arg;}
이렇게 바꾸고
아까 read/write 하던 부분을 바꿔줍니다.
// ! cusromerData가 getRawDataOfCustomers()으로 바뀜
// read
function compareUsage(customerID, laterYear, month){
const later = getRawDataOfCustomers()[customerID].usages[laterYear][month];
const earlier = getRawDataOfCustomers()[cusromerID].usages[laterYear - 1][month];
return {
laterAmount: later,
chage: later - earlier
};
}
// write
getRawDataOfCustomers()[customerID].usages[year][month] = amount;
이제 전체 데이터 구조를 표현하는 클래스를 정의하고 그것을 반환하는 함수를 새로 만듭니다.
class CustomerData {
constructor(data){
this._data = data;
}
}
function getCustomerData() {return customerData;}
function getRawDataOfCustomers() {return customerData._data;}
function setRawDataOfCustomers(arg) {customerData = new CustomerData(arg);}
function setUsage(customerID, year, month, amount){
setRawDataOfCustomers()[customerID].usages[year][month] = amount;
}
이제 위 함수들을 클래스로 옮긴다. (함수 옮기기 기법 사용)
class CustomerData{
constructor(data){
this._data = data;
}
get rawData(){
return _.cloneDeep(this._data);
}
setUsage(customerID, year, month, amount){
this._data[customerID].usages[year][month] = amount;
}
}
위 처럼, 읽기는 깊은 복사를 통해 처리해도 괜찮다.(단, 데이터 구조가 클수록 복제 비용이 커져서 성능이 느려질 수 있다. 한번 더 말하지만 성능은 측정해보고 그 때 판단해도 늦지 않다.)
getter로 객체를 반환하니까, 클라이언트가 원본을 수정하는 것 같은 착각이 들 수도 있는데 이런건, 읽기전욕 proxy를 만들어주던가 freeze를 해주어 예외를 발생시키는 것이 현명할 것이다.
'개발 > Web Programming' 카테고리의 다른 글
quiz.typeofnan.dev 문제 정리 - 5 (0) | 2021.06.19 |
---|---|
quiz.typeofnan.dev 문제 정리 - 4 (0) | 2021.06.18 |
우테코 react-shopping-cart 학습로그 작성 (0) | 2021.06.14 |
내가 보려고 만든 "useEffect 완벽 가이드" 요약 (1) | 2021.06.13 |
우테코 react-payment 미션 학습로그 작성 (1) | 2021.06.13 |