ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 객체의 불변성(Immutability)
    Web/자바스크립트 2021. 3. 21. 16:37
    반응형

    객체의 불변성은 객체가 생성된 이후 해당 객체의 상태를 변경할 수 없는 성질을 의미한다.

    이런 성질을 적용해서 코딩을 하면, 원본 객체의 데이터가 변경되거나 훼손되는 것을 방지할 수 있다.


    자바스크립트는 객체를 참조 형태로 전달하고 전달 받는다.

    그래서 객체를 참조 받는 곳에서 해당 객체의 데이터를 변경하면 원본 객체의 데이터도 변경된다.

    결국에는 동일한 하나의 객체를 이곳저곳에서 참조하고 있는 것이기 때문.

     

    자바스크립트에서는 주로 어떤 식으로 객체의 불변성을 적용하는가 하면, 

    객체의 일부를 변경하는 대신 완전히 새로운 객체를 갈아 끼우는 식으로 적용을 한다.

    '객체 일부만 변경하나, 전체 객체를 갈아끼우나 어차피 데이터는 똑같이 변경되는 거 아닌가요?'

    라고 생각할 수도 있겠지만, 객체의 일부를 변경해도 객체의 변경을 인식할 수 없다는 점이 중요하다.

     

    객체를 가지고 있는 변수는 오로지 해당 객체 자체의 메모리 주소에 대한 참조를 가지고 있다.

    근데, 해당 객체 내부에 존재하는 프로퍼티(속성)들의 주소에 대한 참조는 가지고 있지 않기 때문

    객체 내부의 데이터가 일부 변경되어도 그것을 전혀 인지하지 못하는 것.

    (내부 속성이 변경 되어도 객체 자체의 메모리 주소는 동일해서,

    자바스크립트는 변경된 이후의 객체를 이전과 동일한 객체라고 인식한다.

    객체를 const로 선언해도 내부 속성을 변경할 수 있는 이유.)

     

    즉, 객체의 참조를 가지고 있는 어떤 장소에서 해당 객체를 변경하면 

    해당 참조를 공유하는 모든 곳에서 그 영향을 받는데,

    막상 영향을 받는 곳들에서는 그러한 사실을 전혀 인지하지 못하기 때문에

    예상치 못한 버그(의도치 않은 객체의 변경)가 발생할 수 있다는 것.

    function convertToNewCompany(company, newName, newCEO) {
      company.name = newName;
      company.CEO = newCEO;
      return company;
    }
    // 이 함수는 인자로 받은 객체의 속성까지 변경해버리는 함수이다.
    // 하지만, 이 함수의 사용자가 내부 동작을 자세히 보지 않고
    // '회사명과 CEO를 교체한 새로운 company 객체를 반환해주는 함수'라고 생각했다면..
    
    const company_1 = { name: 'Apple', CEO: 'Tim Cook' }
    const company_2 = convertToNewCompany(company_1, 'Tesla', 'Elon Musk');
    
    console.log(company_1); // { name: 'Tesla', CEO: 'Elon Musk' } 
    console.log(company_2); // { name: 'Tesla', CEO: 'Elon Musk' }
    console.log(company_1 === company_2); // true
    // company_1도 변경되어버렸는데, 사용자는 이 사실을 눈치채지 못할 가능성이 높다.

    (아무도 이따위로 함수를 짜지 않고, 이따위로 생각하지도 않겠지만 그냥 예시로 봐주세염 ㅎ)


    전역 변수를 마구잡이로 사용할 때에도 비슷한 골칫거리가 발생한다.

    사실, 객체의 불변성을 도입하는 이유는 전역변수의 사용을 지양하는 이유와 거의 비슷하다.

     

    예를 들어, 나는 특정 전역 변수의 내용이 'your name is peter, right?' 인줄 알고 출력했는데

    어딘가에서 어떤 변경이 일어나 'shut the fuck up peter, right?' 이 출력되어도, 

    로직이 복잡해지고 코드가 길어지면 어디서 그런 일이 발생했는지 거의 추적이 불가능하게 된다.

    결국에는 내가 어딘가에서 짠 코드가 정상적으로 동작해서 저런 문장이 출력된 것이므로

    에러가 발생하지도 않는다. 매우 골치가 아픈 상황.


    그래서 객체를 아예 불변 객체로 만들어서 변경을 방지하며,

    변경이 필요한 경우에는 방어적 복사(defensive copy)를 통해 새로운 객체를 생성한 후

    기존에 있던 객체에 대한 참조를 새로운 객체에 대한 참조로 완전히 갈아끼워주는 방식을 사용한다.

    (기존 객체를 참조하던 다른 부분들은 전혀 영향을 받지 않음. 그냥 계속 기존 객체를 참조한다)


    먼저, 특정 객체를 불변 객체로 만들려면 'Object.freeze'라는 함수를 사용하면 된다.

    말 그대로 객체를 그 모양 그대로 객체를 얼려버리는 것.

    다만, Object.freeze를 사용하더라도 해당 객체 내부에 있는 객체(nested object)까지 불변하게 만들어주진 않는다.

    즉, nested object까지 얼려주지는 않으니(shallow freeze), 그 부분은 주의해서 사용하라는 뜻이다.

    const company = {
      name: 'Apple',
      leadership: {
        ceo: 'Steve Jobs',
      }
    };
    
    Object.freeze(company);
    
    company.name = 'Tesla'; // 적용되지 않음(무시됨)
    
    console.log(company); // { name: 'Apple', leadership: { ceo: 'Steve Jobs' }}
    
    console.log(Object.isFrozen(company)); // true
    
    company.leadership.ceo = 'Tim Cook'; // 적용됨
    
    console.log(company); // { name: 'Apple', leadership: { ceo: 'Tim Cook' }}

     

    방어적 복사를 위한 첫 번째 방법은 'Object.assign'을 사용하는 것.

    Object.assign(target, source)꼴로 사용을 하는데,

    source 객체들에 있는 데이터들을 target 객체로 복사한 새로운 객체를 반환해준다.

    이 때, source는 여러 개가 될 수 있어서 Object.assign(target, source1, source2, ... )꼴로 사용할 수 있다.

    참고로 Object.assign 역시 deep copy가 아닌 shallow copy이므로

    객체 내부의 객체(nested object)는 참조값이 그대로 복사된다.

    const company = {
      name: 'Apple',
      leadership: {
        ceo: 'Steve Jobs',
      }
    };
    const copied = Object.assign({}, company);
    
    console.log(copied); // { name: 'Apple', leadership: { ceo: 'Steve Jobs' }}
    console.log(company === copied); // false
    
    console.log(company.leadership === copied.leadership); // true;

    * 아래 참조의 Poiemaweb에 들어가면 deep copy 방법이 나와있으니 참고해보면 좋다.

     

    방어적 복사를 위한 두 번째 방법은 Spread operator를 사용하는 것.

    Spread operator 역시 새로운 객체를 반환해주기 때문에

    객체의 불변성을 적용할 때 종종 사용되는 방법이다.

    (참고로 객체에 spread operator를 사용할 경우, 이미 존재하는 프로퍼티는 값이 override 된다)

    얘도 역시나 shallow copy.

    const stock = { company: 'Apple', ceo: 'Steve Jobs' };
    const copied_1 = { ...stock };
    
    console.log(copied_1); // { company: 'Apple', ceo: 'Steve Jobs' };
    console.log(stock === copied_1); // false
    
    const copied_2 = { ...stock, ceo: 'Tim Cook' };
    console.log(copied_2); // { company: 'Apple', ceo: 'Tim Cook' };

     

    하지만 Object.assign과 Object.freeze는 약간의 성능 이슈가 있어서 큰 객체에는 사용하지 않는 것이 좋다고 한다.

    그렇기 때문에 많이들 사용하는 대안이 바로 facebook에서 개발한 'Immutable.js' 라이브러리를 사용하는 것.

    Immutable.js는 List, Stack, Map, OrderedMap, Set, OrderedSet, Record 등의 영구 불변 자료구조를 제공해준다.


    추가로. 리액트에서도 불변성을 반드시 지켜야 하는데, 

    그 이유는 리액트가, 데이터가 변경되기 전과 변경된 후를 비교해서

    변경된 부분만 다시 렌더링해주는 방식을 사용하기 때문.

     

    만약, mutable하게 객체의 일부분만 변경한다면

    해당 객체의 메모리 주소 자체는 변하지 않기 때문에

    객체 자체의 참조를 봐서는 변경 여부를 전혀 알 수가 없다.

    그래서 객체 내부를 샅샅이 뒤져서 어떤 데이터가 변경되었는지 찾아내야한다.

     

    반면 immutable한 방식으로, 새로운 데이터 자체를 끼워넣으면 

    최상위 객체의 참조만 비교하면 되기 때문에 훨씬 빠른 비교가 가능하다.


    글을 쓰고 보니 마치 불변성이 훨씬 좋은 개념인 것처럼 글이 쓰여졌는데,

    사실 객체 내부에 변경되어야 할 데이터가 많은 경우에는 불변성이 오히려 성능을 저하시키기도 한다.

    그래서 불변성을 왜 사용하는지 정확히 이해하고,

    상황에 맞게 immutable과 mutable을 적절히 사용할 수 있는 센스가 필요하겠다.

     

    + 첫 번째 참조글(evans library)은 꼭 읽어보는 것을 추천. 이해가 엄청 잘된다.

    + 관련된 패턴으로는 옵저버 패턴이 있으니, 같이 공부해보면 좋을 듯.

     

     

    참조

     

    변하지 않는 상태를 유지하는 방법, 불변성(Immutable)

    이번 포스팅에서는 순수 함수에 이어 함수형 프로그래밍에서 중요하게 여기는 개념인 에 대한 이야기를 해보려고 한다. 사실 순수 함수를 설명하다보면 불변성에 대한 이야기가 꼭 한번은 나오

    evan-moon.github.io

     

    Immutability | PoiemaWeb

    함수형 프로그래밍의 핵심 원리이다. 객체는 참조(reference) 형태로 전달하고 전달 받는다. 객체가 참조를 통해 공유되어 있다면 그 상태가 언제든지 변경될 수 있기 때문에 문제가 될 가능성도

    poiemaweb.com

     

    반응형

    댓글

Designed by Tistory.