Java

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

jeong_ii 2025. 1. 15. 20:21

객체를 복사하는 방식에는 깊은 복사(Deep Copy)와 얕은 복사(Shallow Copy) 두 가지가 있다.

 

🔹 깊은 복사(Deep Copy)

  • 새로운 메모리 공간(Heap 영역)에 실제 값을 복사한다.
  • 따라서, 원본 객체와 복사된 객체는 서로 독립적이며, 한쪽을 수정해도 다른 객체에 영향을 주지 않는다.

🔹 얕은 복사(Shallow Copy)

  • 참조 값만 복사하므로, 원본 객체와 복사된 객체가 동일한 메모리 공간을 참조한다.
  • 즉, 복사된 객체를 수정하면 원본 객체도 함께 변경된다.

 

다음과 같이 이름(name), 나이(age)를 가지는 Person 클래스가 있다고 해보자.

public class Person {
    String name;
    int age;
    
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    public String getName() {
        return name;
    }
    
    public void setName(String name) {
        this.name = name;
    }
    
    public int getAge() {
        return age;
    }
    
    public void setAge(int age) {
        this.age = age;
    }
}

 

[ 얕은 복사(Shallow Copy) ]

void shallowCopy() {
    Person person = new Person("홍길동", 24);
    Person copyPerson = person;  // 얕은 복사 (참조 값만 복사)

    copyPerson.setName("김민수");
    copyPerson.setAge(26);
}

이름이 "홍길동", 나이가 24세인 person 인스턴스를 생성한 후, 이를 copyPerson에 복사한다.

그리고 copyPerson의 이름을 "김민수", 나이를 26세로 변경한다.

 

이를 출력해 보면,

System.out.println(person); // 김민수, 26
System.out.println(copyPerson); // 김민수, 26

person과 copyPerson 모두 "김민수", 26세로 변경되어 있다.

 

그 이유는 Person copyPerson = person; 코드에서 person의 참조 값만 copyPerson에 복사되었기 때문이다.

즉, 두 변수는 같은 객체를 가리키고 있으며, 한쪽을 변경하면 다른 쪽에도 영향을 미친다.

 

메모리 공간을 그림으로 보면 다음과 같다.

 

person 인스턴스를 생성하면 , Stack 영역에는 참조 값이 저장되고 Heap 영역에는 실제 Person 객체가 생성된다.

그리고 copyPerson에 person을 할당하면, copyPerson 역시 동일한 Heap 영역의 객체를 참조하게 된다.

 

여기서 copyPerson의 값을 수정한다면,

person과 copyPerson은 같은 객체를 참조하고 있기 때문에, 원본 객체(person)에도 영향을 미치게 된다.

 

원본 객체를 유지하고 싶다면 깊은 복사(Deep Copy)를 해야 한다.

 

[ 깊은 복사(Deep Copy) ]

깊은 복사(Deep Copy)를 구현하는 방법에는 대표적으로 세 가지가 있다.

  • 생성자나 팩토리 메서드를 이용한 복사
  • 새로운 객체를 직접 생성하여 복사
  • Cloneable 인터페이스를 구현해 clone() 메서드를 오버라이딩하여 복사

 

🔸 생성자나 팩토리 메서드를 이용한 복사

// 복사 생성자
public Person(Person person) {
    this.name = person.name;
    this.age = person.age;
}

// 복사 팩토리
public static Person copyFactory(Person person) {
    return new Person(person.name, person.age);
}

🔸 새로운 객체를 직접 생성하여 복사

void deepCopy() {
    Person person = new Person("홍길동", 24);
    Person copyPerson = new Person(person.getName(), person.getAge());
}

이를 출력해보면 다음과 같다.

copyPerson.setName("김민수");
copyPerson.setAge(26);

System.out.println(person); // 홍길동, 24
System.out.println(copyPerson); // 김민수, 26

이처럼 깊은 복사를 사용하면 원본 객체와 독립적인 새로운 객체가 생성되므로, 한쪽을 수정해도 다른 객체에 영향을 주지 않는다. 

 

메모리 공간을 그림으로 보면 다음과 같다.

 

🔸 Cloneable 인터페이스를 구현해 clone() 메서드를 오버라이딩하여 복사

Cloneable 인터페이스 자체에는 메서드가 없지만, 이를 구현하면 Object의 clone() 메서드를 사용할 수 있다.

public class Person implements Cloneable {
    private String name; 
    private int age;
    
    public Person() {}
    
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    @Override
    protected Person clone() throws CloneNotSupportedException {
    	return (Person) super.clone();
    }
    
    public String getName() {
    	return name;
    }
    
    public void setName(String name) {
    	this.name = name;
    }
    
    public int getAge() {
    	return age;
    }
    
    public void setAge(int age) {
    	this.age = age;
    }
}
Person person = new Person("홍길동", 24);
Person copy = person.clone();

copy.setName("김민수");
copy.setAge(26);

System.out.println(person); // 홍길동, 24
System.out.println(copy); // 김민수, 26

다른 방법들과 마찬가지로, copy 인스턴스의 값을 변경해도 원본 person 인스턴스에는 영향을 주지 않는다.

 

🔍 Effective Java 13장에서는 clone 재정의는 주의해서 진행하라 라는 아이템이 있다. 대략적인 내용은 다음과 같다.

Cloneable 인터페이스는 복제해도 되는 클래스임을 명시하는 용도의 믹스인 인터페이스지만, 아쉽게도 의도한 목적을 제대로 이루지 못했다. 여기서 큰 문제점은 clone 메서드가 선언된 곳이 Cloneable이 아닌 OBject이고, 그 마저도 protected이다. 그래서 Cloneable을 구현하는 것만으로는 외부 객체에서 clone 메소드를 호출할 수 없다. 리플렉션을 사용하면 가능하지만, 100% 성공하는 것도 아니다. 
이러한 여러 문제점을 가진 인터페이스이지만, Cloneable 방식은 널리 쓰이고 있어서 잘 알아두는 것이 좋다.
 
Cloneable이 몰고 온 모든 문제를 되짚어봤을 때, 새로운 인터페이스를 만들 때는 절대 Cloneable을 확장해서는 안 되며, 새로운 클래스도 이를 구현해서는 안된다. final 클래스라면 Cloneable을 구현해도 위험이 크지는 않지만, 성능 최적화 관점에서 검토한 후 별다른 문제가 없을 때만 드물게 허용해야 한다.
 
기본 원칙은 '복제 기능은 생성자와 팩터리를 이용하는게 최고' 라는 것이다.
단, 배열만은 clone 메소드 방식이 가장 깔끔한, 이 규칙의 합당한 예외라 할 수 있다.