쏙쏙 들어오는 함수형 코딩 - 6

binary·2022년 5월 28일
9
post-thumbnail

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

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

  • 장바구니 동작

    1. 제품 개수 가져오기 👉 읽기
    2. 제품 이름으로 제품 가져오기 👉 읽기
    3. 제품 추가하기 👉 쓰기
    4. 제품 이름으로 제품 빼기 👉 쓰기
    5. 제품 이름으로 제품 구매 수량 바꾸기 👉 쓰기
  • 제품에 대한 동작

    1. 가격 설정하기 👉 쓰기
    2. 가격 가져오기 👉 읽기
    3. 이름 가져오기 👉 읽기

읽기 동작은 데이터를 바꾸지 않고 정보를 꺼내는 것이다.
쓰기 동작은 어떻게든 데이터를 바꾼다. 바뀌는 값은 어디서 사용될지 모르기 때문에 바뀌지 않도록 원칙이 필요하다.

쓰기 동작은 불변성 원칙에 따라 구현해야 한다. 여기서 불변성 원칙은 카피-온-라이트(copy-on-write) 라고 한다.
JavaScript에서는 기본적으로 변경 가능한 데이터 구조를 사용하기 때문에 불변성 원칙을 적용하려면 직접 구현해야 한다.

🌟 카피-온-라이트 원칙 세 단계

  1. 복사본 만들기

  2. 복사본 변경하기 (원하는 만큼)

  3. 복사본 리턴하기

전역변수를 변경하는 동작을 모두 카피-온-라이트로 바꾸면 데이터 더 이상 변경되지 않고 불변 데이터로 동작하게 될 것이다.

function add_elment_last(array, elem) {
  var new_array = array.slice();
  new_array.push(elem);
  return new_array;
}

이 함수는 카피-온-라이트로 구현되었다.

  1. 배열을 복사했고 기존 배열을 변경하지 않았다.

  2. 복사본은 함수 범위에 있기 때문에 다른 코드에서 값을 바꾸기 위해 접근할 수 없다.

  3. 복사본을 변경하고 나서 함수를 리턴한다.

데이터를 바꾸지 않고 정보를 리턴했기 때문에 읽기이다. 카피-온-라이트로 동작을 구현하면 쓰기를 읽기로 바꿀 수 있다.

카피-온-라이트로 쓰기를 읽기로 바꾸기

제품 이름으로 장바구니에서 제품을 빼는 함수가 있다.

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() 메소드를 통해 장바구니를 변경한다.

🤔 만약 장바구니를 변경 불가능한 데이터로 쓰고 싶다면 어떻게 해야 할까?

1. 복사본 만들기

function remove_item_by_name(cart, name) {
  var new_cart = cart.slice(); // 복사한 데이터를 지역변수에 할당
  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); // 장바구니 변경
}

2. 복사본 변경하기

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); // 복사한 데이터의 장바구니 변경
}

3. 복사본 리턴하기

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(shopping_cart, name);

원래라면 위처럼 함수를 사용했을 것이다. 그러나 이 함수안에서 전역변수를 변경하고 있다는 것을 위 코드만 보고는 알 수가 없다.

카피-온-라이트를 적용하여 작성한 코드는 원본 데이터는 변경하지 않고, 원본 데이터를 복사한 데이터를 변경하고 있다.

shopping_cart = remove_item_by_name(shopping_cart, name);

그래서 위처럼 복사한 데이터를 반환하는 함수의 값을 전역변수에 할당해주어야 한다. 그리고 이 함수를 보면 전역변수를 바꾸고 있다는 것을 한 눈에 알 수 있다.

카피-온-라이트 일반화

function removeitems(array, idx, count) {
  var copy = array.slice(); 
  copy.splice(idx, count);
  return copy;
}

위처럼 .splice() 하는 동작을 일반화할 수 있다.

카피-온-라이트 일반화한 함수 사용하기

| 일반화하기 전

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; // 복사본 반환
}

| 일반화한 후

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)
    return removeitems(cart, idx, 1);
  return cart; 
}

