Skip to content

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

불변성을 지키면 데이터 변경으로 인한 예측 불가능한 동작을 피하고,
코드를 더 이해하기 쉽고 안전하게 만들 수 있습니다.

이를 위해 흔히 사용하는 기법이 카피-온-라이트(copy-on-write) 입니다.

스터디 회차: 5회차 (2025년 8월 21일)

카피-온-라이트 원칙 (copy-on-write)

  1. 복사본 만들기
  2. 복사본 변경하기
  3. 복사본 리턴하기

이 과정을 통해 원본은 그대로 두고, 새로운 데이터 구조를 만들어 불변성을 유지하면서 값을 바꿀 수 있습니다.

예제: 이름으로 아이템 제거하기

tsx
function remove_item_by_name(cart, name) {
  var idx = null;
  for (var i = 0; i < cart.length; i++) {
    if (cart[i].name === name) idx = i;
  }
  if (idx !== null) cart.splice(idx, 1);
}

문제점

  • splice는 원본 배열을 변경하기 때문에 불변성이 깨집니다.
  • 이 함수는 호출 시점에 따라 다른 결과가 나오므로 액션이 됩니다.

개선하기

tsx
function remove_item_by_name(cart, name) {
  var new_cart = cart.slice(); // 복사본 만들기
  var idx = null;
  for (var i = 0; i < new_cart.length; i++) {
    if (new_cart[i].name === name) idx = i;
  }
  if (idx !== null) new_cart.splice(idx, 1); // 복사본 변경하기
  return new_cart; // 복사본 리턴하기
}
  • remove_item_by_name에 카피-온-라이트 원칙을 적용합니다.
    1. cart를 복사하여 지역변수를 생성합니다.
    2. 복사본에 대해서 수정합니다.
    3. 수정된 복사본을 리턴합니다.

이렇게 하면 원본을 변경하지 않고, 불변성을 유지한 상태에서 데이터를 수정할 수 있습니다.

읽기도하고 쓰기도하는 함수 분리하기

예제: 자바스크립트의 shift() 메서드

tsx
var a = [1, 2, 3, 4];
var b = a.shift();
console.log(b); // 1
console.log(a); // [2,3,4] (원본이 변경됩니다.)

어떤 동작은 읽고 변경하는 일을 동시에 합니다. 이 경우 두가지 개선 방법이 있습니다.

1. 읽기와 쓰기 동작으로 분리하기

tsx
function first_element(array) {
  return array[0]; // 읽기 -> 계산
}

function drop_first(array) {
  var array_copy = array.slice();
  array_copy.shift(); // 쓰기 -> 카피온라이트
  return array_copy;
}
  • 읽기 동작은 단순히 리턴하는 동작으로, 아무것도 바꾸지 않고 숨겨진 입력이나 출력이 없기 때문에 계산입니다.
  • shift 메서드가 하는 일 그대로 감싸주어 카피온 라이트를 적용합니다.
  • 읽기와 쓰기를 분리하는 접근 방법은 분리된 함수를 따로 쓸 수 있기 때문에 더 좋은 접근 방법입니다.

2. 값을 두 개 리턴하는 함수로 만들기

tsx
function shift(array) {
  var array_copy = array.slice();
  var first = array_copy.shift();
  return {
    first: first,
    array: array_copy,
  };
}
  • 인자를 복사한 후에 첫번째 항목을 지우고, 변경된 배열과 함께 리턴합니다.
  • 기존 shift와 동일한 사용성(값과 배열을 함께 얻음)을 제공하면서도 불변성을 지킵니다.

불변 데이터 구조를 읽는건 계산입니다.

  • 변경 가능한 데이터를 읽는건 액션입니다.
  • 쓰기는 데이터를 바꾸기 때문에 데이터를 변경 가능한 구조로 만듭니다.

즉, 쓰기는 액션을 만듭니다.

  • 쓰기를 모두 없앴다면 데이터는 바뀌지 않습니다. 불변 데이터입니다.
  • 데이터를 불변형으로 만들었다면 그 데이터 읽기는 계산입니다.

