shallow encapsulation
자바스크립트는 객체 지향 프로그래밍을 지원하기 위해 다양한 키워드와 수단을 지속적으로 추가해나가고 있다. 이러한 변화는 개발자들이 복잡한 객체 구조를 다룰 때 가독성과 유지보수성을 높이고, 캡슐화를 보다 쉽게 구현할 수 있도록 돕는다. 특히, #를 사용한 프라이빗 멤버 변수는 기존의 약속된 네이밍 관례(예: _로 시작하는 변수명)를 대체하며, 접근 제어를 언어 차원에서 명확히 보장한다는 점에서 큰 의의를 가진다. 이러한 기능들은 자바스크립트를 단순한 스크립팅 언어에서 벗어나 대규모 애플리케이션 개발에도 적합한 언어로 발전시키는 데 중요한 역할을 하고 있다.
그러나 한편으로는 이러한 설계가 개발자들에게 오해를 불러일으킬 여지가 있다. #를 사용하기만 하면 캡슐화가 완벽하게 보장된다고 착각하는 경우가 그것이다. 프라이빗 멤버는 외부에서 직접 접근할 수 없도록 설계되었지만, 캡슐화는 단순히 접근 제어를 넘어 객체의 설계 전반에 걸쳐 신중한 고려가 필요하다. 객체의 동작을 부적절하게 설계하거나 내부 상태를 쉽게 노출하는 메서드를 제공할 경우, 프라이빗 멤버라도 간접적으로 노출될 위험이 있다.
아래의 클래스는 얼핏 보기에 캡슐화가 잘 이루어진 것처럼 보인다. #array는 프라이빗 멤버 변수로 선언되어 외부에서는 직접 접근할 수 없고, 오직 getter를 통해서만 값을 읽을 수 있다. 별도의 setter를 정의하지 않았기 때문에 #array를 수정할 방법은 없어 보인다.
class Test {
#array = [{ value: 1000 }];
get array() {
return [...this.#array];
}
};
그런데도 캡슐화를 했음에도 외부에서 내부 구조를 변경하는 시도가 문제 없이 이루어졌다. 이 현상은 자바스크립트에서 객체가 참조 타입으로 동작하기 때문에 발생하는 "얕은 캡슐화(shallow encapsulation)"의 전형적인 사례이다. getter가 #array의 참조를 반환하기 때문에, 외부에서 반환된 배열에 접근하여 내부의 객체를 수정할 수 있었던 것이다.
const test = new Test()
test.array[0] = 2000
console.log(test.array) // [ 2000 ]
캡슐화의 목표는 객체 내부 상태를 외부로부터 완전히 보호하여, 상태 변경이 객체의 의도된 메서드를 통해서만 이루어지도록 보장하는 것이다. 그러나 위와 같은 경우, 참조 타입 데이터가 외부로 노출되면 내부 상태가 외부에서 직접 수정될 위험이 생긴다. 객체의 상태 관리가 복잡해지고, 예상치 못한 버그가 발생할 가능성이 커지는 것이다.
아래처럼 스프레드 연산자를 사용해 배열을 복사하고, 객체를 얼려도 소용이 없다. 둘 다 얕은 수준에서만 복사와 냉동이 이루어지기 때문에 중첩된 참조 타입의 내부 값은 여전히 외부에서 수정이 가능하다.
class Test {
#array = [{ value: 1000 }]
get array() {
return Object.freeze([...this.#array]);
}
}
나는 이러한 얕은 캡슐화 문제를 방지하고자 deepFreeze라는 이름의 유틸리티 함수를 만들어 사용하고 있다. deepFreeze는 객체의 모든 중첩된 속성까지 재귀적으로 동결하여, 참조 타입 데이터가 외부에서 수정되지 않도록 보장한다. 이를 통해 캡슐화의 목표를 보다 완벽하게 달성할 수 있다.
function deepFreeze(obj) {
Object.freeze(obj); // 현재 객체 동결
// 객체의 속성 중 객체인 항목들 재귀적으로 동결
Object.keys(obj).forEach((key) => {
if (
typeof obj[key] === "object" &&
obj[key] !== null &&
!Object.isFrozen(obj[key])
) {
deepFreeze(obj[key]);
}
});
return obj;
}
이제 deepFreeze를 사용하여 getter가 리턴하는 값을 얼려버리면 완전한 캡슐화를 달성할 수 있다.
class Test {
#array = [{ value: 1000 }]
get array() {
return deepFreeze([...this.#array]);
}
}
블로그의 정보
Ayden's journal
Beard Weard Ayden