[JAVA] 얕은 복사 / 깊은 복사

쥬니·2022년 9월 16일
1

공부

목록 보기
1/11

❗ 개인적으로 공부했던 내용을 복습하고 정리하기 위한 글입니다! 따라서 내용이 정확하지 않을 수 있습니다!

참조 자료형

자바에서의 배열은 참조 자료형이다. 참조 자료형은 기본 자료형(실제 값을 기록하는 변수)과는 다르게 객체(인스턴스)의 주소를 기록하는 변수이다! 8개의 기본 자료형(boolean, byte, short, long, int, char, float, double)을 제외하면 모두 참조 자료형이라고 한다.

그런데 왜 하필 참조 자료형이라고 하는 걸까? 참조의 영어, Reference를 검색해 보면...

Reference is a relationship between objects in which one object designates, or acts as a means by which to connect to or link to, another object.
(참조는 한 객체가 다른 객체를 연결하거나 연결하는 수단으로 작용하는 객체 간의 관계다.) 번역기가 없다면 큰일날 뻔 했다.

자바를 배울 때 지긋지긋하게 듣는 단어 "객체", 맨날 현실을 사물(Object = 객체)로 보라고 한다. 그런데 참조는, 그런 객체들을 연결하거나 연결하는 수단인 객체 관계라고 한다. 아직은 감이 잘 안온다.

new 키워드

우리가 자바에서 객체나 배열을 만들 때 아래 예시 처럼 new 키워드를 사용하여 생성한다.

int[] arr = new int[5];
Object o = new Object();


그래서 new 키워드는 뭘 하는 지 잘 모르겠지만, 참조 자료형을 만들어주는 키워드구나! 라고 생각하게 된다.

자바에서 메모리 영역은 크게 세가지가 있다고 배웠다. Static, Stack, Heap영역으로 세가지가 있는데, Static영역에선 static 키워드로 선언한 변수들에게 메모리 공간이 할당되고, 프로그램 실행 시작부터 종료까지 살아있다. Stack영역은 LIFO구조고, 호출될 때 할당, 호출이 종료되면 할당이 해제된다. 또한 기본 자료형들이 해당 공간에 할당된다고 한다. Heap영역은 동적 메모리 할당 공간이다. 우리가 만드는 참조 자료형들이 해당 공간을 할당한다.

오 그러면 자바에서는 참조 자료형은 new 키워드를 사용하여 만드니, 참조 자료형은 동적으로 메모리를 할당받아 Heap공간에 생성되는 구나!

그런데, 할당만 한 공간을 어떻게 쓰는 걸까 하는 생각이 들었다. 우리는 참조라는 의미를 다시 생각해보아야 한다. 참조는 한 객체가 다른 객체를 연결하거나 연결하는 수단으로 작용하는 객체 간의 관계, 라는데.... 여기거 우리가 주목해야 할 단어는 연결 이라고 생각한다.

자바에서 new 키워드를 사용하여 Heap영역의 메모리를 할당하여 참조 자료형(배열, 객체 등)을 만든다. 또한 Stack영역의 참조 변수를 통해 Heap 영역에 할당된 메모리를 참조한다. 따라서 Stack 영역에는 배열이나 객체의 시작 주소가 저장되고, 아래와 같이 출력하였을 때 주소값이 반환되는 걸 확인할 수 있다.

int[] arr = new int[5];	
Systme.out.println(arr);	// 객체의 시작 주소가 출력된다!!

즉 Stack 영역에는 주소값이, Heap영역에는 참조 자료형 데이터가 저장되는데, 이걸 Stack영역에서 참조하여 사용하기 때문에 참조 자료형인 것이다.

얕은 복사(Shallow Copy)

위의 설명이 너무 길어졌는데, 얕은 복사와 깊은 복사를 설명하기에 꼭 필요한 설명인 것 같아서 작성했다. 얕은 복사, 깊은 복사를 가장 보기 쉬은 코드는 개인적으로 배열이라고 생각한다! 물론 객체에서도 빈번히 이런 복사에 관한 문제가 발생한다. 우선 얕은 복사의 정의는 다음과 같다.

실제 값이 아닌 주소값을 복사하는 것

우선 origin배열을 생성하고, 해당 배열을 copy라는 이름의 배열에 대입하였다. 즉 origin이라는 배열을 copy배열에 복사한 것이다.

int[] origin = {1, 2, 3, 4, 5};

