구어체로 설명하는 다이어리
06 기본적인 리팩터링 - 변수 캡슐화하기 본문
변수 캡슐화하기
let defaultOwner = { firstName: '마틴', lastName: '파울러' };
▼
let defaultOwnerData = { firstName: '마틴', lastName: '파울러' };
export function defaultOwner() {
return defaultOwnerData;
}
export function setDefaultOwner(arg) {
defaultOwnerData = arg;
}
배경
리팩터링은 결국 프로그램의 요소를 조작하는 일이다. 함수는 데이터보다 다루기가 수월하다. 함수를 사용한다는 건 대체로 호출한다는 뜻이고, 함수의 이름을 바꾸거나 다른 모듈로 옮기기는 어렵지 않다. 여차하면 기존 함수를 그대로 둔 채 전달forward 함수로 활용할 수도 있기 때문이다(즉, 예전 코드들은 변함 없이 기존 함수를 호출하고, 이 기존 함수가 새로 만든 함수를 호출하는 식이다). 이런 전달 함수를 오래 남겨둘 일은 별로 없지만 리팩터링 작업을 간소화하는데 큰 역할을 한다.
반대로 데이터는 함수보다 다루기가 까다로운데, 그 이유는 이런 식으로 처리할 수 없기 때문이다. 데이터를 참조하는 모든 부분을 한 번에 바꿔야 코드가 제대로 작동한다. 짧은 함수 안의 임시 변수처럼 유효범위가 아주 좁은 데이터는 어려울 게 없지만, 유효범위가 넓어질수록 다루기 어려워진다. 전역 데이터가 골칫거리인 이유도 바로 여기에 있다.
그래서 접근할 수 있는 범위가 넓은 데이터를 옮길 때는 먼저 그 데이터로의 접근을 독점하는 함수를 만드는 식으로 캡슐화하는 것이 가장 좋은 방법일 때가 많다. 데이터 재구성이라는 어려운 작업을 함수 재구성이라는 더 단순한 작업으로 변환하는 것이다.
데이터 캡슐화는 다른 경우에도 도움을 준다. 데이터를 변경하고 사용하는 코드를 감시할 수 있는 확실한 통로가 되어주기 때문에 데이터 변경 전 검증이나 변경 후 추가 로직을 쉽게 끼워넣을 수 있다. 나는 유효범위가 함수 하나보다 넓은 가변 데이터는 모두 이런 식으로 캡슐화해서 그 함수를 통해서만 접근하게 만드는 습관이 있다. 데이터의 유효범위가 넓을수록 캡슐화해야 한다. 레거시 코드를 다룰 때는 이런 변수를 참조하는 코드를 추가하거나 변경할 때마다 최대한 캡슐화한다. 그래야 자주 사용하는 데이터에 대한 결합도가 높아지는 일을 막을 수 있다.
객체 지향에서 객체의 데이터를 항상 private로 유지해야 한다고 그토록 강조하는 이유가 바로 여기에 있다. 나는 public 필드를 발견할 때마다 캡슐화해서(이 경우에는 흔히 '필드 캡슐화하기'로 부른다) 가시 범위를 제한하려 한다. 클래스 안에서 필드를 참조할 때조차 반드시 접근자를 통하게 하는 자가 캡슐화self-encapsulation를 주장하는 사람도 있다. 개인적으로는 자가 캡슐화까지는 좀 지나치지 않나 생각한다. 필드를 자가 캡슐화해야 할 정도로 클래스가 크다면 잘게 쪼개야 하기 때문이다. 하지만 클래스를 쪼개기 전 단계로써 필드를 자가 캡슐화하는 것은 도움이 된다.
불변 데이터는 가변 데이터보다 캡슐화할 이유가 적다. 데이터가 변경될 일이 없어서 갱신 전 검증 같은 추가 로직이 자리할 공간을 마련할 필요가 없기 때문이다. 게다가 불변 데이터는 옮길 필요 없이 그냥 복제하면 된다. 그래서 원본 데이터를 참조하는 코드를 변경할 필요도 없고, 데이터를 변형시키는 코드를 걱정할 일도 없다. 불변성은 강력한 방부제인 셈이다.
절차
- 변수로의 접근과 갱신을 전담하는 캡슐화 함수들을 만든다.
- 정적 검사를 수행한다.
- 변수를 직접 참조하던 부분을 모두 적절한 캡슐화 함수 호출로 바꾼다. 하나씩 바꿀 때마다 테스트한다.
- 변수의 접근 범위를 제한한다.
- → 변수로의 직접 접근을 막을 수 없을 때도 있다. 그럴 때는 변수 이름을 바꿔서 테스트해보면 해당 변수를 참조하는 곳을 쉽게 찾아낼 수 있다.
- 테스트한다.
- 변수 값이 레코드라면 레코드 캡슐화하기를 적용할지 고려해본다.
예시
전역 변수에 중요한 데이터가 담겨 있는 경우를 생각해보자.
let defaultOwner = { firstName: '마틴', lastName: '파울러' };
데이터라면 당연히 다음과 같이 참조하는 코드가 있을 것이다.
spaceship.owner = defaultOwner;
갱신하는 코드 역시 있을 것이다.
defaultOwner = { firstName: '레베카', lastName: '파슨스' };
기본적인 캡슐화를 위해 가장 먼저 데이터를 읽고 쓰는 함수부터 정의한다.
function getDefaultOwner() {
return defaultOwner;
}
function setDefaultOwner(arg) {
defaultOwner = arg;
}
그런 다음 defaultOwner를 참조하는 코드를 찾아서 방금 만든 게터 함수를 호출하도록 고친다.
spaceship.owner = getDefaultOwner();
대입문은 세터 함수로 바꾼다.
setDefaultOwner({ firstName: '마틴', lastName: '파울러' });
하나씩 바꿀 때마다 테스트한다.
모든 참조를 수정했다면 이제 변수의 가시 범위를 제한한다. 그러면 미처 발견하지 못한 참조가 없는지 확인할 수 있고, 나중에 수정하는 코드에서도 이 변수에 직접 접근하지 못하게 만들 수 있다. 자바스크립트로 작성할 때는 변수와 접근자 메서드를 같은 파일로 옮기고 접근자만 노출export 시키면 된다.
let defaultOwner = { firstName: '마틴', lastName: '파울러' };
export function getDefaultOwner() {
return defaultOwner;
}
export function setDefaultOwner(arg) {
defaultOwner = arg;
}
변수로의 접근을 제한할 수 없을 때는 변수 이름을 바꿔서 다시 테스트해보면 좋다. 이렇게 한다고 해서 나중에 직접 접근하지 못하게 막을 수 있는 건 아니지만, __privateOnly_defaultOwner처럼 공개용이 아니라는 의미를 담으면서도 눈에 띄는 이름으로 바꾸면 조금이나마 도움이 된다.
마지막으로, 나는 게터 이름 앞에 get을 붙이는 것을 싫어해서 get을 빼도록 하겠다.
let defaultOwnerData = { firstName: '마틴', lastName: '파울러' };
export function defaultOwner() {
return defaultOwnerData;
}
export function setDefaultOwner(arg) {
defaultOwnerData = arg;
}
자바스크립트에서는 게터와 세터의 이름을 똑같이 짓고 인수가 있냐 없냐에 따라 구분하는 방식을 많이 따른다. 나는 이 방식을 오버로딩된 게터-세터Overloaded Getter Setter라고 부르며, 아주 싫어한다. 그래서 get은 붙이지 않겠지만 set은 계속 붙이겠다.
값 캡슐화하기
방금 본 기본 캡슐화 기법으로 데이터 구조로의 참조를 캡슐화하면, 그 구조로의 접근이나 구조 자체를 다시 대입하는 행위는 제어할 수 있다. 하지만 필드 값을 변경하는 일은 제어할 수 없다.
const owner1 = defaultOwner();
assert.equal('파울러', owner1.lastName, '처음 값 확인');
const owner2 = defaultOwner();
owner2.lastName = '파슨스';
assert.equal('파슨스', owner1.lastName, 'owner2를 변경한 수'); // 성공할까?
기본 캡슐화 기법은 데이터 항목을 참조하는 부분만 캡슐화한다. 대부분은 이 정도로 충분하지만, 변수뿐 아니라 변수에 담긴 내용을 변경하는 행위까지 제어할 수 있게 캡슐화하고 싶을 때도 많다.
이렇게 하는 방법은 크게 두 가지다. 가장 간단한 방법은 그 값을 바꿀 수 없게 만드는 것이다. 나는 주로 게터가 데이터의 복제본을 반환하도록 수정하는 식으로 처리한다.
let defaultOwnerData = { firstName: '마틴', lastName: '파울러' };
export function defaultOwner() {
return Object.assign({}, defaultOwnerData);
}
export function setDefaultOwner(arg) {
defaultOwnerData = arg;
}
특히 리스트에 이 기법을 많이 적용한다. 데이터의 복제본을 반환하면 클라이언트는 게터로 얻은 데이터를 변경할 수 있지만 원본에는 아무 영향을 주지 못한다. 단, 주의할 점이 있다. 공유 데이터(원본)를 변경하기를 원하는 클라이언트가 있을 수 있다. 이럴 때 나는 문제가 될만한 부분을 테스트로 찾는다. 아니면 아예 변경할 수 없게 만들 수도 있다. 이를 위한 좋은 방법이 레코드 캡슐화하기다.
class Person {
constructor(data) {
this._lastName = data.lastName;
this._fitstName = data.firstName;
}
get lastName() {
return this._lastName;
}
get firstName() {
return this.firstName;
}
// 다른 속성도 이런 식으로 처리한다.
}
이렇게 하면 defaultOwnerData의 속성을 다시 대입하는 연산은 모두 무시된다. 이런 변경을 감지하거나 막는 구체적인 방법은 언어마다 다르므로 사용하는 언어에 맞는 방법으로 처리하면 된다.
이처럼 변경을 감지하여 막는 기법을 임시로 활용해보면 도움될 때가 많다. 변경하는 부분을 없앨 수도 있고, 적절한 변경 함수를 제공할 수도 있다. 적절히 다 처리하고 난 뒤 게터가 복제본을 반환하도록 수정하면 된다.
지금까지는 게터에서 데이터를 복제하는 방법을 살펴봤는데, 세터에서도 복제본을 만드는 편이 좋을 수도 있다. 정확한 기준은 그 데이터가 어디서 오는지, 원본 데이터의 모든 변경을 그대로 반영할 수 있도록 원본으로의 링크를 유지해야 하는지에 따라 다르다. 링크가 필요 없다면 데이터를 복제해 저장하여 나중에 원본이 변경돼서 발생하는 사고를 방지할 수 있다. 복제본 만들기가 번거로울 때가 많지만, 이런 복제가 성능에 주는 영향은 대체로 미미하다. 반면, 원본을 그대로 사용하면 나중에 디버깅하기 어렵고 시간도 오래 걸릴 위험이 있다.
여기서 명심할 점이 있다. 앞에서 설명한 복제본 만들기와 클래스로 감싸는 방식은 레코드 구조에서 깊이가 1인 속성들까지만 효과가 있다. 더 깊이 들어가면 복제본과 객체 래핑 단계가 더 늘어나게 된다.
지금까지 본 것처럼 데이터 캡슐화는 굉장히 유용하지만 그 과정은 간단하지 않을 때가 많다. 캡슐화의 구체적인 대상과 방법은 캡슐화할 데이터를 사용하는 방식과 그 데이터를 어떻게 변경하려는 지에 따라 달라진다. 하지만 분명한 사실은 데이터의 사용 범위가 넓을수록 적절히 캡슐화하는 게 좋다는 것이다.
[출처]
리팩터링 2판 - 마틴 파울러
[정리]
데이터는 리팩터링하기 까다롭다. 그래서 변수를 캡슐화하기 위해서는 일단 데이터의 접근을 독점하는 함수를 만든다. get, set 등의 함수가 그것이다. get으로 데이터를 가져올 때 원본 데이터의 변경을 피하고 싶을 경우 Object.assign등을 이용해 복제본을 만들 수 있고, 아니면 아예 변경할 수 없도록 클래스 형태로 만든다.
'읽을거리 > 리팩터링' 카테고리의 다른 글
06 기본적인 리팩터링 - 매개변수 객체 만들기 (0) | 2024.12.05 |
---|---|
06 기본적인 리팩터링 - 변수 이름 바꾸기 (0) | 2024.11.06 |
06 기본적인 리팩터링 - 함수 선언 바꾸기 (0) | 2024.11.06 |
06 기본적인 리팩터링 - 변수 인라인하기 (0) | 2024.11.05 |
06 기본적인 리팩터링 - 변수 추출하기 (0) | 2024.11.05 |