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.03.13
Streams API 4. 왜 모든 언어에는 Stream API가 존재할까
Streams API를 공부하다 보면 묘한 기시감을 느끼게 된다. JavaScript에서 ReadableStream, WritableStream, TransformStream을 살펴보고 있는데, 어딘가 낯설지 않다. Java를 써 본 사람이라면 InputStream, OutputStre…
2026.03.13
Streams API 3. 바이트 스트림과 실전 파이프라인
앞선 두 글에서 우리는 Web Streams API의 표면과 의미론을 정리했다. 이제 남은 질문은 하나이다. 이 지식을 실제 어디에 써먹을 것인가. 이 질문에 답하는 순간 스트림 학습은 비로소 완성된다. 표준을 읽고 메서드를 아는 것만으로는 충분하지 않다. fetch() 응답 본문을 어…
2026.03.13
Streams API 2. 상태와 백프레셔의 의미론
스트림을 처음 배울 때는 읽고 쓰는 예제가 꽤 단순해 보인다. getReader()로 읽고, getWriter()로 쓰고, pipeTo()로 연결하면 끝나는 것처럼 느껴진다. 실제로 짧은 데모는 이 정도만 알아도 돌아간다. 그러나 실무에서 스트림 코드를 망가뜨리는 원인은 거의 언제나 더…
2026.03.13
Streams API 1. 읽기와 쓰기의 출발점
자바스크립트에서 비동기를 배울 때 우리는 대개 Promise와 async, await부터 익힌다. 이 조합은 한 번의 결과를 기다리는 문제에는 매우 강력하다. 그러나 데이터가 한 번에 끝나지 않고 계속 흘러들어오는 상황에서는 이야기가 달라진다. 네트워크 응답이 길게 이어지거나, 큰 파일…
2026.04.17
Code Server
처음 코드 서버를 만들었던 건 아마도 3년쯤 전의 일이다. 맥미니만 있던 탓에 밖에서 개발하는 게 쉽지 않았고, 아이패드로 언제 어디서든 개발을 하고 싶었던 끝에 찾아낸 해결책이었다. 다행히 집에는 Synology NAS가 있었고, Docker를 통해 어렵지 않게 코드 서버를 만들 수…
2026.04.11
Trie 자료구조
문자열 데이터를 다룰 때 단순히 “이 단어가 있나?”만으로는 부족한 순간이 있다. 자동 완성처럼 특정 접두사로 시작하는 후보를 모아야 할 때도 있고, 어떤 키에 값을 두고 빠르게 찾고 싶을 때도 있다. 이럴 때 가장 자연스럽게 떠올릴 수 있는 자료구조가 바로 Trie이다.Trie는 무엇…
2026.03.19
Streams API 부록 2. 왜 이미지는 위에서 아래로 나타날까
웹 페이지에서 이미지를 로드할 때 흥미로운 장면을 종종 볼 수 있다. 이미지가 한 번에 완전히 나타나는 것이 아니라, 위에서 아래로 조금씩 채워지면서 나타나는 경우가 있기 때문이다. 특히 네트워크가 느리거나 이미지가 큰 경우 이런 현상이 더 분명하게 보인다. 마치 이미지가 위쪽부터 스캔…
2026.03.19
Streams API 부록 1. HTTP 다운로드 진행률은 어떻게 계산될까
파일을 다운로드할 때 가끔 몇 퍼센트 진행되었는지 혹은 진행 막대(progress bar)가 조금씩 채워지는 모습을 볼 수 있다. 그런데 모든 다운로드가 이런 식으로 진행률을 보여 주는 것은 아니다. 어떤 다운로드는 퍼센트가 표시되지만, 어떤 경우에는 진행 막대 없이 로딩 스피너만 계속…
댓글
댓글을 불러오는 중...