
소프트웨어 개발 과정에서 데이터를 다루다 보면, 변수를 복사했다고 생각했음에도 불구하고 원본 데이터가 함께 변경되어 치명적인 버그를 유발하는 경우를 종종 마주하게 됩니다. 이는 프로그래밍 언어가 메모리를 관리하는 방식, 특히 참조 타입(Reference Type) 데이터를 처리하는 메커니즘을 정확히 이해하지 못했을 때 발생하는 대표적인 문제입니다. 데이터의 무결성을 지키고 예측 가능한 코드를 작성하기 위해서는 깊은 복사(Deep Copy)와 얕은 복사(Shallow Copy)의 차이를 명확히 구분할 수 있어야 합니다. 본 글에서는 이 두 개념의 기술적 차이를 메모리 구조 관점에서 분석하고, 실무에서 적용 가능한 최적의 해결책을 제시합니다.
1. 메모리 구조와 데이터 할당의 원리
복사의 개념을 이해하기 위해서는 먼저 자바스크립트(JavaScript)나 파이썬(Python)과 같은 언어가 데이터를 메모리에 저장하는 두 가지 영역, 즉 스택(Stack)과 힙(Heap)에 대해 이해해야 합니다.
1-1. 원시 타입과 스택(Stack)
숫자(Number), 문자열(String), 불리언(Boolean)과 같은 원시 타입(Primitive Type) 데이터는 고정된 크기를 가지며, 메모리의 스택 영역에 값 자체가 직접 저장됩니다. 따라서 변수 간에 할당이 이루어질 때 값이 그대로 복사되어 독립적인 공간을 가지므로, 복사본을 수정해도 원본에는 아무런 영향을 주지 않습니다.
1-2. 참조 타입과 힙(Heap)
반면 객체(Object), 배열(Array), 함수(Function)와 같은 참조 타입은 데이터의 크기가 유동적이므로 힙 영역에 실제 데이터가 저장됩니다. 이때 변수(식별자)가 위치한 스택 영역에는 힙에 저장된 실제 데이터의 메모리 주소(Reference)만 저장됩니다. 얕은 복사와 깊은 복사의 차이는 바로 이 '주소'를 복사하느냐, '실제 값'을 복사하느냐에서 비롯됩니다.
2. 얕은 복사 (Shallow Copy)의 메커니즘과 위험성
얕은 복사는 객체의 실제 데이터가 아닌, 데이터가 저장된 메모리 주소값만을 복사하는 방식입니다. 겉보기에는 별개의 객체처럼 보이지만, 실제로는 하나의 데이터를 두 변수가 동시에 가리키고 있는 상태입니다.
2-1. 얕은 복사가 발생하는 대표적 사례
자바스크립트의 할당 연산자, 전개 구문(Spread Syntax, ...), Object.assign() 등이 얕은 복사를 수행합니다. 특히 중첩된 객체(Nested Object)가 있을 때 문제가 심각해집니다.
// 얕은 복사의 문제점 예시 (JavaScript)
const user = {
name: "Aros",
meta: { age: 30, region: "Seoul" } // 중첩 객체
};
const copyUser = { ...user }; // 전개 구문을 통한 얕은 복사
copyUser.name = "Tistory"; // 1단계 속성 변경 (원본 영향 없음)
copyUser.meta.age = 99; // 2단계 중첩 객체 변경 (원본도 함께 변경됨!)
console.log(user.meta.age); // 출력: 99 (의도치 않은 원본 데이터 훼손)
위 코드에서 볼 수 있듯, user.meta와 copyUser.meta는 여전히 동일한 힙 메모리 주소를 공유하고 있습니다. 따라서 복사본의 데이터를 수정했을 때 원본 데이터까지 변형되는 사이드 이펙트(Side Effect)가 발생하며, 이는 디버깅을 매우 어렵게 만듭니다.
3. 깊은 복사 (Deep Copy)의 구현과 한계
깊은 복사는 메모리 주소가 아닌, 힙 영역에 있는 실제 데이터를 통째로 복제하여 완전히 새로운 메모리 공간을 할당하는 것을 의미합니다. 원본과 복사본은 어떠한 연결 고리도 없으며 완벽하게 독립됩니다.
3-1. JSON 객체 메서드 활용
과거부터 가장 널리 사용되던 방식은 객체를 문자열로 변환(Stringify)했다가 다시 객체로 파싱(Parse)하는 방법입니다.
const deepCopy = JSON.parse(JSON.stringify(original));
- 장점: 구현이 간단하고 별도의 라이브러리가 필요 없습니다.
- 단점: 속도가 느리며,
Date,RegExp,Function,undefined등의 데이터는 문자열 변환 과정에서 유실되거나 변형됩니다.
3-2. 재귀 함수 및 라이브러리 활용
객체의 깊이만큼 재귀적으로 순회하며 복사하는 함수를 직접 구현하거나, Lodash 라이브러리의 _.cloneDeep() 메서드를 사용하면 완벽한 깊은 복사가 가능합니다. 하지만 이는 코드의 복잡도를 높이거나 외부 의존성을 추가해야 한다는 부담이 있습니다.
4. [추천] 최신 표준 기술: structuredClone()
최근 모던 웹 브라우저와 Node.js 환경에서는 structuredClone()이라는 전역 함수를 표준으로 제공합니다. 이는 기존 JSON 방식의 한계를 극복하고, 성능과 안정성을 모두 갖춘 가장 권장되는 깊은 복사 방법입니다.
4-1. structuredClone의 기술적 이점
이 함수는 '구조화된 복제 알고리즘(Structured Clone Algorithm)'을 사용하여 기존 방식들이 해결하지 못한 난제들을 해결합니다.
- 데이터 보존: Date, Set, Map, RegExp, Error 객체 등 복잡한 타입을 원형 그대로 복사합니다.
- 순환 참조 해결: 객체가 자기 자신을 참조하는 순환 구조(Circular Reference)에서도 에러 없이 작동합니다.
- 성능 최적화: 브라우저 내부적으로 최적화되어 있어 재귀 함수보다 효율적입니다.
// structuredClone 사용 예시
const original = {
date: new Date(),
set: new Set([1, 2, 3]),
self: null
};
original.self = original; // 순환 참조
const deepCopy = structuredClone(original);
console.log(deepCopy.date.getFullYear()); // 정상 출력 (JSON 방식에서는 문자열로 변환됨)
console.log(deepCopy.set.has(1)); // true (Set 객체 유지)
1. 얕은 복사는 메모리 주소만 공유하므로 중첩 객체 수정 시 원본 훼손의 위험이 있습니다.
2. 깊은 복사는 데이터를 새로운 메모리에 완전히 복제하여 원본과 완벽한 독립성을 가집니다.
3. JSON 방식의 데이터 유실 문제를 해결한 최신 표준 structuredClone() 사용을 적극 권장합니다.