[JavaScript] 객체와 불변성

YES31·2022년 11월 18일
0

javascript

목록 보기
2/4

1. 기본형 데이터와 참조형 데이터

🏹기본형(Primitive type) : 값을 그대로 할당

  • Number
  • String(문자열)
  • Boolean
  • Null
  • undefinded
  • Symbol(ES6 부터 추가, 객체 속성을 만드는 데이터 타입)

기본형 데이터는 값을 그대로 할당하는 것.
메모리 내에 고정된 크기로 저장되면서, 원시 데이터 값 자체를 보관, 불변적
기본적으로 데이터는 하나의 메모리로 사용한다. (재사용)

예를 들어보자.
변수명(a,b,c)를 만들면 변수를 저장할 비어있는 데이터 영역을 확보한다.

--변수 a는 313번 저장공간에, b는 314번 공간에 c는 315 공간에 해당 주소를 확보하고 각
해당 주소를 변수명과 맵핑시킨다.
--기존 변수명을 새롭게 할당하려고 하는 경우, 새로운 변수는 별도의 공간을 확보하고,
불리언 값을 통해 기존 변수에 대입된다.(기본 값은 직접적 비교 가능)


여기다가 b와 c가 같다고 표현을 해보았다.
false의 명제는 세상에 하나 뿐이므로 b===c가 된다.
.
.
다시 C에 20을 대입하였다.👇👇
그렇게되면 c는 20이란 데이터값으로 대체 되므로 b !== c 이다.


.
.
🏹Object

  • Array(배열): const 로 선언된 변수 배열에 Array.push를 적용할 수 있는 이유는
    배열은 참조 타입이기 떄문에 데티어의 주소를 대입할 수 있기 때문이다.
  • function(함수) / RegExp(정규표현식): 문자열에 나타나는 특정 문자조합과 대응시키기 위해 사용되는 패턴이다.
  • Map
  • Etc..

참조형은 기본 데이터의 조합
참조형 데이터는 값이 지정된 주소의 값을 할당한다.

저장되는 과정

  1. 미리 공간을 확보하고 변수 Obj와 매칭시키는 것은 기본형과 같음
  2. 할당과정에서 참조형 데이터인 것을 인식하고 프로퍼티/데이터(key/value)인 것을 확인
  3. 프로퍼티 명은 변수와 비슷한 성질을 가짐
  4. 각 프로퍼티와 밸류를 담을 공간(여기서는 1011번이라는 방)을 확보함
  5. 1012번방에 a 프로퍼티의 밸류를 확보할 공간을 확보, 1013번 방의 프로퍼티 밸류를 확보함.
  6. 각각의 주소값마다 기본형 데이터 값을 넣음(1012번에 1 1013번에 b를 넣음) 이상태 에서 obj2를 선언한다.
  7. obj2는 414번 방에 할당한다.
  8. 414번 방에서 1011번 방으로 이동 후, 1011번방에서의 a값을 찾은 뒤 1012번 방으로 이동
    1 대신 10을 넣음.

결론

  • obj === obh2

.
.
.

2. JavaScript 객체와 불변성이란?

Immutability(변경불가성)는 객체가 생성된 이후 그 상태를 변경할 수 없는 디자인 패턴을 의미한다. Immutability은 함수형 프로그래밍의 핵심 원리이다.

불변 객체를 사용하면 복제나 비교를 위한 조작을 단순화 할 수 있고 성능 개선에도 도움이 된다.
하지만 객체가 변경 가능한 데이터를 많이 가지고 있는 경우 오히려 부적절한 경우가 있다.

ES6에서는 불변 데이터 패턴(immutable data pattern)을 쉽게 구현할 수 있는 새로운 기능이 추가되었다.

.
.

불변한 값 VS 변경 가능한 값

아래 Javascript의 원시 타입(primitive data type)은 변경 불가능한 값(immutable value)이다.

  • Boolean
  • null
  • undefined
  • Number
  • String
  • Symbol (New in ECMAScript 6)

원시 타입 이외의 모든 값은 객체(Object) 타입이며 객체 타입 은 변경 가능한 값(mutable value)이다.
즉, 객체는 새로운 값을 다시 만들 필요없이 직접 변경이 가능하다는 것이다.