System.out.println("-- 원본 배열 출력 --");

for(int i = 0; i < origin.length; i++) {
		System.out.print(origin[i] + " ");
}

System.out.println("\n-- 복사 배열 출력 --");

int[] copy = origin;

for(int i = 0; i < copy.length; i++) {
	System.out.print(copy[i] + " ");
}

결과는 다음과 같다.

-- 원본 배열 출력 --
1 2 3 4 5 
-- 복사 배열 출력 --
1 2 3 4 5 

이 출력만 보면 복사가 잘 되었거니, 하고 생각이 된다!

그런데 말입니다, 여기서 origin[2]의 값을 99로 바꾸면 어떻게 될까?

origin[2] = 99;
System.out.println("\n-- 원본 배열 출력 --");
for(int i = 0; i < origin.length; i++) {
	System.out.print(origin[i] + " ");
}

System.out.println("\n-- 복사본 배열 출력 --");
for(int i = 0; i < copy.length; i++) {
		System.out.print(copy[i] + " ");
}

놀랍게도 결과는 다음과 같다.

-- 원본 배열 출력 --
1 2 99 4 5 
-- 복사본 배열 출력 --
1 2 99 4 5 

분명 나는 origin[2]만을 변경하였는데, copy[2]번도 같이 변경이 되었다. 이것이 얕은 복사이다. 얕은 복사는 배열이나 객체의 주소값만을 복사한다. 위에 있던 배열 복사 코드에서는, 배열의 시작 주소를 가지고 있는 배열의 이름을 대입했기 때문에 얕은 복사가 일어나서, 원본 배열의 값을 변경하면 같은 주소를 참조하고 있던 복사본의 배열의 값이 같이 변하게 된 것 처럼 보인다.

이는 배열의 이름을 출력해보면 확인할 수 있다.

System.out.println("원본 배열의 해시코드 : " + origin.hashCode() );
System.out.println("복사본 배열의 해시코드 : " + copy.hashCode() );
원본 배열의 해시코드 : 523429237
복사본 배열의 해시코드 : 523429237

그런데 우리가 실제로 개발할 때에는 이런 얕은 복사 말고, 배열을 통쨰로 복사해야 하는 경우가 있다. 그럴 때 사용하는 게 바로 깊은 복사이다.

깊은 복사(Deep Copy)

깊은 복사의 정의는 다음과 같다.

깊은 복사는 동일한 새로운 배열을 하나 생성해서 실제 내부값까지 복사하는 것이다

자바에서 깊은 복사를 하는 방법에는 여러가지가 있는데 먼저 반복을 통해 배열의 인덱스를 하나하나 복사하는 것이다.

반복을 통한 깊은 복사

int[] origin = {1, 2, 3, 4, 5};

int[] copy = new int[origin.length];
		
for(int i = 0; i < copy.length; i++) {
	copy[i] = origin[i];
}
		
origin[2] = 88;

System.out.println("-- 원본 배열 출력 --");
for(int i = 0; i < origin.length; i++) {
		System.out.print(origin[i] + " ");
}
		
System.out.println("\n 복사 배열 출력 --");
for(int i = 0; i < copy.length; i++) {
		System.out.print(copy[i] + " ");
}
		
System.out.println("\n원본 배열의 해시코드 : " + origin.hashCode() );
System.out.println("복사본 배열의 해시코드 : " + copy.hashCode() );
-- 원본 배열 출력 --
1 2 88 4 5 
 복사 배열 출력 --
1 2 3 4 5 
원본 배열의 해시코드 : 523429237
복사본 배열의 해시코드 : 664740647

위와 같이 배열의 크기만큼 반복문을 돌면서 각각의 인덱스에 저장된 값을 복사하니, 원본 배열만 변환된 것을 볼 수 있다.

arraycopy() 메소드를 이용

int[] origin = {1, 2, 3, 4, 5};
		
int[] copy = new int[5];
		
System.arraycopy(origin, 0, copy, 0, 5);

origin[2] = 99;
System.out.println("-- 원본 배열 출력 --");
for(int i = 0; i < origin.length; i++) {
	System.out.print(origin[i] + " ");
}
		
System.out.println("\n--- 복사본 출력 ---");
for(int i = 0; i < copy.length; i++) {
	System.out.print(copy[i] + " ");
}
		
