기본형(primitive type)과 참조형(reference type) 데이터를 담는 변수의 선언, 초기화, 할당 과정을 메모리 측면에서 이해하고 가변형(mutable type)과 불변형(immutable type)의 의미를 파악해보자.
데이터 타입과 가변성, 불변성
어떤 언어를 배우든 가장 먼저 변수를 배우게 됩니다. 이를 공부하다 보면 mutability와 immutability라는 단어를 마주하게 됩니다. 가변성 불변성이라고 번역되지만 불변하다고 하는데 값을 바꿀 수 있는 것 같고... 의미가 잘 와닿지 않습니다. 메모리의 관점에서 변수의 생성 과정을 보는 것이 불변, 가변의 의미를 이해하는데 조금은 도움이 될 수 있습니다. Js에서 데이터의 타입에 따른 변수의 선언, 초기화, 할당 과정을 메모리 측면에서 살펴보고 mutability와 immutability의 의미를 이해하는 것이 이 글의 목표입니다.
Js에서 데이터 타입은 크게 primitive type과 reference type으로 구분됩니다. Primitive type에는 Number, String, Boolean, null, undefined, Symbol 6가지가 있고 그 외는 모두 reference type으로 보면 됩니다. 기본적으로 우리가 알고 있는 것은 primitive type은 immutability(불변성)을 띄고 reference type은 mutability(가변성)을 띈다고 합니다. 무슨 말인지 직관적으로 이해되지 않습니다. 이를 이해하기 앞서 용어 정리를 한 뒤 메모리 측면에서 primitive type과 reference type의 차이를 살펴보고 이를 통해 위 말을 이해해보겠습니다.
저를 위해 정리한 것이기도 해서 글이 길어졌습니다. 결론만 원하시는 분은 마지막의 "5. mutability와 immutability"부터 읽으시는 것을 추천드립니다.
❗️이 글은 '코어 자바스크립트(정재남 지음)'을 읽고 제 나름대로 정리하고 설명한 글입니다. 따라서 제가 정리하고 싶은 부분을 정리한 것이기 때문에 책의 주제와 상이하고 제가 이해한 것을 토대로 설명 하기에 책 내용과 다르게 설명하는 부분이 많습니다.
용어 정리
이 글에는 변수, 식별자, 데이터, 주소 등의 용어가 빈번하게 등장합니다. 글을 시작하기 앞서 이 용어들에 대해 이글에서의 사용될 의미를 정리하겠습니다. 사전적 정의와 다를 수 있지만 이 글을 쓰는데 제가 사용한 의미로 글을 읽는데 오해가 없었으면 합니다.
- 데이터: 기억하고 싶은 정보(주소, 숫자, 문자열, 객체, 배열 등)
- 변수: 데이터를 저장하는 메모리의 공간
- 값: 변수에 담긴 데이터(일반적으로 변수의 값이라고 말합니다.)
- 식별자: 변수에 붙인 사람을 위한 이름
- 주소: 메모리마다 붙은 컴퓨터를 위한 이름(숫자 데이터와 구분하기 위해 @를 붙여 표현하겠습니다.)
- 할당하다 = 저장하다 = 담다
- 가리키다 = 참조하다
위 용어를 풀어 설명하면 다음과 같습니다. 우리는 무언가를 저장하고 싶을 때 변수를 사용합니다. 이때 그 '무언가'가 데이터가 됩니다. 데이터를 저장하기 위해 변수 a를 만들면(선언하면) 컴퓨터는 메모리에 적당한 공간(변수)을 마련하고 그 공간에 데이터를 담은(할당한) 다음 사람을 위한 이름(식별자) a를 붙입니다. 메모리에는 모두 숫자로 된 주소가 붙어있어서 사람이 'a'라고 하면 컴퓨터는 식별자가 'a'인 변수를 찾고 그 변수(메모리의 공간)의 주소로 이해합니다. 이러한 과정으로 사람과 컴퓨터가 메모리 상의 같은 공간을 말할 수 있습니다.
이 설명은 정확하지 않지만 용어를 이해하는 데는 문제가 없습니다. 아래 글을 통해 위 설명이 왜 정확하지 않은 지 알아보겠습니다.
1. 메모리에서 본 primitive type을 담는 변수의 생성 과정
백견이 불여일행. 글로만 보면 이해가 쉽지 않습니다. 코드로 작성해보고 메모리에서 변수 선언, 초기화, 할당 과정을 도식화해 이해해보겠습니다.
var a = 10;
책이나 인터넷에서 예시를 볼 때마다 let, const대신 var를 사용하는 것이 마음에 들지 않았었는데 막상 이런 설명하는 글을 쓰려니 이해가 됐습니다. let과 const도 과정의 거의 동일하니 var를 쓰는 것이 불편하더라도 조금만 참고 읽어주시면 감사하겠습니다. let과 cosnt는 추후 다른 포스트에서 다루도록 하겠습니다.
위 코드를 보면 Number 타입의 데이터 10을 저장할 변수를 생성하고 그 변수의 식별자를 'a'라고 지정했습니다. 이제 컴퓨터의 입장에서 저 코드를 실행해보겠습니다. 메모리 주소는 숫자 데이터와 구분하기 위해 '@'를 앞에 붙여 표현하겠습니다. 또 주소에 사용된 숫자는 임의의 숫자입니다.
1-1. 용어 정리에서 설명된 변수 생성 과정 (부정확)
(1) 변수를 위한 메모리의 적당한 빈 공간(@1001)을 확보한다.
(2) 이 변수의 식별자를 'a'로 한다.
(3) 이 변수에 데이터 10을 담는다.
제가 용어 정리에서 한 변수의 선언 과정은 위와 같습니다. 하지만 이는 정확하지 않다고 말씀드렸습니다. 이제 실제로는 어떻게 동작하는지 살펴보겠습니다.
1-2. primitive type을 담는 변수의 선언, 할당 과정
(1) 변수를 위한 메모리의 적당한 빈 공간(@1001)을 확보한다.
(2) 이 변수의 식별자를 'a'로 한다.
(3) 저장할 데이터인 10이 데이터 영역에 있는지 탐색한다. 10이 저장되어 있지 않으므로 데이터 영역의 빈 공간(@5014)에 10을 저장한다.
(4) 변수 영역에서 식별자 'a'를 검색한다. (@1001)
(5) 10을 저장한 데이터 영역의 주소(@5014)를 변수 a(@1001)의 값에 저장한다.
이게 뭐람... 이상한 짓을 합니다. 우리가 생각했던 것과 많이 다릅니다. 먼저 식별자가 없는 "데이터 영역"이 등장했고 변수(@1001)의 값에 데이터(10)를 직접 저장하지 않습니다. 데이터 영역에 데이터를 저장한 후 데이터 영역의 주소(@5014)를 변수 영역에 있는 변수(@1001)의 값에 저장합니다. 왜 이렇게 복잡하게 저장할까요? 그냥 변수 영역과 데이터 영역을 나누지 않고 @1001의 값에 10을 저장하면 편하고 메모리도 적게 쓸 수 있지 않을까요?
1-3. 왜 변수 영역에 직접 데이터를 저장하지 않을까?
이는 우리가 너무 쉬운 예제로 생각했기 때문입니다. 프로그램은 훨씬 크고 많은 데이터를 다룹니다. 위와 같이 비효율적으로 보이는 방식이 실제로는 더 효율적입니다. 매우 긴 문자열이 있다고 가정하겠습니다. 이 문자열을 두 개의 변수에 담고 두 변수의 식별자를 각각 a, b 라 하겠습니다. 이때 메모리에서 변수의 생성과정을 살펴보겠습니다.
var a = "엄청 긴 문자열"
var b = "엄청 긴 문자열"
❗️이때 호이스팅이 발생해 변수 선언이 먼저 되고 할당이 나중에 됩니다. 즉,
(변수 a 선언 -> a에 문자열 할당 -> 변수 b 선언 -> b에 문자열 할당)의 순서가 아닌
(변수 a 선언 -> 변수 b 선언 -> a에 문자열 할당 -> b에 문자열 할당)의 순서로 진행됩니다.
현재 설명하고자 하는 "왜 우리가 처음에 생각한 방식이 아닌 더 복잡한 방식으로 변수를 저장하는가?"에 대한 답을 찾는 데는 호이스팅을 고려하지 않아도 문제가 되지 않기 때문에 호이스팅은 고려하지 않고 설명하겠습니다. 호이스팅이 무엇인지 모르신다면 다음 포스트를 읽어보시면 도움 되실 것입니다. 하지만 호이스팅을 모르시더라도 이 글을 읽는데 문제가 없으므로 이 글을 먼저 읽고 호이스팅에 관한 글을 읽는 것을 추천드립니다.
(1) 변수 a를 위한 메모리의 적당한 빈 공간(@1002)을 확보한다.
(2) 이 변수의 식별자를 'a'로 한다.
❗️조금 더 정확하게 하자면 이때 var로 선언된 변수에 경우 데이터 영역에 저장된 undefined를 가리키는 주소가 값에 들어가게 됩니다. 호이스팅과 관련되어 있는 이 부분 역시 고려하지 않도록 하겠습니다.
(3) 변수 a에 저장할 데이터인 "엄청 긴 문자열"이 데이터 영역에 있는지 탐색한다. 저장되어 있지 않으므로 데이터 영역의 빈 공간(@5015)에 "엄청 긴 문자열"을 저장한다.
(4) "엄청 긴 문자열"을 저장한 데이터 영역의 주소(@5015)를 변수 a(@1002)의 값에 저장한다.
(3) 변수 b를 저장할 적당한 메모리(@1004)를 확보하고 이 변수의 식별자를 'b'로 한다.(이제 위 (1), (2) 과정은 한 번에 쓰도록 하겠습니다.)
(5) 변수 b에 저장할 데이터인 "엄청 긴 문자열"이 데이터 영역에 있는지 탐색한다. 데이터 영역의 @5015에 저장되어 있으므로 따로 저장하지 않는다.
(6) "엄청 긴 문자열"을 저장한 데이터 영역의 주소(@5015)를 변수 b(@1004)의 값에 저장한다.
코드에서는 용량이 큰 "엄청 긴 문자열"을 a와 b에 총 두 번 저장했지만 메모리에는 한 곳(@5015)에만 저장되어있습니다. 즉 이런 복잡한 방식은 메모리 상에서 데이터의 중복을 방지합니다. 만약 우리가 처음 생각했던 방식으로 저장했다면 어떻게 되었을까요?
용량이 큰 "엄청 긴 문자열"이 @1002와 @1004에 모두 저장됩니다. 데이터의 중복이 발생합니다. "엄청 긴 문자열"을 10개의 변수에 저장한다면 어떨까요? 데이터에 중복이 발생할수록 메모리를 변수 영역과 데이터 영역으로 나눠 데이터 영역에 데이터를, 변수 영역에는 식별자와 데이터 영역의 주소 값을 저장하는 방식이 유리합니다.
2. 메모리에서 본 primitive type을 담는 변수의 재할당 과정
앞서 우리는 변수의 생성과정(선언, 할당)을 메모리 측면에서 살펴봤습니다. 이때 변수를 재할당하면 메모리에서는 어떤 일이 일어날까요? 간단하게 생각해보면 데이터 영역에 저장된 데이터의 값을 바꾸면 될 것 같습니다. 하지만 컴퓨터는 다르게 동작합니다.
2-1. primitive type을 담는 변수 값의 재할당
위의 "1-2. primitive type을 담는 변수 선언, 할당 과정"의 예시를 다시 사용해보겠습니다.
// "1-2. primitive type을 담는 변수 선언, 할당 과정"에서 사용한 코드
var a = 10
// 재할당
a = "문자열"
var a = 10까지 실행되었을 때 메모리는 아래와 같습니다. 이제 a = "문자열"로 재할당 해보겠습니다.
(1) 저장할 데이터인 "문자열"이 데이터 영역에 있는지 탐색한다. 저장되어 있지 않으므로 데이터 영역의 빈 공간(@5013)에 "데이터"를 저장한다.
(2) 식별자가 'a'인 변수를 변수 영역에서 탐색한다.
(2) "문자열"을 저장한 데이터 영역의 주소(@5013)를 변수 a(@1001)의 값에 저장한다.
또 이상한 짓을 합니다. 그냥 @5014에 저장된 데이터를 "문자열"로 바꾸면 될 것 같은데 왜 데이터 영역에 따로 저장하고 변수 영역의 값을 바꿔주는 걸까요?
2-2. 왜 데이터 영역에 데이터를 바꾸지 않고 새로 저장할까?
자바스크립트는 프로그래밍을 쉽게 하기 위해 동적 타이핑(Dynamic Typing)을 채용한 동적 타입 언어입니다. 타입을 고려하지 않아 쉽고 빠르게 코딩할 수 있다는 장점이 있지만 반대로 정적 타입 언어와 비교했을 때 메모리 낭비가 크고 타입 추론의 과정이 추가됩니다. 또 오류를 방지하기 위해 프로그래밍 시 고려할 것도 많아지는 단점이 있습니다. Js엔진은 할당 단계에서 데이터의 타입을 추론해 메모리에 각 타입에 미리 약속된 만큼의 공간을 확보합니다. 예를 들어 Number는 8byte의 메모리를 할당받습니다. 이렇게 변수에 어떤 데이터를 저장할 때 메모리에 할당되는 공간은 그 데이터의 타입에 따라 결정됩니다. 하지만 앞서 말했듯 Js는 동적 타입 언어로 Number 타입을 저장한 변수 a에 String 타입의 "문자열"을 재할당 할 수 있습니다. Number 타입을 할당받았기 때문에 10이 저장되어있는 메모리(@5014)의 크기는 8byte입니다. 이때 만약 우리가 예상한 데로 @5014의 값을 10에서 새로 할당된 "문자열"로 바꿀 경우 새로 할당된 데이터의 크기가 8byte를 초과한 다면 데이터가 손실되거나 @5014 뒤(@5015)에 담긴 데이터가 있다면 데이터를 모두 이동시켜 공간을 확보해야 합니다. 이는 매우 비효율적입니다. 따라서 "2-1. primitive type을 담는 변수 값의 재할당"과정처럼 새로운 메모리 공간에 데이터를 저장하고 변수 영역의 참조값(@1001의 값)만 바꿔주는 방식으로 재할당이 이루어집니다. 이렇게 타입에 따라 메모리의 크기가 달라지는 것은 앞서 살펴봤던 "1-3. 왜 변수 영역에 직접 데이터를 저장하지 않을까?"의 대답이기도 합니다.
2-3. primitive type을 담는 변수의 복사
이번에는 변수가 복사되는 과정을 알아보겠습니다. 매우 간단하니 편하게 읽어주시면 감사하겠습니다. 이번에도 "1-2. primitive type을 담는 변수 선언, 할당 과정"의 예시부터 시작하겠습니다.
// "1-2. primitive type 변수 선언, 할당 과정"에서 사용한 코드
var a = 10
// 변수 복사
var b = a
역시 var a = 10까지 실행되었을 때 메모리는 아래와 같습니다. 이제 var b = a로 변수를 복사해보겠습니다.
❗️이번에도 엄밀히 말하면 호이스팅에 의해 메모리에서 var b가 먼저 실행됩니다. 역시 고려하지 않겠습니다.
(1) 변수 영역에서 적당한 메모리(@1003)를 확보하고 식별자로 b를 저장한다.
(2) 변수 a(@1001)에 담겨있는 값(참조값 @5014)을 식별자가 b인 변수의 값에 담는다.
이때 a에 다른 값을 할당하면 어떻게 될까요?
// "1-2. primitive type을 담는 변수 선언 과정"에서 사용한 코드
var a = 10
// 변수 복사
var b = a
// a에 값을 재할당
a = "다른 값"
(3) 저장할 데이터인 "다른 값"이 데이터 영역에 있는지 탐색한다. 저장되어 있지 않으므로 데이터 영역의 빈 공간(@5016)에 "다른 값"을 저장한다.
(5) "다른 값"을 저장한 데이터 영역의 주소(@5016)를 변수 a(@1001)의 값에 저장한다.
2-4. primitive type 정리
지금까지 우리는 primitive type을 담는 변수의 선언, 할당, 재할당, 복사에 대해 메모리 측면에서 어떻게 동작하는지 알아봤습니다. primitive type 데이터는 데이터 영역에 저장됩니다. primitive type을 담는 변수는 데이터 영역에 저장되어 있는 primitive type 데이터의 주소를 담고 있습니다.
우리는 아직 이 글의 궁극적인 목표를 찾지 못했습니다. mutability와 immutability는 무엇을 의미하는 것일까요? 이제 reference type 데이터와 reference type을 담는 변수의 생성과정을 알아보고 이 글의 목표를 향해 달려가 봅시다.
3. 메모리에서 본 reference type을 담는 변수의 생성 과정
이번에는 reference type과 reference type을 담는 변수의 생성 과정을 메모리의 관점에서 설펴보겠습니다. 먼저 객체를 살펴보고 배열을 프로퍼티로 갖는 중첩 객체(nested object)를 살펴보겠습니다. reference type을 담는 변수의 생성 과정이 더 복잡합니다. 중간에 헷갈리시더라도 끝까지 그림을 보며 이해해보시고 다시 한번 읽어보시는 것을 추천드립니다.
3-1. reference type(객체)을 담는변수의 선언, 할당 과정
아래 코드를 실행할 때 메모리에서는 어떤 일이 일어나는지 살펴보겠습니다.
var obj1 = {
a : 11,
b : "cloer"
}
(1) 변수 영역에 적당한 공간을 확보하고 식별자로 obj1을 저장한다.
(2) 데이터 영역에 데이터를 저장하려고 보니 데이터가 프로퍼티들로 이루어진 데이터 집합입니다. 이 데이터 집합의 데이터들을 저장하기 위해 별도의 변수 영역(@7021 ~ ?)을 확보하고 그 변수 영역의 주소(@7021 ~ ?)를 데이터 영역의 임의의 공간(@5014)에 저장한다.
(3) 프로퍼티를 저장할 변수 영역의 주소(@7021~?)를 담고 있는 데이터 영역의 주소(@5014)를 식별자가 obj1인 변수(@1002)의 값에 저장한다.
(4) @5014의 변수 영역에 확보된 변수 영역(@7021, @7022)에 각각 식별자로 a와 b를 저장한다.
(5) 식별자가 a인 프로퍼티(@7021)에 할당할 데이터인 11이 데이터 영역에 있는지 탐색한다. 없으므로 데이터 영역의 임의의 공간(@5016)에 11을 저장하고 11을 저장한 메모리의 주소(@5016)를 식별자가 a인 프로퍼티(@7021)의 값에 저장한다.
(6) 식별자가 b인 프로퍼티(@7022)에 할당할 데이터인 "cloer"가 데이터 영역에 있는지 탐색한다. 없으므로 데이터 영역의 임의의 공간(@5013)에 "cloer"를 저장하고 "cloer"를 저장한 메모리의 주소(@5013)를 식별자가 b인 프로퍼티(@7022)의 값에 할당한다.
⭐️ 확실히 primitive type을 담는 변수보다 복잡합니다. reference type을 담는 변수(@1002)는 프로퍼티들을 담는 변수들(@7021, @7022)의 주소를 데이터 영역(@5014)에 담고 프로퍼티에 담기는 데이터가 primitive type이면 "1-2. primitive type을 담는 변수 선언, 할당 과정"이 연속적으로 실행되고 프로퍼티에 담기는 데이터가 reference type이면 재귀 함수처럼 "3-1. reference type을 담는 변수의 선언, 할당 과정"이 또 실행된다고 생각하시면 이해가 쉬우실 수 있습니다.
여기서 알 수 있는 점은 primitive type 데이터는 데이터 영역에 저장되고 reference type 데이터는 데이터 영역에 주소를 저장합니다. 그 주소와 그 주소가 참조하는 변수 영역을 통틀어 reference type 데이터로 보면 이해가 쉽습니다.
primitive type을 담는 변수는 참조가 한번 되고 reference type을 담는 변수는 참조가 최소 3번 됩니다.
프로퍼티도 하나의 변수로 본다면 "변수 - 데이터 - 변수 - 데이터"의 형태입니다.
후에 다른 포스트에서 다루겠지만 사실 변수도 프로퍼티입니다. 전역변수는 전역객체(window, global)의 프로퍼티입니다. 따라서 동작이 같습니다.
- primitive type을 담는 변수: 변수 - primitive type 데이터(변수의 값)
- reference type을 담는 변수: 변수(@1002) - 프로퍼티들의 주소를 담은 reference type 데이터(@5014) - 프로퍼티(@7021, @7022) - 데이터(변수의 값)(@5013, @5016)
3-2. reference type(배열)을 프로퍼티로 갖는 reference type(객체)를 담는 변수의 선언, 할당 과정
위에서 reference type을 담는 변수는 참조가 '최소' 3번 된다고 설명했습니다. 배열을 프로퍼티로 갖는 객체를 예로 들어 왜 '최소'라는 수식어가 붙었는지 설명드리겠습니다. 아래 코드를 메모리 측면에서 실행해 보겠습니다.
var obj1 = {
a : 11,
arr : [11, "cloer", 100]
}
(1) 변수 영역에 적당한 공간을 확보하고 식별자로 obj1을 저장한다.
(2) 데이터 영역에 데이터를 저장하려고 보니 데이터가 프로퍼티들(a, arr)로 이루어진 데이터 집합입니다. 이 데이터 집합의 데이터들을 저장하기 위해 별도의 변수 영역(@7021 ~ ?)을 확보하고 그 변수 영역의 주소(@7021 ~ ?)를 데이터 영역의 임의의 공간(@5014)에 저장한다.
(3) 프로퍼티를 저장할 변수 영역(@7021~?)의 주소를 담고 있는 데이터 영역(@5014)의 주소를 식별자가 obj1인 변수(@1002)의 값에 저장한다.
(4) @5014의 변수 영역에 확보된 변수 영역(@7021, @7022)에 각각 식별자로 a와 arr를 저장한다.
(5) 식별자가 a인 프로퍼티(@7021)에 할당할 데이터인 11이 데이터 영역에 있는지 탐색한다. 없으므로 데이터 영역의 임의의 공간(@5013)에 11을 저장하고 11을 저장한 메모리의 주소(@5013)를 식별자가 a인 프로퍼티(@7021)의 값에 저장한다.
(6) 식별자가 arr인 프로퍼티(@7022)의 데이터를 데이터 영역에 저장하려고 보니 데이터가 배열 형태로 이루어진 데이터 집합입니다. 이 데이터 집합의 데이터들을 저장하기 위해 별도의 변수 영역(@9071 ~ ?)을 확보하고 그 변수 영역의 주소(@9071 ~ ?)를 데이터 영역의 임의의 공간(@5015)에 저장한다.
(7) 배열을 저장할 변수 영역(@9071~?)의 주소를 담고 있는 데이터 영역(@5015)의 주소를 식별자가arr인 프로퍼티(@7022)의 값에 저장한다.
(8) @5015의 변수 영역에 확보된 변수 영역(@9071, @9072, @9073)에 각각 배열의 인덱스(0, 1, 2)를 식별자로 저장한다.
(9) @5015의 변수 영역의 인덱스가 0인 메모리(@9071)에 할당할 데이터인 11이 데이터 영역에 있는지 탐색합니다. @5013에 11이 저장되어 있으므로 11의 주소(@5013)를 obj1.arr [0](@9071)의 값에 저장한다.
(10) @5015의 변수 영역의 인덱스가 1인 메모리(@9072)에 할당할 데이터인 "cloer"가 데이터 영역에 있는지 탐색합니다. 없으므로 데이터 영역의 임의의 공간(@5016)에 "cloer"를 저장하고 "cloer"를 저장한 메모리의 주소(@5016)를 obj1.arr [1](@9072)의 값에 저장합니다.
(11) @5015의 변수 영역의 인덱스가 2인 메모리(@9073)에 할당할 데이터인 100이 데이터 영역에 있는지 탐색합니다. 없으므로 데이터 영역의 임의의 공간(@5019)에 "cloer"를 저장하고 "cloer"를 저장한 메모리의 주소(@5016)를 obj1.arr [1](@9072)의 값에 저장합니다.
"3-1. reference type을 담는 변수(객체)의 선언, 할당 과정"의 마지막에서 설명드린 부분이 이해가 되셨으면 좋겠습니다.
reference type을 담는 변수는 프로퍼티들을 담는 변수들의 주소를 데이터 영역에 담고 프로퍼티에 담기는 데이터가 primitive type이라면 "1-2. primitive type을 담는 변수 선언, 할당 과정"이 연속적으로 실행되고 프로퍼티에 감기는 데이터가 reference type이라면 재귀 함수처럼 "3-1. reference type을 담는 변수의 선언, 할당 과정"이 또 실행된다.
이때 코드에서 obj1.arr[1]을 요청하면 메모리에서는 다음과 같은 과정이 진행됩니다.
1. 식별자가 obj1인 변수(메모리 영역)를 검색합니다. -> @1002
2. 식별자가 obj1인 변수의 값이 주소(@5014)이므로 그 주소로 이동합니다. -> @5014
3. @5014 메모리의 값이 주소(@7021 ~ ?)이므로 그 주소로 이동합니다. -> @7021 ~ ?
4. @7021 ~ ? 메모리에서 식별자가 arr인 메모리를 검색합니다. -> @7022
5. 식별자가 arr인 메모리의 값이 주소(@5015)이므로 그 주소로 이동합니다. -> @5015
6. @5015 메모리의 값이 주소(@9071 ~ ?)이므로 그 주소로 이동합니다. -> @9071 ~ ?
7. @9071 ~ ? 메모리에서 식별자가 1인 메모리를 검색합니다. -> @9072
8. 식별자가 1인 메모리의 값이 주소(@5019)이므로 그 주소로 이동합니다. -> @5019
9. @5019 메모리의 값이 문자열 데이터("cloer")이므로 데이터를 반환합니다. ->"cloer"
4. 메모리에서 본 reference type을 담는 변수의 재할당 과정
4-1. reference type(객체)을 담는 변수의 복사
reference type을 담는 변수는 복사에서 문제가 발생하기 쉽습니다. 문제가 자주 발생하는 부분을 왜 예상과 다르게 동작하는지 메모리로 살펴보겠습니다. 바로 위 "3-2. reference type(배열)을 프로퍼티를 갖는 reference type(객체)를 담는 변수의 선언, 할당 과정"의 결과에서 이어가겠습니다. 아래 그림은 obj1이 메모리상에 생성되어 있는 상태고 아래 과정은 "객체 복사"부터 입니다.
// "3-2. reference type(배열)을 프로퍼티를 갖는 reference type(객체)를 담는 변수의 선언, 할당 과정"
var obj1 = {
a : 11,
arr : [11, "cloer", 100]
}
// 객체 복사
var obj2 = obj1
(1) 변수 영역에 적당한 공간을 확보하고 식별자로 obj2를 저장한다.
(2) 식별자가 obj1인 변수(@1002)를 검색하고 그 변수의 값(@5014)을 식별자가 obj2인 변수(@1004)의 값에 저장한다.
obj1(@1002)와 obj2(@1004)는 같은 값(@5014)을 저장하고 있습니다. 객체를 복사하면 두 변수는 "같은 객체를 가리킨다."라는 말이 어떤 뜻인지 이해되셨을 것입니다. "같은 객체를 가리킨다."의 의미는 "같은 메모리를 참조한다."혹은 "같은 메모리 주소를 값으로 가지고 있다."와 같습니다.
4-2. reference type(객체)를 담는 변수의 재할당
reference type 변수의 재할당은 크게 프로퍼티의 재할당 혹은 변수의 재할당 두 가지로 볼 수 있습니다. 코드로 보면 이해가 되실 것입니다.
// "3-2. reference type(배열)을 프로퍼티를 갖는 reference type(객체)를 담는 변수의 선언, 할당 과정"
var obj1 = {
a : 11,
arr : [11, "cloer", 100]
}
// 변수의 재할당
obj1 = 50
obj1 = {
x : 1000,
y : "Y"
}
// "3-2. reference type(배열)을 프로퍼티를 갖는 reference type(객체)를 담는 변수의 선언, 할당 과정"
var obj1 = {
a : 11,
arr : [11, "cloer", 100]
}
// 프로퍼티의 재할당
obj1.a = 2000
변수의 재할당은 위 "2. 메모리에서 본 primitive type을 담는 변수의 재할당 과정"과 "3. 메모리에서 본 reference type을 담는 변수의 생성 과정"을 이해하셨다면 알 수 있을 것입니다. 메모리에 primitive type혹은 reference type의 데이터를 저장하고 변수의 값만 바꿔주면 됩니다.
보통 객체의 복사에서 발생하는 문제는 객체 복사 후 프로퍼티를 재할당할 때 발생합니다. 아래 코드를 실행하며 어떤 문제가 발생하고 왜 발생하며 어떻게 예방하는지 알아보겠습니다. "4-1. reference type(객체)를 담는 변수의 복사"에 이어서 진행하겠습니다.
// "3-2. reference type(배열)을 프로퍼티를 갖는 reference type(객체)를 담는 변수의 선언, 할당 과정"
var obj1 = {
a : 11,
arr : [11, "cloer", 100]
}
// "4-1. reference type(객체)을 담는 변수의 복사"
var obj2 = obj1
// 객체 복사 후 프로퍼티 재할당
obj2.a = 2000
(1) 저장할 데이터인 2000이 데이터 영역에 저장되어있는지 확인한다. 없으므로 데이터 영역 임의의 공간(@5017)에 2000을 저장한다.
(2) 변수 영역에서 식별자가 obj2인 변수를 찾는다. -> @1004
(3) 변수 영역에서 식별자가 obj2인 변수의 값이 주소(@5014)이므로 그 주소로 이동한다. -> @5014
(4) @5014의 값이 주소(@7021 ~ ?)이므로 그 주소로 이동한다. -> @7021 ~ ?
(5) @5014의 변수 영역에서 식별자가 a인 메모리를 찾는다. -> @7021
(6) @7021의 값에 2000을 저장한 메모리 주소(@5017)를 저장한다.
우리는 obj2라는 변수를 만들고 obj1을 obj2에 복사했습니다. 그 뒤 obj2의 프로퍼티인 a의 값을 11에서 2000으로 바꿨습니다. 이렇게 객체의 복사와 프로퍼티의 재할당이 끝났습니다. 이제 어떤 문제가 발생하는지 알아봅시다.
console.log(obj1.a) // 2000
우리는 분명 obj2.a의 값을 바꿨는데 obj1.a도 2000으로 바뀌었습니다. obj1과 obj2가 같은 주소를 저장하고 있기 때문입니다. 위와 같이 복사하는 것을 shallow copy라고 합니다. shallow copy에서 발생하는 이런 문제를 방지하기 위해 immutable.js, immer.js, immutablilty-helper와 같은 라이브러리를 사용해 deep copy를 합니다. 구조 분해 할당, Object.assign과 같은 내장 기능을 사용할 수도 있지만 중첩 객체(nested object)도 고려해야 하기에 라이브러리를 사용합니다.
이제 우리는 Js가 변수를 어떻게 메모리에 저장하고 변경하는지 알았습니다. 이제 우리의 목표 mutability와 immutabilty에 대해 알아봅시다.
5. mutability와 immutability
변수는 변수에 담기는 데이터의 타입과 상관없이 데이터 영역의 주소를 값으로 저장했습니다. primitive type 데이터는 데이터 영역에 저장되었고 reference type 데이터는 새로운 변수 영역을 확보한 후 그 영역의 주소를 데이터 영역에 저장했습니다. 이 차이에서 mutability와 immutability가 나뉘게 됩니다. mutability와 immutability는 값을 변경할 때 데이터 영역이 바뀌는지 안 바뀌는지를 의미합니다. 예를 들어 설명해 보겠습니다.
5-1. 값이 수정될 때 mutability와 immutability
// primitive type
var a = 10
//reference type
var b = {
x : 10,
y : 20
}
이제 변수의 값을 수정 한 뒤 mutability와 immutability의 의미를 알아보겠습니다. 데이터 영역에 집중해주세요.
// primitive type
var a = 10
//reference type
var b = {
x : 10,
y : 20
}
// 수정
a = 100
b.z = 100
변수 a(@1002)가 참조하던 primitive type 데이터(@5013) 10은 바뀌지 않고 데이터 영역에 그대로 있습니다. 즉 primitive type 데이터(@5013)는 불변(immutability)했습니다. 값을 수정했을 때 데이터 영역의 다른 공간에 100을 저장하고 변수 a의 값을 바꿔줬을 뿐입니다.
변수 b(@1004)는 참조하던 reference type 데이터는 어떻게 되었을까요? "3-1. reference type(객체)을 담는 변수의 선언, 할당 과정"의 마지막에서 이런 설명을 드렸습니다.
primitive type 데이터는 데이터 영역에 저장되고 reference type 데이터는 데이터 영역에 주소를 저장합니다. 그 주소와 그 주소가 참조하는 변수 영역을 통틀어 reference type 데이터로 보면 이해가 쉽습니다.
즉 reference type 데이터(@5014, @7021 ~ @7023)는 @7023이 추가되어 변경(mutablity) 되었습니다.
정리해보면 primitive type 데이터는 불변(immutability) 하기 때문에 primitive type 데이터를 담는 변수에 다른 값을 할당하려면 기존의 데이터를 변경하지 못해서 새로 데이터를 저장하고 참조를 바꿔줘야 합니다. 하지만 reference type 데이터는 변경 가능(mutability) 하기 때문에 기존의 데이터를 그대로 참조하고 그 데이터가 참조하는 변수 영역을 변경해 줬습니다.
- primitive type은 변하지 않았다. (@5013)
- reference type은 변했다. (@5014, @7021, @7022) -> (@5014, @7021, @7022, @7023)
5-2. 최종 정리
우리가 흔히 mutability와 immutability에서 혼동하는 개념이 상수와 변수입니다. 상수와 변수는 변수 영역에서 값이 변할 수 있으면 상수(var, let), 변할 수 없으면 상수(const)입니다. mutability와 immutability는 데이터가 변할 수 있는지 없는지를 지칭합니다. reference type 데이터를 주소를 참조하는 데이터 영역과 그 주소가 참조하는 변수 영역 모두로 생각한다면 변한 다는 것이 와닿습니다. primitive type 데이터는 데이터 영역에 저장되고 한번 저장되면 변하지 않습니다.
- mutability: reference type 데이터를 데이터 영역의 주소와 그 주소가 참조하는 변수 영역 전체로 생각하면 변한다.
- immutability: primitive type 데이터를 데이터 영역의 데이터로 생각하면 변하지 않는다.
- 상수(const): 값(데이터 영역을 참조하는 주소)이 변하지 않는 변수
- 변수(var, let): 값(데이터 영역을 참조하는 주소)이 변할 수 있는 변수
읽어주셔서 감사합니다.
'Languages > JS∕TS' 카테고리의 다른 글
[TS] How to get type from property of objects in array (0) | 2022.07.02 |
---|---|
[TS] interface, impliments, extends 예시 (0) | 2022.02.23 |
[TS] 기본 자료형 (0) | 2022.02.22 |
[JS] 호이스팅과 TDZ (0) | 2021.10.05 |
[JS] 1. 기본 문법과 변수, 상수 (0) | 2021.10.05 |