💡 객체는 변할 수 있는 값이고 원시 타입은 불변한 값이다.

예를 들어 살펴보자. C 언어와는 다르게 Javascript의 문자열은 변경 불가능한 값(immutable value) 이다. 이런 값을 “primitive values” 라 한다. (변경이 불가능하다는 뜻은 메모리 영역에서의 변경이 불가능하다는 뜻이다. 재할당은 가능하다)

let str = 'Hello'
str = 'world'

위의 예시를 보면 쉽게 이해할 수 있다.
첫번째 줄이 실행되면 메모리에 문자열 'Hello'가 생성되고, 변수 str은 'Hello'의 메모리 주소를 가리킨다. 그리고 두번째 줄이 실행되면 이전에 생성되었던 문자열 'Hello'를 수정하는 것이 아니라 새로운 문자열 ‘world’를 메모리에 생성하고 식별자 str은 이것을 가리킨다. 이때 문자열 ‘Hello’와 ‘world’는 모두 메모리에 존재하고 있다. 변수 str은 문자열 ‘Hello’를 가리키고 있다가 문자열 ‘world’를 가리키도록 변경되었을 뿐이다. *(원시 타입은 불변한 값이기 때문)

.
.
.

3. 불변 객체를 만드는 방법

먼저 불변(immutability)이란 뭘까? 단어에서 유추해볼 수 있다시피 '변하지 않는' 뜻이라고 생각하면 되겠다. 그럼 '불변 객체'란? '변하지 않는 객체' 즉 이미 할당된 객체가 변하지 않는다는 뜻을 가지고 있다.

자바스크립트에서 불변 객체를 만들 수 있는 방법은 기본적으로 2가지 인데 const와 Object.freeze()를 사용하는 것이다.


❓ Const
자바스크립트는 ES6(ECMA 2015)부터 let과 const 키워드를 제공한다. 이 중 const 키워드를 사용하면 상수 변수 선언을 할 수 있다. 그리고 const는 상수로 취급되기 때문에 다음과 같이 값을 변경하려고 시도하면 에러가 발생한다.

const hello = 'JS World';

hello = 'JS Hell'; // Uncaught TypeError: Assignment to constant variable.

하지만 자바스크립트의 const는 다른 여러 언어의 상수 취급과는 완전히 동일하지 않다.
그 이유는 정확히 얘기하면 ES6의 const는 할당된 값이 상수가 되는 것이 아니고, 바인딩된 값이 상수가 되기 때문이다.

const b = {};
b.key = 'value';

console.log(b); // {'key': 'value'}

위 코드와 같이 const로 선언된 객체의 속성 변경이 가능한 이유는 실제 객체가 변경되는 것은 맞지만 const로 선언한 변수와 객체 사이의 바인딩은 변경되지 않기 때문이다. 따라서 가장 먼저 봤던 코드와 같이 변수 자체를 재할당하려는 경우에는 변수와 값 사이의 바인딩이 변경되어 오류가 발생하는 것이다.

프로그래밍에서 상수는 코드 내에서 개발자의 실수로 인해 값이 변경되지 않도록 변수를 보호하거나 다른 코드에서 실수로 이미 할당된 변수를 재할당하지 않도록 하는데 유용하기 때문에 많이 사용된다.

하지만 자바스크립트의 const로 객체를 선언할 경우에 객체의 속성은 언제든지 변경이 가능하기 때문에 immutable한 상수로 사용된다고 보기 어렵다.

그래서 const와 같이 유용하게 사용되는 것이 Object.freeze()이다.
.
.

❓ Object.freeze()
바스크립트에서 제공하는 Object.freeze()는 MDN 문서에서 "객체를 동결하기 위한 메서드"라고 설명하고 있다.

즉, Object.freeze를 사용하면 동결된 객체를 만들 수 있고, 동결된 객체에는 속성을 추가하거나 제거하는 동작이 불가능한 Immutable한 객체를 만들 수 있는 것이다. 또한 Object.freeze로 동결된 객체는 프로토타입의 변경도 막아준다.

let itGo = {
	elsa = 'Princess';
};

Object.freeze(itGo);

