이번 포스팅에서는 불변성(Immutability)에 대해 알아보고자 한다.
< 목차 >
- Immutable type
- Mutable type
1. Immutable type
불변성에 대해 알아보기 위해서는 우선 자바스크립트의 변수 타입에 대해 살펴볼 필요가 있다. 자바스크립트의 변수는 크게 원시 타입(Primitive type)과 참조 타입(Reference type)으로 나눌 수 있으며, 원시 타입의 종류는 아래와 같다.
- Boolean
- String
- Number
- undefined
- null
- Symbol (ES6부터 추가)
이러한 원시 타입의 데이터들은 소위 불변성(Immutability)을 갖고 있다고 얘기한다. 실제 변수에 값이 할당되는 과정을 살펴보면서 이해해보도록 하자.
let str = "Hello";
str = "World";
첫번째 줄에서 str 이라는 변수에 "Hello" 라는 문자열 데이터가 할당되었다. 자바스크립트는 콜 스택(Call Stack)과 메모리 힙(Memory Heap)이라는 구조를 통해 데이터 및 코드 실행을 관리하는데, 원시타입의 데이터들은 콜 스택 메모리에 저장된다. 다시 말해, 스택 메모리에 "Hello" 라는 데이터 값이 주소와 함께 저장되며 식별자 str은 해당 주소를 가리키게 되는 것이다.
이후 두번째 줄이 실행되면 "Hello"라는 기존 데이터가 수정되는 것이 아니라 새로운 문자열 "World"를 스택 메모리에 생성하고 해당 메모리 주소를 식별자 str이 가리키게 된다. 두번째 줄이 실행된 이후에도 여전히 문자열 "Hello"는 메모리에 존재하고 있으며 식별자 str의 입장에서는 "Hello" 문자열 데이터가 저장된 메모리 주소를 가리키고 있다가 "World" 데이터를 저장하고 있는 메모리 주소를 가리키도록 변경되었을 뿐인 것이다.
재할당뿐만 아니라 값이 복사되는 과정에서 역시 불변성을 유지하게 된다.
let a = 10;
let b = a;
a = 20;
console.log(a) // 20
console.log(b) // 10
첫번째 줄에서 a 변수에 10 이라는 데이터 값이 할당되면 스택 메모리에 주소와 함께 10 이라는 값이 저장되고 a는 해당 주소를 가리키게 된다. 두번째 줄과 같이 b 변수에 a 변수 값을 복사할 경우, b는 10 이 저장된 메모리 주소를 가리키게 되고 이후 a 변수에 20 이라는 값을 재할당 하게 되면, 10 이라는 데이터는 여전히 메모리에 저장된 채 20 이라는 새로운 데이터가 메모리에 생성되면서 식별자 a는 20 이 저장된 메모리 주소를 가리키게 된다.
이처럼 문자열 값도 한 번 만든 값을 바꿀 수 없고 숫자 값도 다른 값으로 변경할 수 없다. 변경은 새로 만드는 동작을 통해서만 이뤄지고 이것이 바로 불변값의 성질이다. 어떤 데이터에 대해 자신의 주소를 참조하는 변수의 개수가 0 이 된다면, 가비지 컬렉터(Garbage Collector)의 수거 대상이 되어 빈 공간이 되지만 가비지 컬렉팅을 당하지 않는 한, 한 번 만들어진 값은 영원히 변하지 않는다.
2. Mutable type
앞서 살펴본 원시 타입의 경우 모든 값은 불변값이다. 이와 달리 참조 타입(Reference type)의 데이터는 기본적으로 가변값의 성질을 갖는다. 참조형은 객체(Object)가 있고 배열, 함수, 날짜, 정규 표현식 등과 ES6에서 추가된 Map, Set 등이 객체의 하위 분류에 속한다.
- Object
- Array
- Function
- Date
- RegExp
- Map
- Set
참조 타입의 기본적인 성질은 가변값이지만, 활용 방안에 따라 불변값으로 만들어서 사용할 수도 있다. 우선 예제를 살펴보면서 참조형 타입의 데이터를 가변값이라고 부르는 이유를 알아보자.
const obj1 = {
name: "Jay"
};
const obj2 = obj1;
console.log(obj2.name) // Jay
obj1.name = "Jane";
console.log(obj2.name) // Jane
console.log(obj1 === obj2) // true
obj2 변수에 obj1 를 복사하고 난 후 obj1.name 프로퍼티를 변경하면 obj2.name 프로퍼티 역시 변경되는 것을 확인할 수 있다. 그리고 === 연산자를 통해 obj1 과 obj2 를 비교하면 true 값이 나오는 것을 알 수 있다. 어떻게 된 것일까?
참조 타입은 원시 타입과 다르게 데이터의 크기가 동적으로 변할 수 있다. 이러한 특징 때문에 Object 데이터 자체는 메모리 힙(Memory heap)이라는 공간에 저장되며 변수가 가리키는 주소에는 원시 타입과 달리 메모리 힙의 주소값이 들어있다.
결국 const obj2 = obj1; 과 같은 방식으로 복사를 진행할 경우, 식별자 obj2 가 가리키는 값은 obj1 에 할당된 객체의 메모리 힙 주소값이 되는 것이다. obj1 과 obj2 모두 메모리 힙에 존재하는 동일한 데이터를 바라보고 있기 때문에 obj1.name 프로퍼티 값을 변경할 경우, obj2.name 프로퍼티의 값 역시 변경되는 상황이 벌어지게 된다. (참고로 이러한 방식의 복사를 "얕은 복사"라고 한다) 그렇다면 왜 참조 타입의 데이터는 가변값이라고 얘기하는 것일까?
참조 타입의 데이터에서 눈여겨 봐야할 부분은 원시 타입의 데이터와는 다르게 "객체의 변수(프로퍼티) 영역"이 별도로 존재한다는 점이다. 객체 자체를 저장하는 메모리 힙 주소와 별개로 객체의 프로퍼티 하나 하나 모두 메모리 주소와 값을 가지고 있다. 객체의 프로퍼티 값이 원시 타입일 경우, 각각의 프로퍼티 데이터는 불변값을 가지지만 복사된 객체의 입장에서는 원본 객체의 프로퍼티가 수정됨에 따라 자신의 프로퍼티 역시 변경되기에 "가변값"이라는 표현을 사용하는 것이다. 따라서 참조형 데이터가 "가변값"이라고 설명할 때의 "가변"은 참조형 데이터 자체를 변경할 경우가 아니라 그 내부의 프로퍼티를 변경할 때만 성립하는 말이 된다. 데이터 자체를 변경하고자 하면(새로운 데이터를 할당하고자 하면) 원시 타입의 데이터와 마찬가지로 기존 데이터는 변하지 않는다. 즉, 다음과 같이 내부 프로퍼티를 변경할 필요가 있을 때마다 매번 새로운 객체를 만들어 할당 한다면 객체 역시 불변성을 확보할 수 있게 된다.
const obj1 = {
name: "Jay"
};
let obj2 = obj1;
console.log(obj2.name) // Jay
obj2 = {
name: "Jay"
};
obj1.name = "Jane";
console.log(obj2.name) // Jay
console.log(obj1 === obj2) // false
새로운 객체를 만들어 재할당 하는 경우 메모리 힙에 새로운 데이터가 생성되므로 불변성을 유지한 채 복사를 진행할 수 있게 된다. 이러한 방식의 복사를 "깊은 복사"라고 하며 불변성이 확보된 객체는 "불변 객체"로 취급된다. 얕은 복사와 깊은 복사에 대해서는 이전에 정리해 놓은 글이 있으므로 참고하길 바란다.
2022.01.07 - [JavaScript] - JavaScript - 얕은복사 , 깊은복사
왜 불변성을 지켜야 하는가?
끝으로 불변성을 지키는 것이 왜 중요한지에 대해 얘기해보고자 한다. 개발자라면 당연히 유지보수성이 높고 가독성이 좋은 코드를 작성하기 위해 노력해야만 한다. 불변성을 지키지 않은채 데이터를 조작하고 다룬다면 데이터가 바뀌어가는 변화의 흐름을 쫓아가기 어려울 뿐더러 원본 객체를 손상시켜 예기치 못한 사이드 이펙트 혹은 버그들을 유발할 수도 있다. 반면, 불변성을 지키면서 데이터를 변화시킨다면 예기치 못한 곳에서 데이터가 변화되었을 수도 있다는 의심 없이 코드를 작성할 수 있으며 이는 예측 가능하고 신뢰할 수 있는 코드의 발판이 된다. 단순히 불변성이라는 개념 그 자체에 집중하기 보다 왜 불변성을 지켜가면서 코드를 작성해야 하는지에 포커스를 두고 공부해 나아간다면 더 좋을 것 같다.
'JavaScript' 카테고리의 다른 글
JavaScript - Map , Set (2) | 2023.11.01 |
---|---|
JavaScript - this? this! (0) | 2023.10.26 |
JavaScript - 일급 객체(First Class Object) & 일급 함수(First Class Function) (0) | 2023.10.19 |
JavaScript - 전역(global)변수 , 지역(local)변수 (0) | 2022.02.04 |
JavaScript ES6 - 템플릿 리터럴 , 객체 리터럴 , 구조분해 할당 (0) | 2022.01.27 |