알고리즘 구현 문제를 풀다보면, 나는 값만 파라미터로 넘겼는데 배열의 원본 값이 바뀌는 경우가 있다.
이런 문제를 해결해 내 것으로 만들기 위해 얕은 복사와 깊은 복사를 알아보자.
얕은 복사
얕은 복사는 객체의 주소값을 복사하는 방식이다. 즉, 해당 객체는 원본 객체를 참조하게 된다.
기본 타입은 값이 복사되지만, 참조 타입은 주소값이 복사된다. ➡️ 자바에서 타입을 나눈 이유
int[] arr1 = new int[4];
arr1[0] = 1;
arr1[1] = 2;
arr1[2] = 3;
arr1[3] = 4;
int[] arr2 = arr1;
arr2[0] = 4;
System.out.println(Arrays.toString(arr1));
System.out.println(Arrays.toString(arr2));
우리의 의도는 `arr1`을 변경하지 않고 `arr2`를 [1, 2, 3, 4]로 초기화하고 `arr2`의 0번 index값만 4로 바꾸는 것이다.
실제로 값을 출력해 보면 다음과 같다.
의도와는 다르게 `arr1`의 0번 index값이 바뀌었다.
다른 예시를 보자.
public static void main(String[] args) throws IOException {
int[] origin = new int[4];
origin[0] = 1;
origin[1] = 2;
origin[2] = 3;
origin[3] = 4;
int[] result = function(origin);
System.out.println("origin: " + Arrays.toString(origin));
System.out.println("result: " + Arrays.toString(result));
}
private static int[] function(int[] origin) {
for (int i = 0; i < origin.length; i++) {
origin[i] += 1;
}
return origin;
}
우리의 의도는 ` origin`배열의 모든 요소에 1을 더한 배열을 `result`에 할당하고 싶다. 이후 `origin`을 사용해야 하므로 `origin`의 변경은 없어야 한다. 실제 값을 출력하면 의도와는 다르게 동작하는걸 볼 수 있다.
자바는 Call by Value 방식이라고 하는데, 위 예시를 보면 Call by Reference 방식으로 동작한다고 볼 수 있다.
하지만 흔히 자바는 Call by Value 방식은 거짓말일까?
자바의 Call by Value
자바는 분명히 Call by Value로 동작한다. 기본 타입의 값과 참조 타입의 값은 스택 메모리에 저장된다.
`int number = 10;`이라는 코드는 스택 메모리에 `number: 10`이라는 형태로 한 공간에 저장된다.
`int[] origin = new int[4];`라는 코드도 스택 메모리에 `origin: 0x1234`라는 형태로 한 공간에 저장된다.
여기서 0x1234는 힙 메모리에 저장된 실제 origin 객체(크기가 4인 int배열)의 주소이다.
자바는 메서드에 파라미터를 전달할 때 기본 타입의 경우 실제 값을, 참조 타입의 경우 주소값을 복사한다. 하지만 위 예시에서 origin의 주소값을 복사해 전달했기 때문에 function 메서드에서도 실제 origin 객체로 접근하는 것이다. 연산 후, origin의 주소값을 반환하기 때문에 result도 origin을 가리키게 된다.
깊은 복사
깊은 복사는 객체의 모든 값을 새로운 메모리 공간에 복사하는 방식이다.
복사본과 원본은 서로 독립적인 객체다.
첫번째 예시의 깊은 복사
int[] arr1 = new int[4];
arr1[0] = 1;
arr1[1] = 2;
arr1[2] = 3;
arr1[3] = 4;
// 깊은 복사 - 방법 1: clone() 사용
int[] arr2 = arr1.clone();
// 또는 방법 2: Arrays.copyOf() 사용
// int[] arr2 = Arrays.copyOf(arr1, arr1.length);
// 또는 방법 3: System.arraycopy() 사용
// int[] arr2 = new int[arr1.length];
// System.arraycopy(arr1, 0, arr2, 0, arr1.length);
arr2[0] = 4;
System.out.println(Arrays.toString(arr1)); // [1, 2, 3, 4] 출력
System.out.println(Arrays.toString(arr2)); // [4, 2, 3, 4] 출력
두번째 예시의 깊은 복사
public static void main(String[] args) throws IOException {
int[] origin = new int[4];
origin[0] = 1;
origin[1] = 2;
origin[2] = 3;
origin[3] = 4;
int[] result = function(origin); // 이제 function은 원본을 변경하지 않음
System.out.println("origin: " + Arrays.toString(origin)); // [1, 2, 3, 4] 출력
System.out.println("result: " + Arrays.toString(result)); // [2, 3, 4, 5] 출력
}
private static int[] function(int[] origin) {
// 깊은 복사를 통해 원본 배열 보존
int[] copy = origin.clone();
// 복사본만 수정
for (int i = 0; i < copy.length; i++) {
copy[i] += 1;
}
// 수정된 복사본 반환
return copy;
}
깊은 복사 하는 메서드는 따로 설명하지 않겠다. gpt, claude나 다른 블로그에서 다루는 글이 많으니 참고하고 한 가지 주의사항만 알고 가자.
⚠️ `clone()` 메서드 주의사항
`clone()`는 배열에 대해 깊은 복사를 진행한다. 즉, 새로운 배열 객체가 생성된다.
하지만 배열의 내부에 참조 타입 객체가 있는 경우, 해당 객체의 주소값만 복사된다. ➡️ 배열의 각 객체에 대해선 얕은 복사처럼 동작한다.
'Java' 카테고리의 다른 글
[Java] 동일성과 동등성에 관하여 (0) | 2025.03.17 |
---|---|
[Java] equals()와 hashCode()에 관하여 (0) | 2025.03.14 |
[Java] 일급 컬렉션이란? (1) | 2025.03.08 |
[Java] Checked Exception과 Unchecked Exception (1) | 2025.03.07 |