Ch06 변경 가능한 데이터 구조를 가진 언어에서 불변성 유지하기
모든 동작을 불변형으로 만들 수 있을까?
중첩된 데이터에서 불변 동작을 구현할 수 있을까?
동작을 읽기, 쓰기 또는 둘 다로 분류하기
읽기
- 데이터를 바꾸지 않고 정보를 꺼내는 것
쓰기
어떻게든 데이터를 바꿈
바뀌는 값이 어디서 사용될지 모르기 때문에 바뀌지 않도록 원칙이 필요 👉 불변성 원칙
copy-on-write 원칙 세 단계
지난 장에서 구현한 addElementLast
로 보는 3 단계
// 기존 배열 array 입력
function addElementLast(array, elem) {
// 1. 복사본 만들기
const newArray = array.slice();
// 2. 복사본 변경하기
newArray.push(elem);
// 3. 복사본 리턴하기
return newArray;
}
복사본을 만들고, 기존 배열은 변경하지 않음
복사본은 함수 범위에 있기 때문에, 외부에서 값을 바꾸기 위해 접근할 수 없음
복사본을 변경하고 리턴한 이후에는 값을 바꿀 수 없음
👉 데이터를 바꾸지 않고 정보를 리턴하기 때문에 읽기 동작
- 쓰기를 읽기로 바꾼 것
쓰기와 읽기를 동시에 하는 동작은?
ex. Array.prototype.shift
const a = [1, 2, 3, 4];
const b = a.shift(); // 값을 바꾸는 동시에 배열 첫 번째 항목을 리턴
console.log(b); // 1
console.log(a); // [2, 3, 4]
두가지 방법
읽기와 쓰기 함수로 각각 분리
책임이 확실히 분리되기 때문에 더 좋음
1. 읽기 - 쓰기 동작으로 분리
// 읽기 동작
function getFistElement(array) {
return array[0];
}
// 쓰기 동작
// 리턴값을 사용하지 않으므로 리턴하지 않음
function dropFistElement(array) {
array.shift();
}
2. 쓰기 동작을 copy-on-write로 바꾸기
function dropFistElement(array) {
const copiedArray = array.slice();
copiedArray.shift();
return copiedArray;
}
함수에서 값을 두 개 리턴
1. 동작을 감싸기
메서드를 바꿀 수 있도록 새로운 함수로 감싸기
function shift(array) {
return array.shift();
}
2. 읽기 전용 함수로 바꾸기
copy-on-write 활용
function shift(array) {
const copiedArray = array.slice();
const firstElement = copiedArray.shift();
return { firstElement, copiedArray };
}
변경 가능한 데이터를 읽는 것은 액션, 읽을 때마다 다른 값을 읽을 수도 있음
쓰기는 데이터를 변경 가능한 구조로 만듦
어떤 데이터에 쓰기가 없다면 데이터는 변경 불가능한 데이터가 되고, 불변 데이터 구조를 읽는 것은 계산
따라서 쓰기를 읽기로 바꿀수록, 데이터를 불변형으로 만들수록, 코드에 계산이 많아지고 액션이 줄어든다
불변 데이터 구조에 대한 오해와 사실
변경 가능한 데이터 구조보다 메모리를 더 많이 쓰고 느림
그럼에도 불변 데이터 구조를 사용하면서 대용량 고성능 시스템을 구현하는 사례는 많고, 이는 일반 애플리케이션에 쓰기 충분히 빠르다는 것
언제든 최적화할 수 있음: 섣부른 최적화는 하지 않는 것, 불변 데이터 구조를 사용하고 속도가 느린 부분이 있으면 그때 최적화
가비지 콜렉터는 매우 빠름: 대부분의 언어에서는 가비지 콜렉터 성능이 꾸준히 개선되어 옴
생각보다 많이 복사하지 않음
FP 언어에는 빠른 구현체가 있음
데이터 구조를 복사 할 때 최대한 많은 구조적 공유(얕은 복사)
불변 데이터 구조에서 구조적 공유는 안전하고, 메모리를 적게 사용함
객체에 대한 copy-on-write
Object.assign
메서드 활용
// 원래 코드
function setPriceOrigin(item, newPrice) {
item.price = newPrice;
}
// copy-on-write
function setPriceCopyOnWrite(item, newPrice) {
const copiedItem = Object.assign({}, item);
copiedItem.price = newPrice;
return copiedItem;
}
중첩된 쓰기를 읽기로 바꾸기
중첩된 쓰기도 중첩되지 않은 쓰기와 동일한 패턴
중첩된 모든 데이터 구조가 바뀌지 않아야 불변 데이터
- 중첩된 데이터 일부를 바꾸려면 변경하려는 값과 상위의 모든 값을 복사해야 함
// 원래 코드
function setPriceByName(cart, name, price) {
for (let i = 0; i < cart.length; i++) {
if (cart[i].name === name) {
cart[i].price = price;
}
}
}
// copy-on-write
function setPriceByName(cart, name, price) {
const copiedCart = cart.slice();
for (let i = 0; i < copiedCart.length; i++) {
if (copiedCart[i].name === name) {
copiedCart[i] = setPriceCopyOnWrite(copiedCart[i], price);
}
}
return copiedCart;
}
Clojure
, Haskell
등 FP 언어는 기본적으로 copy-on-write를 지원하지만,
JS
에서는 직접 구현해야 하므로 유틸 함수로 만들어 쓰자