본문 바로가기
Java/Java Story

[Java] Shallow copy(얕은 복사) vs Deep copy(깊은 복사)

by 잭피 2020. 11. 30.

 

 

 

안녕하세요~ 잭코딩입니다!

 

이번 내용에서는 Shallow copy(얕은 복사)와 Deep copy(깊은복사)를 살펴봅시다

 


 

코드를 짜다보면 객체를 복사해야할 경우가 생깁니다

이 때 실수로 복사를 잘못하면 큰 이슈가 생길 수 있습니다

Shallow copy(얕은 복사) vs Deep copy(깊은 복사)

Shallow copy(얕은 복사)는 '주소값'을 복사합니다 (즉, 참조하고 있는 실제값은 같습니다)

Deep copy(깊은 복사)는 '실제값'을 새로운 메모리 공간에 복사합니다 (즉, 실제값이 다릅니다)

 

한번 코드와 그림을 통해 알아볼까요?

 

우선 이름과 돈을 가진 JackCoding 클래스를 만듭니다

public class JackCoding {
  String name;
  long money;

  public JackCoding(String name, long money) {
    this.name = name;
    this.money = money;
  }

  public void changeName(String name) {
    this.name = name;
  }
  
  public void spend(int money) {
    this.money -= money;
  }
}

 

1. Shallow copy(얕은 복사)

@Test
void shallowCopy() {

    JackCoding jack = new JackCoding("jack", 10000);
    JackCoding jackCopy = jack;

    jack.changeName("coding"); 
    jack.spend(3000);
  }

name : jack, money : 10000원을 가진 JackCoding 객체의 인스턴스 jack을 생성합니다

그리고 jackCopy에 이 인스턴스를 복사해둡니다

 

jack은 이름을 coding으로 개명하고 그 비용으로 3000원을 지출합니다

 

그러면 jack은 이름이 coding이고 가진 돈은 7000원,

jackCopy는 이름이 jack이고 가진 돈은 10000원이 있을거라 생각할 수 있습니다

하지만 결과는 아래처럼 두 인스턴스 모두 JackCoding{name='coding', money=7000}라는 값을 가지고 있습니다

System.out.println(jack); // JackCoding{name='coding', money=7000}
System.out.println(jackCopy); // JackCoding{name='coding', money=7000}

그 이유는 위에서 jackCopy는 값을 복사한게 아니고 참조값을 복사했습니다

그림을 통해 볼까요?

JackCoding jack = new JackCoding("jack", 10000);
JackCoding jackCopy = jack;

먼저 jack 인스턴스를 생성하면 stack에 참조값, heap에 실제값이 올라갑니다

그리고 jackCopy 인스턴스를 생성할 때는 jack의 참조값을 저장합니다 

아래의 그림과 같습니다

 

jack, jackCopy는 동일한 값을 참조

 

즉, jackCopy는 Heap에 따로 값을 할당받은게 아닌 jack의 값을 같이 참조하고 있습니다

여기서 jack의 값을 수정해볼까요?

jack.changeName("coding"); 
jack.spend(3000);

 

 

jack과 jackCopy 인스턴스가 참조하는 실제값이 수정되었습니다

따라서 jackCopy는 jack의 값을 참조하고 있으니 함께 바뀐 값을 참조합니다 

 

하지만 코드를 짜면서 참조값이 아닌 실제값을 복사해야 하는 경우가 있을 것입니다

실제값을 복사하려면 Deep copy(깊은 복사)를 해야합니다

 

2. 깊은 복사(Deep copy)

객체의 깊은 복사를 구현하는 방법은 여러가지가 있습니다

글쓴이는 주로 3가지 방법을 사용했습니다

 

1) 복사 생성자 또는 복사 팩터리를 이용하여 복사합니다
2) 직접 객체 생성하여 복사합니다 

3) Cloneable을 구현하여 clone() 재정의 

하지만 3) clone() 재정의는 final 인스턴스 또는 배열이 아닌 경우 사용을 권하지 않는다고 합니다

 

clone() 재정의 경우는 주의해서 진행해야합니다
자세한 내용은 아래 링크를 통해 참고해주세요
jackjeong.tistory.com/m/30
 

[이펙티브자바 3판] ITEM13. clone 재정의는 주의해서 진행하라

이번장의 핵심은... 새로운 인터페이스를 만들 때는 절대 Cloneable을 확장해서는 안되며, 새로운 클래스도 이를 구현해서는 안 된다. final 클래스라면 Cloneable을 구현해도 위험이 크지 않지만, 성능

jackjeong.tistory.com

 
따라서 cloneable을 구현하여 clone()을 재정의하기보다는 복사 생성자나 복사 팩터리를 이용하여 Deep copy하는게 좋습니다

1. 복사생성자 or 복사팩터리

public class Jackcoding {

   // 복사 생성자
	public Jackcoding(Jackcoding jackcoding) {
		this.name = jackcoding.name;
		this.momey = jackcoding.momey;
	}

	// 복사 팩터리
	public static Jackcoding newInstance(Jackcoding jackcoding) {
		Jackcoding j = new Jackcoding();
		j.name = jackcoding.name;
		j.momey = jackcoding.momey;
		return j;
	}
}

 

2. 직접 객체를 생성하여 복사 

@Test
void deepCopy() {
  JackCoding jack = new JackCoding("jack", 10000);
  JackCoding jackCopy = new JackCoding();
  jackCopy.setName(jack.getName());
  jackCopy.setMoney(jack.getMoney());

  jack.changeName("coding");
  jack.spend(3000);
}

 

3. Cloneable을 구현하여 clone() 재정의
(final 인스턴스 또는 배열이 아닌 경우 추천하지 않습니다)

public class JackCoding implements Cloneable {
  
  String name;
  long money;
	
  ... 

  @Override
  protected JackCoding clone() throws CloneNotSupportedException {
    return (JackCoding) super.clone();
  }
@Test
void deepCopy() throws CloneNotSupportedException {
    JackCoding jack = new JackCoding("jack", 10000);
    JackCoding jackCopy = jack.clone();

    jack.changeName("coding");
    jack.spend(3000);
  }

이제 결과를 보면 실제값이 복사된 것을 확인할 수 있습니다

jack은 JackCoding{name='coding', money=7000}

jackCopy는 JackCoding{name='jack', money=10000}

System.out.println(jack); // JackCoding{name='coding', money=7000}
System.out.println(jackCopy); // JackCoding{name='jack', money=10000}

한번 그림을 통해 볼까요?

JackCoding jack = new JackCoding("jack", 10000);
JackCoding jackCopy = jack.clone();

실제 jack 인스턴스 참조값이 참조하고 있는 실제값을 Heap 영역에 복사합니다

그리고 jackCopy는 복사한 값의 참조값을 가집니다

 

 

jack 인스턴스의 실제값을 복사

 

 

이제 jack 인스턴스의 실제값을 바꿔볼까요?

jack.changeName("coding");
jack.spend(3000);

 

아래 그림처럼 jack의 참조값이 참조하고 있는 값을 변경하더라도 jackCopy는 관심도 없습니다

jackCopy의 참조값이 참조하는 값은 jack 인스턴스와는 관련없는 독립된 값이기 때문입니다

 

 

 

 

이렇게 shallow copy와 deep copy에 대해 알아보았습니다

감사합니다

댓글