진짜 개발자
본문 바로가기

Web/javascript

javascript - [코어자바스크립트] 불변객체와 복사(얕은복사와 깊은복사)

728x90
1. 불변객체

Javascript의 불변객체와 값 복사에 관하여 포스팅을 진행하겠습니다.

 

본 포스팅은 코어자바스크립트라는 책을 기반으로 작성되었습니다.

 

 

 

1. 불변객체

1.1 불변객체란

https://galid1.tistory.com/622

우선 불변객체에 관해서 잘 모르시다면 위의 글을 먼저 읽어보시는 것을 추천드립니다. (JAVA로 작성되어 있지만, 참조형의 개념을 아신다면 크게 문제없이 읽을 수 있습니다.)

 

간단히 말씀드려 불변객체란 어떤 객체내부의 프로퍼티들을 변경할 수 없도록 되어있는 객체를 일컫습니다. 그렇다면 언제 이런 불변객체가 필요할까요?

 

 

 

1.2 불변객체의 필요성 (기본형데이터와 참조형데이터의 차이)

문제상황

예를들어 위와같이 복잡한 데이터 구조를 가지는 people 객체가 존재한다고 하겠습니다.

 

이때, 이름만 다르고, 나머지 속성들은 동일한 people객체가 5개가 더 필요하다고 한다면, 어떻게 할까요?? 기존객체를 이용하여, 이름만을 바꾸면 되지 않을까요??

 

우선 changeName(people, newName) 이라는 함수를 만들었습니다. 이 함수는 내용을 복사할 people 객체와, 새로운 이름을 나타내는 newName 변수를 매개변수로 전달받고, 이를 이용해 이름만이 변경된 객체를 반환하는 함수입니다.

 

잘 동작할것으로 예상이되시나요??

 

아쉽게도 앞서 작성한 예제의 결과는 우리가 예상한것과는 전혀 다른 결과를 발생시킵니다. 이러한 일이 발생하는 이유는, changeName() 함수의 동작을 살펴보면 알 수있듯이. 같은 객체를 가리키는 새로운 변수만을 생성하고 그 변수가 가리키는 name 프로퍼티의 값을 변경을 주었기 때문입니다.

 

즉 people1 ~ people5 까지의 모든 변수는 같은 주소공간을 바라보고 있고, 그 주소공간중 name 프로퍼티가 가리키는 데이터를 변경하였기 때문에, 모든 변수에 담긴 name프로퍼티 값이 같이 변경된 듯한 현상이 발생하는 것입니다. 이런 식으로 객체를 생성한다면, 원하지 않는 객체의 값마저 변경되버리게 됩니다. 바로 이런경우를 대비하여 불변객체가 필요합니다.

 

 

 

문제상황 해결

앞선 문제상황의 원인은, 모든 변수가 같은 주소공간을 바라보기 때문입니다. 따라서 문제를 해결하기 위해서는 원본 객체 내부의 프로퍼티(값)들을 복사한 새로운 객체를 생성하여 변수에 할당해주어야 합니다.

 

변경된 changeName(people, newName) 함수를 다시 살펴보도록하죠, 이번에는 변수를 선언하고, 매개변수로 전달된 people변수를(people 변수가 가리키는 주소공간을) 할당하는 것이 아닌, 새로운 객체를 생성하여 people의 내부 프로퍼티들을 담아주었습니다. 결과는 당연히 올바르게 출력이됍니다.

 

 

 

기본형 데이터와 참조형 데이터의 차이

https://galid1.tistory.com/661

우선 기본형 데이터를 메모리에할당 하는 과정을 위링크를 통해 보고 오시는것을 추천드립니다. 굳이 보시지 않더라도 이해하실 수 있도록 노력하여 작성은 해보겠습니다 ..

 

위에서 a변수에는 1이란 값을 담고, b는 a를 담도록 했습니다. 이후, b에 다시 2라는 값을 할당 했는데요, 이때 a와 b에는 서로다른 값이 잘 들어가게 됩니다. 앞서 객체의 경우에는 이런식으로 할 경우 a변수의 값역시 변경이 되었는데 기본형 데이터의 경우에는 그렇지 않음을 알 수 있습니다. 왜그럴까요?

 

이는 기본형 데이터와, 참조형데이터의 메모리할당 방식의 차이가 있기 때문입니다.

 

위의 예제의 메모리 할당과정을 말로 풀어설명 드리면, 우선 메모리에서 빈공간을 찾은 뒤 그 공간에 a라는 식별자를 부여합니다. 그후, 메모리의 데이터영역에서 1이라는 숫자를 찾습니다. 1이 없기 때문에, 데이터 영역에 빈공간을 찾고 그공간에 1을 대입합니다. 이후 1의 주소를 a라는 식별자의 값에 담습니다.

이어서 b입니다. b역시 메모리에서 빈공간을 찾고 그 주소에 b라는 식별자를 부여합니다. 그다음 a가 가리키는 값(1)의 주소를 b에 담습니다. 이때 b에 2를 새로히 할당하면, 메모리에서 다시 2를 찾습니다. 이후 2가 존재하지 않기 때문에 빈공간을 찾아 2를 담고 이 주소를 b의 값에 담습니다.

