[JavaScript]원시 값과 객체의 비교 - 데이터 불변성
[ 원시 타입 primitive type & 객체 타입 reference type ]
자바스크립트가 제공하는 7가지 데이터 타입은 Number, String, Boolean, null, undefined, Symbol, 객체 타입(Object, Array, function)이 있습니다.
이 데이터 타입은 크게 원시 타입(primitive type)과 객체 타입(reference type)으로 구분할 수 있습니다.
원시 타입 | 객체 타입 |
원시 값은 변경 불가능한 값(immutable value) | 객체는 변경 가능한 값(mutable value) |
값에 의한 전달(pass by value) | 참조에 의한 전달(pass by reference) |
원시 값을 변수에 할당하면 변수에는 실제 값이 저장됨 | 객체를 변수에 할당하면 변수에는 참조 값이 저장됨 |
[ 원시 값 - 데이터 불변성 Immutability ]
원시 타입의 값, 즉 원시 값은 변경 불가능한 값(immutable value)입니다.
이 말은 한번 생성된 원시 값은 메모리 공간에 저장되어 읽기 전용값으로서 변경할 수 없다는 의미입니다.
변경 불가능하다는 말이 무슨 뜻일까요? 분명 var 또는 const로 선언한 변수는 값을 변경할 수 있지 않나요?
변수는 하나의 값을 저장하기 위해 확보한 메모리 공간 자체 또는 그 공간을 식별하기 위해 붙인 이름이고, 값은 변수에 저장된 데이터를 의미합니다.
변경 불가능하다는 것은 변수가 아닌 값에 대한 진술입니다. 즉, 변수 값을 변경할 수 없다는 것이 아니라 원시 값 자체를 변경할 수 없다는 의미입니다.
let score; // 변수 선언
console.log(score); // undefined
score = 1; // 값의 할당
console.log(score); // 1
score = 2; // 값의 재할당
console.log(score); // 2
위와 같은 코드를 보시면 변수 score에 변경 불가능한 값인 원시 값이 할당되어 있는 것을 확인할 수 있습니다.
이때 변수에 새로운 원시 값을 재할당하면 메모리 공간에 저장되어 있는 재할당 이전의 원시 값을 변경하는 것이 아닌, 새로운 메모리 공간을 확보하고 재할당한 원시 값을 저장한 후, 변수는 새롭게 재할당한 원시 값을 가리키게 됩니다.
즉, 값의 재할당이 일어나면 변수가 참조하던 메모리 공간의 주소가 바뀌는 것입니다.
주소 | 메모리 | |
0x00000000 | ||
... | ... | |
score (변수 선언시) |
0x000000F2 | undefined |
... | ... | |
score (값의 할당시) |
0x00001332 | 1 |
... | ... | |
score (값의 재할당시) |
0x0669F913 | 2 |
... | ... | |
0xFFFFFFFF |
★ 문자열과 불변성
문자열 역시 원시 값이지만 다른 원시 값과는 다른 독특한 특징을 가지고 있습니다.
예를 들어 숫자 타입은 기본적으로 8바이트로 1이든지 1000이든지 그 크기를 명확히 규정하고 있지만, 문자열은 1개의 문자가 2바이트의 메모리 공간에 저장되어 몇 개의 문자로 이뤄졌느냐에 따라 필요한 메모리 공간의 크기가 결정됩니다.
또한 문자열은 유사 배열 객체로 인덱스로 하나의 문자에 접근할 수도 있으며 length 프로퍼티를 갖고 있습니다. 때문에 유사 배열 객체가 어떻게 원시 값이 되느냐? 할 수도 있지만, 문자열은 다음 두 가지 특징을 갖습니다.
- 문자열을 가리키는 변수에 새로운 문자열을 재할당하면 기존 문자열은 메모리에 그대로 남아있고 변수는 새로운 메모리 공간에 저장된 새로운 문자열을 가리킴
- 유사 배열 객체이므로 인덱스로 각 문자를 접근할 순 있지만, 일부 문자를 재할당하여도 문자열에는 반영되지 않음
let str = 'Hello';
console.log(str.length, str); // 5 Hello
str = 'World!';
console.log(str.length, str); // 6 World!
str[0] = 'A';
console.log(str.length, str); // 6 World!
★ 원시 값 복사
변수에 원시 값을 갖는 변수를 할당하면 할당받는 변수에는 할당되는 변수의 메모리 주소가 복사되어 전달됩니다.
이는 변수가 값이 아니라 메모리 주소를 기억하고 있기 때문입니다. 즉, 식별자(변수)는 메모리 주소에 붙인 이름이라고 할 수 있습니다.
let a = 1;
let b = 2;
console.log(a, b, a === b); // 1 2 false
b = a;
console.log(a, b, a === b); // 1 1 true
let c = 1;
console.log(a, c, a === c); // 1 1 true
a = 3;
console.log(a, b, a === b) // 3 1 false
위 예제와 같이 변수 a와 b 에 각각 1, 2가 할당되면 특정 메모리 공간에 원시 값 1, 2가 저장됩니다.
주소1 | 주소2 | 주소3 | 주소4 | 주소5 |
1 | 2 | |||
a | b |
변수 b에 변수 a를 할당하면 a가 기억하고 있는 메모리 주소를 복사하게 되어 같은 주소를 바라보게 됩니다.
주소1 | 주소2 | 주소3 | 주소4 | 주소5 |
1 | 2 | |||
a, b |
이때 변수 c를 새롭게 생성하여 1이라는 값을 할당하게 되면, 메모리 공간에 원시 값 1이 있는지 확인하고 그 값이 있다면 그 메모리 주소를 변수 c에 저장하게 됩니다.
주소1 | 주소2 | 주소3 | 주소4 | 주소5 |
1 | 2 | |||
a, b, c |
그렇다면 같은 공간을 가리키는 변수가 세개나 되네요? 이때 이 중 하나라도 값의 재할당이 이뤄지면 나머지 변수들의 값도 재할당이 될까요?
정답은 No 입니다. 변수 a 에 3으로 값의 재할당이 이뤄지면 메모리의 새로운 공간에 원시 값 3을 저장하여 a는 그 메모리 공간을 가리키게 됩니다.
주소1 | 주소2 | 주소3 | 주소4 | 주소5 |
1 | 2 | 3 | ||
b, c | a |
결국은 특정 메모리 공간에 있는 원시 값은 변경이 불가능하므로 같은 원시 값을 바라보고 있는 변수가 여러개이고 그 중 하나의 변수에 값의 재할당이 이루어져도 서로 간섭할 수 없다는 것입니다.
[ 객체 - 변경 가능한 값 ]
객체는 원시값과 달리 확보해야 할 메모리 공간의 크기를 사전에 정해둘 수 없습니다.
원시 값은 상대적으로 적은 메모리를 소비하지만, 객체는 그 경우에 따라 크기가 매우 클 수도 있고, 객체를 생성하고 프로퍼티에 접근하는 것도 비용이 많으 드는 일입니다.
만약 객체도 원시 값처럼 메모리에 객체 자체를 저장해두고 변경 불가능한 값으로 기억하면 메모리를 굉장히 많이 소비하겠죠?
따라서 객체는 변경 가능한 값(mutable value)으로 객체를 할당한 변수가 기억하는 메모리 주소를 통해 메모리 공간에 접근하면 그 참조 값(reference value)에 접근할 수 있습니다.
이때 참조 값은 생성된 객체가 저장되어 있는 메모리 공간의 주소입니다.
const a = {
name: 'Lee',
};
위와 같은 객체를 생성하면 메모리는 다음과 같은 구조를 갖습니다.
주소 | 메모리 | |
a | 0x000000F2 | 0x00001332 |
... | ||
0x00001332 | { name: 'Lee' } |
|
★ 객체 복사
객체는 생성할 때마다 새로운 메모리에 할당되고 객체의 내용이 같더라도 서로 다른 주소를 가리키게 되어 완전히 다른 데이터라고 할 수 있습니다.
이때 같은 프로퍼티를 갖는 서로 다른 두 객체의 프로퍼티가 원시 값이라면 두 원시 값은 동일한 메모리 공간에 저장되어 있어 같은 데이터라고 할 수 있습니다.
변수에 객체를 가리키는 변수를 할당하면 어떻게 될까요?
이때 여러 개의 변수(식별자)는 하나의 객체를 공유할 수 있습니다. 두 변수는 서로 다른 메모리 공간에 존재하지만 동일한 객체를 가리키는 참조 값이 복사되어 원본 또는 사본 중 어느 한쪽에서 객체를 변경하면 서로 영향을 주고받게 됩니다.
let a = {
name: 'Lee',
}
let b = {
name: 'Lee',
}
// 두 객체는 내용이 같지만 다른 참조값을 가짐
console.log(a, b, a === b) // {name: 'Lee'} {name: 'Lee'} false
// 하지만, 그 프로퍼티는 원시 값으로 동일한 메모리 공간에 존재
console.log(a.name, b.name, a.name === b.name) // Lee Lee true
// b는 a의 참조값을 복사함
b = a;
console.log(a, b, a === b) // {name: 'Kim'} {name: 'Kim'} true
// a와 b는 같은 객체를 공유하므로 어느 한쪽에서 객체를 변경하면 서로 영향을 주고받음
a.name = 'Kim';
console.log(a, b, a === b) // {name: 'Kim'} {name: 'Kim'} true