[TIL] 원시 값과 객체의 복사

June 05, 2021

알고리즘 문제를 풀 때 종종 배열을 복사해서 사용해야 하는 경우가 발생한다. 이때 배열을 제대로 복사하지 못하여 원본 배열까지 수정되는 바람에 정답을 한참 동안 못 구한 적이 있다. 자바스크립트의 복사에 대해 제대로 이해하고 넘어가자.

원시 타입과 객체 타입

자바스크립트의 7가지 데이터 타입(숫자, 문자열, 불리언, null, undeifined, 심벌, 객체 타입)은 크게 원시 타입과 객체 타입으로 나뉜다. 두 타입의 주요 차이는 다음과 같다.

  • 원시 값은 변경 불가능한 값(immutable value)이다. 반면 객체 타입의 값은 변경 가능한 값(mutable value)이다.
  • 원시 값을 변수에 할당하면 변수(확보된 메모리 공간)에는 실제 값이 저장된다. 반면 객체를 변수에 할당하면 변수에는 참조 값이 저장된다.
  • 원시 값을 갖는 변수를 다른 변수에 할당하면 원본의 원시 값이 복사되어 전달된다. 이를 값에 의한 전달(pass by value)라고한다. 반면 객체를 가리키는 변수를 다른 변수에 할당하면 원본의 참조 값이 복사되어 전달된다.(pass by reference)

원시 값의 불변성

원시 값이 변경 불가능하다는 뜻은 변수에 새로운 값을 재할당 할 수 없다는 의미가 아니다. 최초에 원시 값을 저장하기 위해 확보한 메모리 공간에 들어있는 값을 변경할 수 없다는 의미이다. 만약 원시 값이 담긴 변수에 새로운 값을 할당하는 경우, 새로운 값을 담을 새로운 메모리 공간을 확보해야 한다. 따라서 변수가 가리키던 메모리의 주소도 바뀌게 된다. 반대로 말하면 원시 값을 할당한 변수는 재할당 이외에 변수 값을 변경할 수 있는 방법이 없는 것이다. 이러한 값의 특성을 불변성(immutability)라고 한다.

원시 타입인 문자열을 생각해보자.

let string = 'hello';
string[0] = 'H';
console.log(string);	// 'hello'

값을 재할당 하지 않고는 원래의 값을 변경할 수 없다. 원시 값의 불변성 때문이다.

원시 값의 복사

원시 값이 담긴 변수를 복사하면 다음과 같이 동작한다.

let number = 100;
let copy = number;

console.log(number);	// 100
console.log(copy);		// 100

number = 200;

console.log(number);	// 200
console.log(copy);		// 100

이때 중요한 점은 number에 담긴 100이라는 값과 copy에 담긴 100이라는 값은 서로 다른 메모리 공간에 저장된 별개의 값이라는 것이다. 따라서 number 변수에 새로운 값을 재할당 하더라도 copy 변수에는 어떠한 영향도 주지 않는다. 이는 원시 값은 값에 의한 전달이 되기 때문이다.

let number = 100;
let copy = number;

// number라는 식별자가 0x000000F2라는 메모리 공간을 확보한다.
// 100이라는 값이 0x000000F2 라는 메모리 공간에 저장된다.
// copy라는 식별자는 0x00001332라는 새로운 메모리 공간을 확보한다.
// 0x00001332라는 메모리 주소에 100이라는 값이 저장된다.

변수는 식별자이다. 식별자는 메모리 주소를 통해 메모리 공간에 저장된 값에 접근한다. 따라서 엄밀히 말하면 원시 값도 변수에 값 그 자체가 아니라 메모리 주소를 전달하는 것이다. 다만, 전달된 메모리 주소를 통해 메모리 공간에 접근하면 그곳에 바로 값이 있다.

참조 값이 전달되는, 변경 가능한 값 “객체”

바로 앞의 설명처럼 원시 값은 변수가 기억하는 메모리 주소를 따라가면 바로 값에 도달할 수 있다.

그러나 객체는 다르다. 객체를 할당한 변수가 기억하는 메모리 주소를 따라가면 객체의 값이 있는 것이 아니라, 참조 값(reference value)이 있다. 다시 이 참조 값을 따라가면 비로소 본래의 객체 값이 저장된 메모리 공간이 있다. 즉 참조 값은 생성된 객체가 저장된 메모리 공간의 주소인 것이고, 변수는 ‘메모리 공간의 주소가 저장된 메모리 주소’를 기억하는 것이다.

let person = {
   name: "Kim"
};