따라서, 쓰기를 읽기로 만들수록 (데이터 구조를 불변형으로 만들수록) 더 많은 계산이 생기고 액션이 줄어듭니다.

중첩된 쓰기를 읽기로 바꾸기

예제: 장바구니에서 이름으로 가격 변경하기

tsx
function setPriceByName(cart, name, price) {
  for (var i = 0; i < cart.length; i++) {
    if (cart[i].name === name) cart[i].price = price;
  }
}

문제점

  • cart를 인자로 받지만, 주소값이 복사되므로 값을 변경시 원본이 변경됩니다.

개선하기

tsx
function setPrice(item, new_price) {
  var item_copy = Object.assign({}, item); // 객체 복사
  item_copy.price = new_price;
  return item_copy;
}

function setPriceByName(cart, name, price) {
  var cartCopy = cart.slice(); // 배열 복사
  for (var i = 0; i < cartCopy.length; i++) {
    if (cartCopy[i].name === name) {
        cartCopy[i] = setPrice(cartCopy[i], price);  // 복사된 배열에 변경된 객체로 적용
    }
  }
  return cartCopy;
}

shopping_cart = setPriceByName(shopping_cart, "t-shirt", 13); // 복사된 배열 적용
  • 가장 안쪽에 있는 setPrice에서 발생하는 쓰기 동작부터 바꾸는 것이 쉽습니다.
  • setPrice에 먼저 카피-온-라이트를 적용하고 setPriceByName에 적용합니다.

원본을 수정하지 않고 복사하기

카피-온-라이트에서 원본을 수정하지 않고 복사하는 방법은 다음과 같습니다.

tsx
// 배열 복사하기
const copyArr = arr.slice();

// 객체 복사하기
const copyObj = Object.assign({}, obj);

얕은 복사 (shallow copy) 와 구조적 공유 (structural sharing)

예제: 장바구니에서 이름으로 가격 변경하기

tsx
function setPriceByName(cart, name, price) {
  var cartCopy = cart.slice();
  for (var i = 0; i < cartCopy.length; i++) {
    if (cartCopy[i].name === name) {
      cartCopy[i] = setPrice(cartCopy[i], price);
    }
  }
  return cartCopy;
}

function setPrice(item, new_price) {
  var item_copy = Object.assign({}, item);
  item_copy.price = new_price;
  return item_copy;
}

카피-온-라이트가 적용된 코드에서 실제로 복사된 것은 다음과 같습니다.

  • 배열 하나 (cartCopy)
  • 조건에 맞는 객체 하나

나머지 객체들은 원본과 복사본이 같은 참조를 공유합니다. 이를 "구조적 공유"라고 합니다.

구조적 공유

구조적 공유

  • 복사된 배열도 원래 객체를 가리킵니다.
  • "얕은 복사"를 통해 객체 모두를 복사하지 않고 참조값만 복사합니다.

구조적 공유

  • 변경할 객체만 복사하여 변경합니다.
  • 변경된 객체는 복사된 배열에 적용합니다.
    • 이러한 과정중에서도 원본 객체와 배열은 변경되지 않았습니다.

따라서, 최소한의 복사와 메모리 활용으로 불변성을 유지하면서 값을 변경할 수 있습니다.

구조적 공유

구조적 공유란 두 개의 중첩된 데이터 구조가 어떤 참조를 공유하는 것을 의미합니다.

  • 데이터가 바뀌지 않는 불변 데이터 구조라면 구조적 공유는 안전합니다.
  • 구조적 공유는 메모리를 적게 사용하고 모든 것을 복사하는 것보다 빠릅니다.

정리하기

  • 카피-온-라이트를 적용하면 원본은 그대로 두고 복사본을 변경할 수 있습니다.
  • 읽기와 쓰기를 분리하거나, 두 값을 리턴하는 방식으로 동작을 명확히 할 수 있습니다.
  • 중첩된 구조에서는 안쪽부터 불변성 적용구조적 공유를 통해 효율적으로 불변성을 지킬 수 있습니다.