머릿속으로 천천히 이과정을 그리시다 보면 a의 값이 왜 바뀌지 않는지 알 수 있습니다. a의 값에 담긴 데이터 주소공간은 1로 유지된채로 b의 값에 담긴 데이터 주소공간만이 바뀌기 때문입니다. 이러한 할당과정 때문에 "javascript의 기본형은 대입시 값을 복사한다" 라고 하는 것입니다.

 

 

 

 

 

1.3 javascript의 불변객체

javascript에는 java와 같이 변수에 접근제한자를 부여하여 외부에서 변경이 불가능하게 하는 등을 할 수가 없습니다. 따라서 javascript에서 객체를 불변하게 만들기 위해서는 약속을 통해 지키도록해야 합니다.

 

예를 들면 기존 people 객체에서 이름만이 변경한 새로운 객체를 만들때에는 앞서 만든 changeName() 함수를 이용해서만 새로운 객체를 만들 것이라는 약속등을 모두가 지켜야 비로소 people 객체가 불변객체가 되는 것입니다.

 

라이브러리를 통한 불변객체 강제화

사실 불변객체의 중요성은 이미 오래전부터 언급이 되어왔기 때문에, 객체의 불변을 유지할 수 있도록 도와주는 라이브러리 (immutable.js, baobao.js)등이 존재합니다. 약속이란 것은 깨지기 쉽기 때문에, 사람간의 규칙으로 프로젝트를 관리하기 보다는 강제화할 수 있는 라이브러리 사용이 더욱 추천됩니다.

 

 

 

 

 

2. 얕은복사와 깊은복사

2.1 1.2의 changeName() 함수 일반화

얕은 복사와 깊은 복사를 설명드리기 앞서, 위의 1.2에서 생성했던 이름을 변경할 때 사용한 changeName()함수를 조금 더 일반화해보도록 하겠습니다.

 

우선 copyObject() 함수에 대해 설명드리고 왜 이런식으로 변경을 하였는지 말씀드리겠습니다.

  1. 함수의 첫줄에서 newObject라는 빈 객체를 생성합니다.
  2. 두번째줄의 for in문을 이용하면 특정 객체의 property들을 순환하며 prop이라는 변수에 담아줍니다. 이는 각 property의 키값에 해당합니다.
  3. 세번째줄에서는 newObject에 target의 값을 복사하는 과정이 일어납니다. 첫번째 for문이 실행될때만을 조금더 자세히 말씀드리면, 매개변수인 target객체의 첫번째 property(name)가 prop변수에 담기게 됩니다. 이것을 이용하여, newObject['name'] , newObject['age'] ... 를 만들어낼 수 있습니다. 그 후, target[prop]을 이용하여 원본 객체를 나타내는 target의 각 property값들을 불러올 수 있습니다.

 

이번에 새로히 작성한 함수의 경우에는, 단순히 객체를 복사하는 과정에 초점을 맞추어 작성된 함수입니다. 앞서 만들었던 changeName() 함수의 경우에는, 이름을 변경하여 새로운 객체를 반환하는 것에 초점이 맞추어져있었기 때문에, age, gender를 변경하기 위해서는 그것을 위한 함수를 또 만들어야 합니다.

 

 

 

2.2 깊은복사와 얕은 복사

2.1에서 만든 copyObject()는 얕은복사라는 것을 수행하는데요 함수에는 문제가 숨어있습니다. 문제상황 예시를 살펴보겠습니다.

 

얕은 복사(문제점)

?? 처음 만들었던 changeName() 과 같은 상황이 발생하네요. copyObject() 함수를 다시 살펴보면 이유를 금방 알 수 있습니다.

 

copyObject() 함수에서는 새로운 객체를 생성하여 새로운 객체에 target객체의 프로퍼티들을 할당하고 있습니다. 여기까지는 문제가 없어보이네요, 하지만 target 객체 내부의 프로퍼티가 또다른 객체를 가리키는 참조변수라면 어떨까요??

 

결국 useruser2에 담긴 friends property에는 같은 객체를 가리키는 주소값이 담기게 될것입니다.

 

왜 이런문제가 발생할까요? 원인은 얕은복사를 했기 때문인데요. 얕은 복사는 한 객체가 가지는 바로 하위의 property들만을 복사하는 것을 말합니다. 이때문에, user객체가 가지고 있는 또 다른 객체인 firends 배열에 대해 복사가 이루어지지 않아, useruser2가 같은 friends 배열을 바라보게 되는 것입니다.

 

 

 

깊은복사(를 통한 문제해결)

얕은 복사를 통해 한단계만 복사를하게 되어, 내부에 또 객체가 있는 경우에 발생하는 문제를 해결하기위해서는, 깊은복사를 통해 해결하면 됩니다. 즉 깊은복사는 재귀적 복사를 통해 내부에 존재하는 모든 단계의 객체들을 복사하는것을 의미합니다.

 

위의 deepCopyObject() 함수의 경우에는 앞서 말씀드린 얕은복사의 문제점을 해결하기 위해 새로 정의한 깊은복사입니다. 재귀 호출을 통해, 내부의 프로퍼티가 객체인 경우에는 다시 한번 복사를 하게 되는 함수이죠.