먼저 let으로 선언된 객체를 Object.freeze를 통해 동결된 객체로 만들었다.
따라서 아래와 같이 해당 객체의 속성을 변경하는 시도는 무시된다.
itGo.elsa = 'Prince';
console.log(itGo) // {elsa: "Princess"} -> Not Modified
 

하지만 Object.freeze는 동결된 객체를 반환할 뿐 재할당을 허용한다.
따라서 let으로 선언된 객체는 Object.freeze를 사용하더라도 다음과 같이 재할당 할 수 있다.

itGo = {
	'Olaf': 'Snowman'
};

console.log(itGo); // {'Olaf': 'Snowman'}

❗ const + Object.freeze
그렇다!
우리가 원하는 Immutable한 객체를 생성하기 위해서는 const와 Object.freeze를 함께 사용하면 된다.

const truth = {
	'dogIs': 'Cute'
};

Object.freeze(truth);

위 코드는 "강아지는 귀엽다" 라는 절대 불변의 진리(?)를 truth라는 객체의 속성으로 등록했다.
그리고 해당 truth 객체를 Object.freeze를 통해 동결된 객체로 만들었다.
 
===============================================================================
해당 객체는 아래와 같이 재할당과 속성 변경이 불가능한 Immutable한 객체가 되었다.
                              
truth.dogIs = 'Not Cute';
truth = {'catIs': 'Pretty'}; // Uncaught TypeError: Assignment to constant variable.
concole.log(truth); // {'dogIs': 'Cute'} // Not Modified                              
                              

.
.
.

4. 얕은 복사와 깊은 복사

  • 깊은 복사(Deep Copy) : '실제 값'을 새로운 메모리 공간에 복사하는 것을 의미
  • 얇은 복사(Shallow Copy) : '주소 값'을 복사하는 것을 의미

🎯 얕은 복사(Shallow Copy)

public class CopyObject {

    private String name;
    private int age;
    
    public CopyObject() {
    }

    public CopyObject(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}



import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.*;

class CopyObjectTest {

    @Test
    void shallowCopy() {
        CopyObject original = new CopyObject("JuHyun", 20);
        CopyObject copy = original; // 얕은 복사

        copy.setName("JuBal");

        System.out.println(original.getName());
        System.out.println(copy.getName());
    }
}

위 코드에서는 copy 객체에 set메소드를 통해 이름을 변경했는데,
실제 결과는 original 객체와 copy 객체 모두 값이 변경이 되었습니다.

CopyObject copy = original 의 코드에서 객체의 얕은 복사를 통해 '주소 값'을 변경했기 때문에 참조하고 있는 실제 값은 동일하고, 복사한 객체가 변경된다면 기존의 객체도 변경이 되는 것입니다.

.
.
이해를 돕기 위한 메모리 구조 상태🎈

따라서 코드로는 copy 객체의 name만 변경했지만,
동일한 주소를 참조하고 있기 때문에 original의 객체에도 영향을 끼치게 됩니다.
.
.
.

🎯 깊은 복사(Deep Copy)

깊은 복사를 구현하는 방법은 여러가지가 있습니다.

  • Cloneable 인터페이스 구현
  • 복사 생성자
  • 복사 팩터리 등등....

Cloneable 인터페이스 구현

  • Cloneable 인터페이스는 위와 같이 빈 껍데기의 인터페이스지만
  • 주석을 살펴보면 Object 클래스의 clone() 메소드를 반드시 구현하라고 설명이 되어있습니다.
public class CopyObject implements Cloneable {

    private String name;
    private int age;

    public CopyObject() {
    }

    public CopyObject(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    protected CopyObject clone() throws CloneNotSupportedException {
        return (CopyObject) super.clone();
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}


    @Test
    void shallowCopy() throws CloneNotSupportedException {
        CopyObject original = new CopyObject("JuHyun", 20);
        CopyObject copy = original.clone();

        copy.setName("JuBal");

        System.out.println(original.getName());
        System.out.println(copy.getName());
    }

 // * 깊은 복사를 통해 테스트를 진행해보면 얕은 복사와는 달리 original 인스턴스의 값은 변경이 되지 않습니다.

.
.

이해를 돕기 위한 메모리 구조 상태🎈

profile
🏀 주니어 개발자

0개의 댓글