프로그래밍 언어/[JAVA]

[JAVA] 얕은 복사(Shallow Copy)와 깊은 복사(Deep Copy)에 대하여

연구소장 J 2023. 2. 14. 22:01

얕은 복사(Shallow Copy)란?

public class Human {

    String name;
    int age;

    public Human(){
    }

    public Human(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;
    }
}

 

위와 같은 Human 클래스가 있다고 하자. 이 클래스를 다음과 같이 복사한 뒤 멤버 값을 변경해 보자.

 

public static void main(String[] args) {
        Human human = new Human("Jackson", 31);
        Human copyHuman = human;

        copyHuman.setName("Bob");

        System.out.println(human.getName());
        System.out.println(copyHuman.getName());

    }

 

출력값:

 

얕은 복사
[그림 1] 얕은 복사

 

copyHuman = human과 같이 객체를 복사한 뒤 값을 변경시킨 결과는 위와 같다.

개발자의 의도는 복사한 copyHuman의 값만 변경하는 거였을지라도, 원본인 human의 값도 변경되어 버린다.

 

이는 copyHuman = human과 같은 대입이 실제 값이 아닌 주소를 대입하는 것이기 때문이다.

실제로 두 값의 주소를 아래와 같이 출력해 보면 같은 주소임을 알 수 있다.

 

public static void main(String[] args) {
        Human human = new Human("Jackson", 31);
        Human copyHuman = human;

        copyHuman.setName("Bob");

        System.out.println(human);
        System.out.println(copyHuman);

    }

얕은 복사는 주소값을 대입한다
[그림 2] 같은 주소 값

 

 

이처럼 얕은 복사(Shallow Copy)는 실제 값이 아닌, 값을 가리키는 "주소"를 복사해 같은 객체를 가리키는 것을 뜻한다.

그림으로 쉽게 나타내면 다음과 같다.

 

얕은 복사의 모습
[그림 3] 얕은 복사의 모습

 

Human 인스턴스는 Heap 영역에 생성되고 이를 가리키는 human 변수가 Stack 영역에 생성된다.

이때 copyHuman가 human을 얕은 복사 하면 같은 주소값의 인스턴스를 가리키게 된다.

 

얕은 복사의 부작용
[그림 4] 얕은 복사의 부작용

 

따라서 copyHuman에서 setName() 메서드를 호출해 값을 변경하면, human의 name 값도 변경되는 것이다.

 

이러한 상황을 방지할 수 있는 복사 방식이 바로 깊은 복사이다.

 

 

깊은 복사(Deep Copy)란?

깊은 복사는 주소값이 아닌 실제 값을 복사하는 것을 뜻한다.

 

이러한 깊은 복사는 주로 Cloneable 인터페이스 구현, 복사 생성자, 복사 팩토리 이용 등을 이용할 수 있다.

 

 

1) Cloneable 인터페이스 구현

 

Cloneable 인터페이스의 clone() 메서드를 구현하여 깊은 복사를 진행하는 방식이다. 

 

이펙티브 자바의 저자 조슈아 블로크는 해당 저서에서 clone() 메서드의 재정의를 주의해서 진행하라고 했다.

더 자세한 내용을 알고 싶다면 다음 블로그에 잘 정리되어 있으니 참고하자

 

 

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

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

jackjeong.tistory.com

 

결론은 배열을 제외하고는 생성자와 팩토리를 이용하는 방식이 더 낫다는 것이다. 

 

2) 생성자, 팩터리 이용

 

public class Human {

    String name;
    int age;

    public Human(){
    }

    /*복사 생성자*/
    public Human(Human original){
        this.name = original.getName();
        this.age = original.getAge();
    }

    /*복사 팩터리*/
    public static Human copy(Human original){
        Human copyObject = new Human();
        copyObject.name = original.getName();
        copyObject.age = original.getAge();
        return copyObject;
    }
    
    public Human(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;
    }
}

 

위와 같이 복사 생성자와 복사 팩터리 메서드를 클래스에 추가하자.

 

public static void main(String[] args) {
        Human human = new Human("Jackson", 31);
        Human copyHumanUsingConstructor = new Human(human);
        Human copyHumanUsingFactory = Human.copy(human);

        copyHumanUsingConstructor.setName("Bob");
        copyHumanUsingFactory.setName("IU");

        System.out.println(human.getName());
        System.out.println(copyHumanUsingConstructor.getName());
        System.out.println(copyHumanUsingFactory.getName());

    }

 

이후 위와 같은 코드를 실행하면 결과는 어떨까?

 

깊은 복사의 결과
[그림 5] 깊은 복사 결과

 

원본인 human을 복사 생성자를 이용해 깊은 복사한 copyHumanUsingConstructor의 값은 "Bob"으로, 

복사 팩터리 메서드를 이용해 깊은 복사한 copyHumanUsingFactory의 값은 "IU"로 잘 바뀌었으며,

원본인 human의 값은 그대로 "Jackson"인 것을 확인할 수 있다.

 

public static void main(String[] args) {
        Human human = new Human("Jackson", 31);
        Human copyHumanUsingConstructor = new Human(human);
        Human copyHumanUsingFactory = Human.copy(human);

        copyHumanUsingConstructor.setName("Bob");
        copyHumanUsingFactory.setName("IU");

        System.out.println(human);
        System.out.println(copyHumanUsingConstructor);
        System.out.println(copyHumanUsingFactory);

    }

 

실제로 세 값의 주소를 출력해 보면 다음과 같다.

깊은 복사는 주소값이 다르다
[그림 6] 깊은 복사 주소값

 

세 값의 주소가 모두 다른 것을 확인할 수 있다.

이를 그림으로 나타내면 다음과 같다.

 

깊은 복사
[그림 7] 깊은 복사

 

깊은 복사는 Heap 영역의 같은 인스턴스를 가리키지 않는다. Heap 영역에 새로운 인스턴스를 생성하고 해당 인스턴스에 값을 복사하는 방식으로 동작한다. 

 

따라서 복제된 값을 변경해도 원본이 바뀌는 부작용이 발생하지 않는다.

 

 

 

반응형