Ch06 변경 가능한 데이터 구조를 가진 언어에서 불변성 유지하기

·

3 min read

모든 동작을 불변형으로 만들 수 있을까?

중첩된 데이터에서 불변 동작을 구현할 수 있을까?

동작을 읽기, 쓰기 또는 둘 다로 분류하기

읽기

  • 데이터를 바꾸지 않고 정보를 꺼내는 것

쓰기

  • 어떻게든 데이터를 바꿈

  • 바뀌는 값이 어디서 사용될지 모르기 때문에 바뀌지 않도록 원칙이 필요 👉 불변성 원칙

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에서는 직접 구현해야 하므로 유틸 함수로 만들어 쓰자