9/06/2023

Primitive vs. Reference

Primitive vs. Reference

메모리와 메모리 관리를 다룰 때 기본 데이터(Number, String, Boolean, Undefined, Null, Symbol, BigInt)를 원시 타입이라고 부르며 객체(기본 데이터 외 객체 리터럴, 배열, 함수를 포함한 모든 것)를 참조 타입이라고 부른다. 자바스크립트 엔진의 두 가지 구성 요소, 즉 호출 스택과 힙에서 참조 타입은 힙에 저장되고 원시 타입은 호출 스택에 저장된다. 즉, 원시 타입은 선언된 실행 컨텍스트에 저장된다.

1
2
3
4
5
6
let name = "선심";
let name2 = name;
name = "박선심";
 
console.log(name); // 박선심
console.log(name2); // 선심
cs
예시를 통해서 차이점을 알아본다. 예를 들어 변수 name = '선심'을 선언하면 먼저 자바스크립트는 변수 이름을 가진 고유 식별자를 생성한다. 그리고 0010과 같은 특정 주소에 메모리가 할당되고 마지막으로 값이 메모리의 지정된 주소에 저장된다. 값 '선심'은 메모리 주소 0010에 저장된다. 이 모든 작업은 원시 타입이 저장되는 호출 스택에서 발생한다. 중요한 점은 식별자가 실제로 주소를 가리키며 값 자체를 가리키지 않는다는 것이다. 따라서, name 변수는 '선심'과 동일하다고 말할 수 있지만 사실 메모리 주소 0010과 동일하며그 주소에 '선심'의 값을 가지고 있다. let name2 = name에서 name2 변수는 name 변수와 동일한 메모리 주소를 가리킨다. name2 = '선심'처럼 보일 것이다. 하지만 다음 줄 name = '박선심'으로 설정하면 메모리 주소 0010에 값이 '박선심'이 되지 않는데 만약 그렇게 되면 둘 다 동일한 주소를 가리키고 있기 때문에 name2도 변경될 것이기 때문이다. 하지만 메모리 주소는 불변(immutable)이기 때문에 이는 말이 되지 않는다. 따라서, 새로운 메모리가 할당되고 name 식별자는 새로운 주소를 0011을 가리키며 0011의 새로운 값 '박선심'을 가진다.

1
2
3
4
5
6
7
8
9
10
11
const sunshim = {
  job: "경찰",
  age: 57;
};
 
// 스택의 메모리에 값이 아니라 힙의 메모리에 값 변경이라서 오류 X
const friend = sunshim;
friend.age = 47;
 
console.log("friend", friend); // { job: '경찰', age: 47 }
console.log("sunshim", sunshim); // { job: '경찰', age: 47 }
cs
참조 타입은 원시 타입과 다른 방식으로 동작한다. 객체 sunshim은 생성되면 힙에 저장되고 메모리 주소와 값을 가진다. sunshim 식별자는 힙에 생성된 메모리 주소 D10E를 직접적으로 가리키지 않으며 호출 스택에 생성된 메모리 주소 0012를 가리킨다. 이 메모리는 힙의 메모리 주소 D10E을 값으로 사용하여 힙에 있는 객체를 가리킨다. 즉, 호출 스택에 있는 메모리는 힙에 있는 메모리를 참조한다. 이 때문에 객체를 참조 타입이라고 부른다. 다시 말하면, 변수를 객체로 선언하면 식별자가 호출 스택 내의 메모리를 가리키며 이 메모리는 힙 내의 메모리를 가리킨다. 객체가 이렇게 동작하는 이유는 객체가 호출 스택에 저장하기에는 너무 클 수 있기 때문이다. 따라서, 객체는 거의 무제한 메모리 풀인 힙에 저장되며 호출 스택은 단순히 객체가 실제로 힙에 저장된 위치를 가리키는 참조를 유지함으로써 필요할 때 언제든지 해당 객체를 찾을 수 있다.

friend라는 새 변수를 생성하고 이를 sunshim 객체와 동일하게 설정하면 원시 타입과 마찬가지로 friend 식별자는 sunshim 식별자와 정확히 동일한 메모리 주소를 가리킨다. friend 객체는 본질적으로 sunshim 객체와 정확히 동일하다. friend.age를 47로 설정하면 객체가 힙에서 찾아지고 57이 47로 변경된다. 그런데 friend 변수를 상수로 정의했음에도 문제없이 객체를 조작할 수 있는데 friend 식별자의 메모리 주소(0012)에 값(즉, D10E)을 변경하는 것이 아니므로 여전히 동일한 객체를 가리킨다. 힙 안의 값을 변경한 것이기에 이것은 문제가 되지 않는다. 따라서, const로 선언된 모든 변수가 불변하다는 것은 사실이 아니면 원시 타입에 대해서만 적용되고 참조 타입에 대해서는 적용되지 않는다. sunshim 객체에서도 age가 47인 것인 이유는 sunshim과 friend가 메모리 힙에서 정확히 동일한 객체를 가리키고 있기 때문이다. 따라서 이 객체에서 무언가를 변경할 때마다 friend와 sunshim 두 곳에서 모두 반영된다. 이는 객체를 복사하고 있다고 생각할 때 실제로는 동일한 객체를 가리키는 새 변수를 생성하는 것뿐이라는 것이다. 이는 자바스크립트가 실제로 작동하는 방식에 매우 큰 영향을 미친다.

일반적으로 객체를 복사할 때 Object.assign 메서드를 사용하는데 이 메서드는 두 개의 객체를 병합하고 새로운 객체를 반환한다. 문제는Object.assign 메서드는 1번 수준에서만 작동한다. 즉, 객체 내부에 객체가 있다면 내부 객체는 여전히 동일한데 이는 메모리 내에서 여전히 동일한 위치를 가리킨다는 것이다. 따라서, Object.assign 메서드는 얕은 복사(shallow copy)만 만든다. 얕은 복사는 1번 수준의 속성만 복사하고 깊은 복사는 모든 것을 복사한다. lodash 라이브러리를 사용해서 깊은 복사를 사용할 수 있다.

update: 2023-09-06

댓글 없음:

댓글 쓰기