재사용할 수 있도록 일반화하면 귀찮은 일이 많이 줄어든다.

데이터를 읽고 쓰는 함수를 작성할 때 데이터를 불변하게 유지하기 위해 .slice()... 어쩌구저쩌구 작성하지 않고 일반화 함수로 대체할 수 있다! 또 중복되는 코드 사용도 피할 수 있다!

🤷‍♀️ 쓰기를 하면서 읽기도 하는 동작은 어떻게 할까?

.shift() 메소드같이 어떤 동작은 읽고 변경하는 일을 동시에 한다. 이런 동작을 카피-온-라이트로 어떻게 바꿀 수 있을까?

  1. 읽기와 쓰기 함수로 각각 분리한다.

  2. 함수에서 값을 두 개 리턴한다.

1. 읽기와 쓰기 함수로 각각 분리한다.

.shift() 가 반환하는 값은 배열의 첫 번째 요소이다.
따라서 배열의 첫 번째 요소를 반환하는 계산함수를 만든다.

function first_element(array){
  return array[0];
}

first_element() 함수는 배열을 바꾸지 않는 읽기 함수이기 때문에 카피-온-라이트를 적용할 필요가 없다.

function drop_first(array){
  var array_copy = array.slice();
  array_copy.shift();
  return array_copy;
}

2. 함수에서 값을 두 개 리턴한다.

function shift(array) {
  var array_copy = array.slice();
  var first = array_copy.shift();
  return {
    first : first,
    array : array_copy
  }
}

| 다른 방법

위에서 나눈 함수를 합쳐 값을 두 개 리턴하도록 만들 수도 있다.

function shift(array) {
  return {
    first : first_element(array),
    array : drop_first(array)
  }
}

🤷‍♀️ 불변 데이터 쓰면 느린 거 아닌가여?

불변 데이터 구조는 변경 가능한 데이터 구조보다 메모리를 더 많이 쓰고 느리다. 그러나 불변 데이터 구조를 사용하면서 대용량의 고성능 시스템을 구현하는 사례도 많다. 이런 사례가 많다는 건 불변 데이터도 일반 앱이 쓰기 충분히 빠르다는 증거이다.

- 불변 데이터 쓰면 좋은 점

  1. 언제든 최적화 가능

  2. 가비지 콜렉터는 매우 빠름

  3. 생각보다 많이 복사하지 않음

  4. 함수형 프로그래밍 언어에는 빠른 구현체가 있음

🌟 객체에 대한 카피-온-라이트

지금까지는 데이터가 배열이라는 가정 아래 .slice() 로 데이터를 복사했다.

객체 데이터에 카피-온-라이트를 적용하는 방법도 배열과 같다.

  1. 복사본 만들기

  2. 복사본 변경하기

  3. 복사본 리턴하기

객체를 복사하려면 Object.assign({}, object) 를 사용하면 된다.

이를 이용하여 제품 가격을 설정하는 함수를 작성하면 아래와 같다.

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

❗️4줄 요약

  1. 함수형 프로그래밍에서는 불변 데이터가 필요하다.

  2. 카피-온-라이트는 데이터를 불변형으로 유지할 수 있는 원칙이다. (복사본을 만들고 원본 대신 복사본 변경)

  3. 카피-온-라이트는 값을 변경하기 전에 얕은 복사를 하고 반환한다. (통제할 수 있는 범위에서 불변성을 구현가능)

  4. 보일러 플레이트 코드를 줄이기 위해 기본적인 배열과 객체 동작에 대한 카피-온-라이트 버전을 만들어 두는 것이 좋다.


혹시나 잘못된 정보가 있다면 댓글로 알려주세요 ! 저의 성장의 큰 도움이 될 것 같습니다.🌱

4개의 댓글

comment-user-thumbnail
2022년 6월 3일

후아~~ 여기까지도 책 양이 제법 되었을텐데 정말 핵심만 잘 뽑아서 정리를 해주셨네요 +_+bb 좋은 글 좋은 자료 감사합니다 :)

1개의 답글
comment-user-thumbnail
2022년 6월 5일

👍👍👍

1개의 답글