System.out.println("\n원본 배열의 해시코드 : " + origin.hashCode() );
System.out.println("복사본 배열의 해시코드 : " + copy.hashCode() );
-- 원본 배열 출력 --
1 2 99 4 5 
--- 복사본 출력 ---
1 2 3 4 5 
원본 배열의 해시코드 : 523429237
복사본 배열의 해시코드 : 664740647

해당 메소드는
1. 새로운 배열을 생성한 후
2. System 클래스에서의 arraycopy() 호출 한다

System.arraycopy(원본배열이름, 원본배열에서 복사를 시작할 인덱스, 복사본배열이름, 복사본배열에서 복사가 시작될 인덱스, 복사할 갯수);

이 메소드의 장점은 몇 번 인덱스부터 몇 개를 어느 위치부터 넣을건지 직접 지정이 가능하다는 점이다.

copyOf() 메소드를 이용

위의 arraycopy()를 사용해도 좋지만, 해당 메소드는 필요로하는 매개변수가 많다. 다행히도 copyOf()메소드를 사용하면, 위 방법보다 더 간단하게 깊은 복사를 할 수 있다.

int[] origin = {1, 2, 3, 4, 5};
        
int[] copy = Arrays.copyOf(origin, 5);
		
origin[2] = 99;
System.out.println("==원본 배열 출력 ==");
for(int i = 0; i < origin.length; i++) {
	System.out.print(origin[i] + " ");
}
        
System.out.println("\n== 복사 출력 ==");
for(int i = 0; i < copy.length; i++) {
	System.out.print(copy[i] + " ");
}
        
System.out.println("\n원본 배열의 해시코드 : " + origin.hashCode() );
System.out.println("복사본 배열의 해시코드 : " + copy.hashCode() );
==원본 배열 출력 ==
1 2 99 4 5 
== 복사 출력 ==
1 2 3 4 5 
원본 배열의 해시코드 : 523429237
복사본 배열의 해시코드 : 664740647

copyOf()메소드는 무조건 배열의 0번부터 복사를 진행한다!

clone() 메소드 이용

그런데, 위의 copyOf()도 귀찮다고 하는 사람이 분명 있을 것이다. 매개변수를 두개나 써야 하잖아... 하고, 귀찮아하는 사람들을 위한 메소드가 있다.

바로 clone() 메소드를 이용하는 방법이다.

int[] origin = {1, 2, 3, 4, 5};
		
int[] copy = origin.clone();
		
origin[2] = 99;
System.out.println("==원본 배열 출력 ==");
for(int i = 0; i < origin.length; i++) {
	System.out.print(origin[i] + " ");
}
		
System.out.println("\n== 복사 출력 ==");
for(int i = 0; i < copy.length; i++) {
	System.out.print(copy[i] + " ");
}
		
System.out.println("\n원본 배열의 해시코드 : " + origin.hashCode() );
System.out.println("복사본 배열의 해시코드 : " + copy.hashCode() );
==원본 배열 출력 ==
1 2 99 4 5 
== 복사 출력 ==
1 2 3 4 5 
원본 배열의 해시코드 : 523429237
복사본 배열의 해시코드 : 664740647

이 메소드는 아주 간단히 복사본배열이름 = 원본배열이름.clone(); 만 하면 된다. 인덱스 지정을 할 필요도, 갯수를 지정할 필요도 없고, 원본 배열과 완전히 똑같은 배열을 깊은 복사를 통해 만들어 낸다.

귀찮다고 다 나쁜 건 아니다! arraycopy() 메소드는 매개변수를 많이 적어야해서 조금 귀찮은 대신, 몇 번 인덱스부터 몇 개를 어느 위치부터 넣은건지 직접 지정 가능하다. 코딩할 때 정답은 없다고 한다. 위의 깊은 복사에도 방법이 여러가지가 있는 것처럼, 코드를 작성할 때 적재적소에 맞게 잘 사용하는 것이 중요하다고 생각한다.

마치며...

얕은 복사와 깊은 복사를 설명하다보니, 참조 자료형부터 메모리 영역까지 조금 씩 설명을 한 것 같다. 처음 블로그에 공부한 것을 정리하다 보니 미숙한 점도 많았지만 개인 정리도 되고, 복습이 되는 것 같아 유용했다! 혹시 댓글 달아주시는 분들이 있을지는 모르겠지만, 틀린 내용이 있다면 언제든 말씀 부탁드립니다! 😁
끝!

0개의 댓글