shallow encapsulation
작성일:2025.01.28|조회수:0

자바스크립트는 객체 지향 프로그래밍을 지원하기 위해 지속적으로 언어 기능을 확장해왔다. class 문법의 도입, private field(#)의 추가는 그 대표적인 예다. 특히 #을 이용한 프라이빗 멤버는 기존의 관례적 약속(예: _private)과 달리, 언어 차원에서 접근을 차단한다는 점에서 의미가 크다. 이로 인해 자바스크립트는 더 이상 단순한 스크립팅 언어가 아니라, 대규모 애플리케이션에서도 일정 수준의 객체 모델링을 감당할 수 있는 언어가 되었다.
그러나 문제는 여기서 시작된다. 많은 개발자가 #를 사용했다는 사실만으로 “캡슐화가 완료되었다”고 착각한다. 하지만 캡슐화(encapsulation)는 단순히 접근을 막는 것(access control) 이 아니라, 상태 변경의 통로를 통제하는 것에 가깝다. 다시 말해, 외부에서 내부 상태를 어떤 방식으로든 변경할 수 있다면, 그것은 이미 캡슐화가 깨진 상태다.
다음과 같은 클래스는 겉보기에는 캡슐화가 잘 이루어진 것처럼 보인다. 내부 상태는 프라이빗 필드로 숨겨져 있고, 외부에서는 getter를 통해서만 값을 조회할 수 있다. setter가 없기 때문에 외부에서 값을 수정할 수 없는 구조처럼 보이기도 한다.
class Test {
#array = [{ value: 1000 }];
get array() {
return [...this.#array];
}
}그러나 실제로는 외부 코드에서 내부 상태를 문제없이 변경할 수 있다. getter를 통해 반환된 배열에 접근해 내부 객체의 값을 수정하면, 그 변경 사항은 그대로 객체 내부에 반영된다.
const test = new Test();
test.array[0].value = 2000;
console.log(test.array); // [{ value: 2000 }]이 현상은 자바스크립트의 참조 타입 특성에서 비롯된다. 스프레드 연산자를 사용하면 배열 자체는 새로 생성되지만, 배열 안에 포함된 객체까지 복사되지는 않는다. 결국 외부와 내부가 동일한 객체 참조를 공유하게 되고, 외부에서의 수정이 내부 상태 변경으로 이어진다. 이처럼 접근은 차단되어 있지만 참조를 통해 상태가 변경될 수 있는 상태를 나는 얕은 캡슐화, 즉 shallow encapsulation이라고 부른다.
얕은 캡슐화가 문제가 되는 이유는 상태 변경의 통제권이 객체 외부로 흘러나가기 때문이다. 객체가 스스로 유지해야 할 불변 조건이 외부 코드에 의해 쉽게 깨질 수 있고, 상태가 언제 어디서 변경되었는지 추적하기도 어려워진다. 이는 유지보수성과 디버깅 난이도를 크게 높인다.
이 문제를 해결하기 위해 흔히 떠올리는 방법이 Object.freeze를 사용하는 것이다. 반환되는 값을 얼려버리면 외부에서 수정할 수 없을 것처럼 보인다.
class Test {
#array = [{ value: 1000 }];
get array() {
return Object.freeze([...this.#array]);
}
}그러나 이 역시 근본적인 해결책은 아니다. Object.freeze 또한 얕은 동결만 수행하기 때문이다. 배열 자체는 수정할 수 없게 되지만, 배열 내부에 포함된 객체는 여전히 변경 가능하다. 결과적으로 참조 타입이 중첩된 구조에서는 동일한 문제가 반복된다.
이러한 한계를 보완하기 위해 나는 deepFreeze라는 유틸리티 함수를 사용한다. 이 함수는 객체의 모든 중첩된 속성을 재귀적으로 순회하며 동결한다. 즉, 객체 그래프 전체를 불변 상태로 만든다.
function deepFreeze(obj) {
Object.freeze(obj);
Object.keys(obj).forEach((key) => {
const value = obj[key];
if (
typeof value === "object" &&
value !== null &&
!Object.isFrozen(value)
) {
deepFreeze(value);
}
});
return obj;
}이제 getter에서 반환하는 값을 deepFreeze로 감싸면, 외부에서는 어떤 방식으로도 내부 상태를 변경할 수 없다.
class Test {
#array = [{ value: 1000 }];
get array() {
return deepFreeze([...this.#array]);
}
}다만 deepFreeze는 어디까지나 방어적 수단이라는 점을 인식해야 한다. 깊은 객체 구조일수록 성능 비용이 증가하고, 불변성을 강하게 전제한 설계를 요구한다. 경우에 따라서는 값을 그대로 반환하는 대신, 필요한 정보만 제공하는 쿼리 메서드를 설계하는 편이 더 나은 선택일 수 있다.
예를 들어 내부 배열 전체를 노출하는 대신, 특정 값을 조회하는 메서드를 제공하면 참조 자체를 외부로 내보내지 않아도 된다. 이러한 설계는 애초에 얕은 캡슐화 문제가 발생할 여지를 제거한다.
결국 캡슐화의 완성도는 #를 사용했는지 여부가 아니라, 외부에 어떤 형태의 값을 노출했는지에 달려 있다. 참조 타입을 그대로 반환하는 순간, 프라이빗 필드의 의미는 크게 퇴색된다. 캡슐화는 문법의 문제가 아니라 설계의 문제이며, shallow encapsulation을 인식하지 못하면 “캡슐화된 것처럼 보이는 코드”를 계속해서 만들어내게 된다.
더 읽어보기
2026.04.11
Trie 자료구조
문자열 데이터를 다룰 때 단순히 “이 단어가 있나?”만으로는 부족한 순간이 있다. 자동 완성처럼 특정 접두사로 시작하는 후보를 모아야 할 때도 있고, 어떤 키에 값을 두고 빠르게 찾고 싶을 때도 있다. 이럴 때 가장 자연스럽게 떠올릴 수 있는 자료구조가 바로 Trie이다. Trie는 무…
2026.03.19
Streams API 부록 2. 왜 이미지는 위에서 아래로 나타날까
느린 네트워크에서 큰 이미지를 열면 가끔 화면이 위에서 아래로 채워진다. 마치 누군가 아주 성실하게 롤러로 이미지를 칠하는 것처럼 보인다. 물론 브라우저 안에 그런 직원은 없다. 있다면 우리보다 야근을 더 많이 하고 있을 것이다. 이 현상은 단순한 시각 효과가 아니라 네트워크 전송, 브…
2026.03.19
Streams API 부록 1. HTTP 다운로드 진행률은 어떻게 계산될까
다운로드 화면에서 진행 막대가 조금씩 차오르면 이상하게 안심된다. 반대로 스피너만 계속 돌면 파일이 오는 중인지, 서버가 고민 중인지, 내 인생이 잠깐 멈춘 건지 알 수 없다. 사용자는 둘 다 “다운로드 중”이라고 느끼지만, 내부적으로는 꽤 다른 상황일 수 있다. 진행률 계산 자체는 복…
2026.03.13
Streams API 4. 왜 모든 언어에는 Stream API가 존재할까
Streams API를 공부하다 보면 묘한 기시감이 온다. JavaScript에서 ReadableStream, WritableStream, TransformStream을 보고 있는데, Java의 InputStream, Go의 io.Reader, Rust의 Read와 Write가 멀리서…
2026.05.26
차트는 멈췄는데 윈도우가 움직인다
상황 어느날 서비스를 살펴보시던 팀장님께서 이런 말씀을 slack에 남기셨다. 진호님, 예측 차트에서 zoom을 계속하면 어느 순간 라인 차트가 아니라 단일 스캐터 차트처럼 보이는 데, 이거 수정하면 좋을 거 같아요. 어느정도 zoom을 하면 그 이후로는 zoom이 안 되도록 할 수 없…
2026.05.21
피자가게로 이해하는 디자인 패턴
에이든 피자는 처음부터 복잡한 시스템을 만들 생각이 없었다. 처음에는 메뉴 몇 개만 만들면 됐다. 그런데 손님은 커스텀 주문을 넣기 시작했고, 주방은 상태를 나눠야 했고, 결제와 배달앱과 알림이 하나씩 붙었다. 코드도 가게를 닮는다. 장사가 잘될수록 이상하게 더 쉽게 망가진다. 디자인…
2026.05.21
7. Decorator — 토핑 추가할 때마다 클래스를 새로 만들 수 없다
에이든 피자에서 주문서를 객체로 만들자 취소와 재주문은 한결 편해졌다. 그런데 주문이 편해지자 손님들도 한결 편해졌다. 편해진 손님은 더 많은 요구를 한다. "치즈 추가요", "올리브도 추가요", "소스 많이요", "조금 더 바삭하게 구워주세요" 같은 요청이 주문대 위로 쌓이기 시작했다…
2026.05.21
6. Command — 주문서를 객체로 만들면 취소도 재주문도 쉬워진다
에이든 피자의 시범 운영이 어느덧 일주일을 향해 가고 있다. 주방은 이제 제법 능숙하게 피자를 구워내고, 포스기(POS) 시스템도 팩토리 메서드 덕분에 다양한 지점 메뉴를 무리 없이 받아낸다. 하지만 시스템이 안정될수록 요구사항은 더 정교해지기 마련이다. 오늘은 주문 그 자체가 문제가…
댓글
댓글을 불러오는 중...