[Java] 얕은 복사와 깊은 복사
프로그래밍에서 자료구조는 매우 중요하다. 그중 배열은 자료구조의 기초이자 핵심이라고 볼 수 있다.
배열에 대해 더 상세하고 깊게 배우기 위해 얕은 복사와 깊은 복사에 대해 정리해 보겠다.
우선 코드를 보자.
import java.util.Arrays;
public class arrayCopy {
public static void main(String[] args) {
String[] food = {"치킨", "피자"};
String[] shallowCopy = food;
System.out.println("원본 배열: " + Arrays.toString(food));
System.out.println("복사 배열: " + Arrays.toString(shallowCopy));
shallowCopy[1] = "햄버거";
System.out.println("==복사 배열 2번째 요소 변경==");
System.out.println("원본 배열: " + Arrays.toString(food));
System.out.println("복사 배열: " + Arrays.toString(shallowCopy));
}
}
해당 코드는 food가 원본 배열 shallowArray가 복사 배열이다.
shallowArray는 food를 대입받았다.
String[] shallowCopy = food;
해당 복사는 food와 shallowCopy가 서로 다른 배열인 것 같다. 즉, 배열 안에 요소들만 같고 두 배열은 달라 보인다.
하지만 shallowCopy 배열의 [1] 1번 index 요소를 변경하면 food 배열의 요소도 변경된 것을 확인할 수 있다.
복사본 배열의 값을 변경했을 뿐인데 왜 원본 배열의 값도 변경된 걸까?
이유는 얕은 복사(shallow copy)때문이다.
얕은 복사
얕은 복사를 알아가기 전 우리는 Java에서 데이터 타입이 기본형과 참조형으로 나뉜다는 것을 알 수 있다.
참조형은 객체를 생성할 때 객체 주소를 참조하는 값 같은 기본형(int, float, double 등)을 제외한 나머지를 참조형이라 한다.
Java에서는 배열도 참조형이다.
배열이 왜 참조형인가?
1. 객체로 구현되었다.
Java에서 배열이 객체로 구현되었다. Object 클래스에서 상속받은 메서드를 사용할 수 있음.
2. new 연산자로 생성.
클래스를 만들고 객체를 만들 때 우리는 new 키워드를 사용해 객체를 생성했다. 배열도 마찬가지이다. 일반적으로 배열을 만들 때 우리는 int[] arr = new int[10] 이런식으로 만든다.
int[] arr; => 이대로 끝나면 arr의 값은 null이다.
배열을 생성할 때 생성 과정에 대해서도 살펴볼 필요가 있다.
1. 참조 변수 생성
int[] arr;
참조변수를 생성했지만, 메모리에 할당은 안 함.
2. new int[5];
JVM이 배열 객체를 힙 메모리에 생성. 모든 요소 초기화.
3. 참조 연결
arr은 힙 메모리에 생성된 배열 객체의 주소값을 저장하며 이를 통해 배열 요소에 접근 가능.
다시 맨 위 예제 코드에서 복사본 배열의 값을 변경했는데 왜 원본 배열의 값들도 변경된 걸까?
해당 문제는 배열의 참조변수로 알 수 있다.
food와 shallowCopy는 같은 참조값이다. 즉, 메모리에 생성된 한 개의 배열을 참조하고 있다.
그러므로 food 배열과 shallowCopy 배열은 복사된 것이 아닌 그냥 같은 배열을 공유하고 있다고 볼 수 있다.
System.out.println(food);
System.out.println(shallowCopy);
해당 결과는 같은 주소값을 출력한다.
즉 값이 둘 다 바뀐 이유는 얕은 복사 때문이다.
얕은 복사의 정의
얕은 복사는 객체의 주소값만 복사한다.
원본 객체와 복사된 객체가 같은 참조를 공유한다.(참조값이 같음)
참조형 변수들은 동일한 메모리 주소를 가리키게 된다.
얕은 복사의 값 변경
원본 배열과 복사된 배열 모두 동일한 데이터를 공유하므로 복사본 변경 시 원본에도 영향을 준다.
깊은 복사
얕은 복사를 피하기 위해서는 새 배열을 생성하고 원본 배열의 모든 요소값들을 새 배열에 저장해주면 된다.
import java.util.Arrays;
public class DeepCopy {
public static void main(String[] args) {
int[] original = {5,8,10};
int[] deepCopy = new int[original.length]; //원본 배열의 길이만큼 배열 생성.
for(int i=0; i<deepCopy.length; i++){
deepCopy[i] = original[i];
}
System.out.println("==값 확인==");
System.out.println(Arrays.toString(original));
System.out.println(Arrays.toString(deepCopy));
System.out.println("==복사 배열 값 변경==");
deepCopy[0] = 50000;
System.out.println(Arrays.toString(original));
System.out.println(Arrays.toString(deepCopy));
}
}
해당 코드는 원본 배열 크기만큼 복사할 배열을 생성하고, 생성한 복사 배열에 원본 배열의 요소들을 각 각 대입 연산으로 넣어주면 된다.
즉, 기본형 데이터 타입으로 복사한 것이다.
기본형 데이터 타입은 얕은 복사든 깊은 복사든 독립적이다.
위 코드처럼 반복문으로 깊은 복사를 구현할 수 있지만, Java에서는 깊은 복사와 관련된 다양한 메서드들을 제공한다.
1. clone()
2. System.arraycopy(Object src, int srcPos, Object dest, int destPos, int count)
3. Arrays.copyOf()
4. Arrays.copyRange()
package copy;
import java.util.Arrays;
public class DeepCopy {
public static void main(String[] args) {
System.out.println("------clone() 사용------");
int[] original = {5, 8, 10};
int[] deepCopy = original.clone();
deepCopy[2] = -80000;
System.out.println("원본: " + Arrays.toString(original));
System.out.println("복사: " + Arrays.toString(deepCopy));
System.out.println("\n------System.arrayCopy() 사용------");
int[] deepCopy2 = new int[original.length - 1];
System.arraycopy(original, 0, deepCopy2, 0, 2); //원본 배열의 요소 2개만 복사
deepCopy2[1] = 99999990;
System.out.println("원본: " + Arrays.toString(original));
System.out.println("복사: " + Arrays.toString(deepCopy2));
System.out.println("\n------Arrays.copyOf() 사용------");
int[] deepCopy3 = Arrays.copyOf(original, 3);
deepCopy3[0] = 900;
System.out.println("원본: " + Arrays.toString(original));
System.out.println("복사: " + Arrays.toString(deepCopy3));
System.out.println("\n------Arrays.copyRange() 사용------");
int[] deepCopy4 = Arrays.copyOfRange(original, 0, 3);
deepCopy4[2] = -100000;
System.out.println("원본: " + Arrays.toString(original));
System.out.println("복사: " + Arrays.toString(deepCopy4));
}
}
결론
특징 | 얕은 복사 | 깊은 복사 |
복사 대상 | 참조(주소값)만 복사 | 객체의 데이터 자체 복사 |
독립성 | 원본과 복사본이 연결되어 있음 | 원본과 복사본이 독립적임 완전히 다름 |
속도 | 빠름 | 상대적으로 느림 |
메모리 사용 | 적음 | 최소 한 개 더 만드니 더 사용 |
원본에 영향 | 복사본 수정 시 원본 영향 | 복사본 수정 시 원본 영향 없음 |