// person이라는 식별자가 0x000000F2라는 주소의 메모리 공간을 확보한다.
// {name: "Kim"} 이라는 객체가 0x00001332 라는 새로운 메모리 공간에 생성된다.
// 0x000000F2 메모리 주소에 0x00001332라는 메모리 주소(= 참조 값)가 저장된다.
// person이 기억하는 메모리 주소 0x000000F2를 따라가면, 참조 값 0x00001332가 있고
// 다시 참조 값 0x00001332를 따라가면 해당 메모리 공간에는 {name: "Kim"}이라는 객체가 있다.

원시 값은 변경 불가능한 값이라서 변수의 값을 변경하려면 재할당이라는 방법뿐이라고 했다. 그러나 객체는 변경 가능한 값이기 때문에, 재할당 없이도 프로퍼틸을 추가/삭제하는 것처럼 변수에 할당된 객체를 직접 변경할 수 있다.

객체를 할당한 변수는 객체 그 자체가 아니라, 참조 값을 기억한다고 했다. 따라서 객체 자체에서 값이 변경되더라도 그 객체를 가리키고 있는 참조 값은 변하지 않는다. 변수는 참조 값을 기억하고 있기 때문에, 재할당 없이도 변수가 가리키는 객체의 값이 바뀔 수 있는 것이다.

객체의 복사

결국 복사할 때 문제가 되는 것은 객체다. 객체는 변경 가능한 값(mutable value)이면서 참조 값이 전달(pass by reference)되기 때문이다.

위에서 객체가 어떤 식으로 참조되는 지를 살펴봤다. 따라서 객체가 할당된 변수를 복사하면, 객체 그 자체 값이 복사되는 것이 아니라 참조 값이 복사되는 것을 할 수 있다.

let person = {
    name: "Kim"
};

let copy = person;

// copy라는 새로운 변수는 0x00000524 라는 새로운 메모리 주소를 가리킨다.
// 그 메모리 주소에는 {name: "Kim"} 객체가 바로 담기는 것이 아니라, 
// 객체를 담고 있는 메모리 주소를 가리키는 주소인 참조 값 0x00001332가 담긴다.
// person과 copy는 각각 0x000000F2와 0x000000524라는 메모리 주소를 기억하지만
// 두 메모리 주소는 0x00001332 라는 동일한 참조 값을 갖는다.
// 따라서 person과 copy는 궁극적으로 0x00001332라는 참조 값이 가리키는 동일한 {name: "Kim"} 객체를 가리킨다.

따라서 복사 후 원본 값을 변경하면 새로운 메모리 주소를 가리키는 원시 값과 다르게 작동한다. 이를 일치 연산자로 확인해보면 다음과 같다.

// 원시 타입
let number = 100;
let numCopy = number;
number === numCopy	// true;

number = 200;
number === numCopy	// false

// 객체 타입
let obj = {name: "Kim"};
let objCopy = obj;
obj === objCopy		// true;

obj.name = "Lee";
obj === objCopy		// true;

원시 값의 비교와 객체의 비교

원시 값을 할당한 변수를 비교하면 원시 값을 비교한다.

반면 객체를 할당한 변수를 비교하면 참조 값을 비교한다.

다음 문제를 이해해보자.

let obj1 = { name: "Kim"};
let obj2 = { name: "Kim"};

obj1 === obj2;				
// false; 
// 내용은 같지만 당연히 서로 다른 메모리에 저장된 별개의 객체다.

obj1.name === obj2.name;	
// true;
// obj1.name과 obj2.name은 둘다 "Kim"이라는 문자열로 평가되는 표현식이다.
// 원시 값을 비교할 땐 값 자체를 비교한다고 했으니 true가 된다.

이처럼 객체는 여러 개의 식별자가 하나의 객체를 공유할 수 있다. 내가 알고리즘 문제를 풀면서 실수가 발생했던 것도 바로 이 이유 때문이었다. 한쪽 변수에서 객체를 변경할 때, 참조 값이 동일한 다른 변수가 가리키고 있는 객체도 수정되었기 때문이다. 헷갈리게 왜 이렇게 만들었을까?

결국 효율성성능 때문이다.

원시 값처럼 객체를 복사할 때마다 같은 값을 새롭게 생성한다면 나와 같은 실수는 발생하지 않을 것이다. 그러나 객체는 크기가 매우 클 수도 있고, 크기도 일정하지 않으며, 객체의 프로퍼티 값으로 또 다른 객체를 가질 수도 있다. 즉 복사해서 생성하는 데 메모리의 비용이 클 수 있다는 뜻이다. 메모리를 효율적으로 사용하고 비용을 절약하여 성능을 향상시키기 위해 객체는 변경 가능한 값으로 설계되어 있는 것이다.

다음으로는 ‘얕은 복사’와 ‘깊은 복사’에 대해 알아볼 예정이다.

참고

참조에 의한 객체 복사
원시 값과 객체의 비교 | 모던 자바스크립트 Deep Dive | 위키북스


Profile picture

42KIM A person trying to create something.
👉Github