[Java] Shallow copy, Deep copy, clone() 메소드

clean·2024년 1월 16일
0
post-thumbnail

Java에서 참조 자료형이 저장되는 방식

java에서 new 연산자를 이용해 만들어지는 참조 자료형(ex. 인스턴스, 배열, 컬렉션 등)은 실제 객체는 Heap 영역에, 그리고 그 객체에 접근할 수 있는 참조 변수는 stack 영역 또는 method 영역에 저장이 되게 된다.


출처: https://kotlinworld.com/3

이러한 특성 때문에, 참조 자료형을 복사할 때 두가지 방식으로 복사가 가능해졌는데 첫번째는 두 변수가 같은 주소값을 참조하는 Shallow Copy(얕은 복사), 두번째는 아예 객체의 값을 복사하는 Deep Copy(깊은 복사)이다.

Copy의 종류

Shallow Copy(얕은 복사)

얕은 복사는 두 참조 변수가 같은 객체를 참조하도록 복사를 한 것이다. 즉 그림으로 표현하면 다음과 같다.

출처: https://developer-talk.tistory.com/86

다음 코드를 보자.

Person.java

package copy;

public class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "이름: " + getName() + "   나이: " + getAge();
    }
}

PersonSample.java

package copy;

public class PersonMain {
    public static void main(String[] args) {
        Person person = new Person("A", 24);
        Person copiedPerson = person; // 얕은 복사

        System.out.println("변경 전 person: " + person);
        System.out.println("변경 전 copiedPerson: " + copiedPerson + "\n");

        copiedPerson.setName("B"); // 이름 변경

        System.out.println("변경 후 person: " + person);
        System.out.println("변경 후 copiedPerson: " + copiedPerson);
    }

}

Person.java는 String형 필드 name, int형 필드 age가 선언 되어 있는 클래스이다. 그리고 toString() 메소드를 name과 age 정보를 보여주도록 오버라이딩 해주었다.

PersonSample.java는 name = "A", age = 24인 person이라는 객체를 만든 후, copiedPerson 변수가 이를 얕은 복사한 코드이다.

주석에 "이름 변경"이라고 적힌 곳에서 copiedPerson의 name을 "B"로 변경해주고, 아래에서 person과 copiedPerson의 정보를 출력해주었다.

실행 결과를 보면 person과 copiedPerson의 name이 둘다 바뀐 것을 확인할 수 있는데, 이는 copiedPerson이 얕은 복사를 하여 person과 같은 객체(같은 메모리 공간)을 참조하고 있기 때문이다.

Deep Copy(깊은 복사)

객체의 참조가 아닌, 객체의 '값'을 복사해오는 것을 깊은 복사라고 한다. 즉, 새로운 객체를 생성한 다음 기존 객체의 값들을 새로운 객체에 복사하는 방법이다. 이렇게 하면 원본 객체의 참조 변수와 복사된 객체의 참조 변수는 각각 다른 객체(다른 메모리 공간)를 참조하게 된다. 이렇게 되면 두 변수가 서로 다른 객체를 참조하고 있기에 어느 한 객체의 값을 변경해도 다른 객체에 영향을 주지 않을 것이다.

Deep Copy를 쉽게 구현하는 방법은 Cloneable 인터페이스를 구현하여 clone() 메소드를 사용하는 방법이다. Object 클래스의 clone() 메소드는 protected로 선언되어 있기 때문에, 어디서든 접근할 수 있도록 public으로 접근 제어자를 바꿔서 오버라이딩 해주어야한다.

아까 위에서 보았던 Person 클래스를 Cloneable을 구현하는 클래스로 변경해보자.

package copy;

import java.lang.Cloneable;

// Cloneable 인터페이스를 구현
public class Person implements Cloneable {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

	// clone() 메소드 오버라이딩
    @Override
    public Object clone() throws CloneNotSupportedException { // CloneNotSupportedException은 Checked Exception이므로 반드시 예외처리 해주어야한다.
        return super.clone();
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "이름: " + getName() + "   나이: " + getAge();
    }
}

clone() 메소드를 호출할 때 발생할 수 있는 CloneNotSupportedException은 Checked Exception(컴파일 타임에 발견되는 Exception)이기 때문에 반드시 예외 처리를 해주어야 한다.

PersonMain.java의 복사하는 부분도 clone() 메소드로 수정한 후 실행해보자.

package copy;

public class PersonMain {
    public static void main(String[] args) {
        Person person = new Person("A", 24);

        Person copiedPerson = null;
        try {
            copiedPerson = (Person)person.clone(); // 변경된 부분(깊은복사)
        } catch(Exception e) {
            e.printStackTrace();
        }


        System.out.println("변경 전 person: " + person);
        System.out.println("변경 전 copiedPerson: " + copiedPerson + "\n");

        copiedPerson.setName("B");

        System.out.println("변경 후 person: " + person);
        System.out.println("변경 후 copiedPerson: " + copiedPerson);
    }

}

clone() 메소드를 오버라이딩하여 사용함으로써 깊은 복사가 되어 복사본을 변경하더라도 원본 객체의 값이 변경되지 않은 것을 확인할 수 있다.

Collection의 복사

Collection의 Shallow Copy(얕은 복사)

아래 코드를 보자.

package copy;

import java.util.ArrayList;
import java.util.List;

public class CopySample {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(2);
        list.add(3);

        List<Integer> copiedList = list; // 1. 얕은 복사

        System.out.println("list(before add): " + list);
        System.out.println("copiedList(before add) " + copiedList);

        boolean is_same = list == copiedList;
        
        System.out.println("같은 객체인가요?  " + is_same);
        copiedList.add(5); // 2. 복사된 배열에 5 추가
        System.out.println("list(after add): " + list);
        System.out.println("copiedList(after add) " + copiedList);


    }

}

위 코드에서 '1. 얕은 복사'라고 표시된 부분을 보면, 그냥 새로운 리스트 컬렉션 변수를 만들어서 거기에 기존 ArrayList를 대입해주고 있다. 이렇게 하면 copiedList가 list를 얕은 복사 방식으로 복사하게 되어서 copiedList와 list가 메모리의 같은 공간(같은 객체)을 참조하게 된다.

'2. 복사된 배열에 5 추가' 부분을 보면, copiedList에 새로운 원소를 추가했다. 그리고 list와 copiedList를 출력해보았다. 결과는 아래와 같다.

copiedList에 추가한 원소가 list를 출력해도 똑같이 들어있는 것을 확인할 수 있다. 두 변수가 같은 객체를 참조하고 있기 때문이다.

Collection의 Deep Copy(깊은 복사) - 원소가 Primitive Type일 때

package copy;

import java.util.ArrayList;
import java.util.List;

public class CopySample {
    public static void main(String[] args) {
        ArrayList<Integer> list = new ArrayList<>(); // 1. 이부분 변경
        list.add(1);
        list.add(2);
        list.add(3);

        List<Integer> copiedList = (ArrayList<Integer>)list.clone(); // 2. 이부분 변경. clone() 메소드로 깊은 복사

        System.out.println("list(before add): " + list);
        System.out.println("copiedList(before add) " + copiedList);

        boolean is_same = list == copiedList;
        System.out.println("같은 객체인가요?  " + is_same);
        copiedList.add(5);

        System.out.println("list(after add): " + list);
        System.out.println("copiedList(after add) " + copiedList);


    }

}

ArrayList를 clone() 메소드로 복사해주면 깊은 복사가 되어(즉, 새로운 ArrayList가 만들어지고 그 새로 만들어진 리스트에 기존 값들이 복사되어) 복사본에 새로운 원소를 추가하거나 변경하더라도 기존 리스트에 영향을 주지 않는다.

이 경우는 Collection의 요소가 primitive type이기 때문에 간단했다. 하지만 reference type인 경우는 어떻게 될까?

Collection의 Deep Copy(깊은 복사) - 원소가 Reference Type일 때

package copy;

import java.util.ArrayList;
import java.util.List;

public class CopySample {
    public static void main(String[] args) {
        ArrayList<Person> list = new ArrayList<>(); // 1. 이부분 변경
        list.add(new Person("A", 20));
        list.add(new Person("B", 21));
        list.add(new Person("C", 22));

        List<Person> copiedList = (ArrayList<Person>)list.clone(); // 2. 이부분 변경. clone() 메소드로 깊은 복사

        System.out.println("list(before modification): " + list);
        System.out.println("copiedList(before modification) " + copiedList);

        copiedList.get(0).setName("Z");


        System.out.println("list(after modification): " + list);
        System.out.println("copiedList(after modification) " + copiedList);


    }

}

앞에서 봤던 Person 클래스 객체를 요소로 가지는 Collection으로 코드를 변경하고, 그 ArrayList를 clone()으로 복사하였다.
그리고 복사본의 가장 앞에 있는 원소의 name을 "Z"로 변경해 보았다. 'clone()으로 복사했으니 원본엔 영향이 없지 않을까?'라고 생각할 수도 있지만

원본과 복사본 둘 다 바뀌어 버렸다.
이는 Collection을 clone으로 복사하더라도, 그 안에 있는 요소들의 참조가 끊어지지 않고 같은 객체를 참조하고 있기 때문이다.

이런 경우에는 리스트의 요소들을 for문으로 돌며 요소 객체를 하나씩 clone() 해주어야 한다.

package copy;

import java.sql.Array;
import java.util.ArrayList;
import java.util.List;

public class CopySample {
    public static void main(String[] args) {
        ArrayList<Person> list = new ArrayList<>(); // 1. 이부분 변경
        list.add(new Person("A", 20));
        list.add(new Person("B", 21));
        list.add(new Person("C", 22));

//        List<Person> copiedList = (ArrayList<Person>)list.clone(); // 2. 이부분 변경. clone() 메소드로 깊은 복사

        List<Person> copiedList = new ArrayList<>(); // 빈 리스트를 선언하고
        try{
            // 복사를 하는 부분. p.clone()으로 생성된 객체를 copiedList에 추가한다.
            for(Person p : list) {
                copiedList.add((Person)p.clone());
            }
        } catch(CloneNotSupportedException e) {
            e.printStackTrace();
        }

        System.out.println("list(before modification): " + list);
        System.out.println("copiedList(before modification) " + copiedList);

        copiedList.get(0).setName("Z");


        System.out.println("list(after modification): " + list);
        System.out.println("copiedList(after modification) " + copiedList);


    }

}

Reference

https://inpa.tistory.com/entry/JAVA-%E2%98%95-Object-%ED%81%B4%EB%9E%98%EC%8A%A4-clone-%EB%A9%94%EC%84%9C%EB%93%9C-%EC%96%95%EC%9D%80-%EB%B3%B5%EC%82%AC-%EA%B9%8A%EC%9D%80-%EB%B3%B5%EC%82%AC

https://zorba91.tistory.com/20

https://kotlinworld.com/3

profile
블로그 이전하려고 합니다! 👉 https://onfonf.tistory.com 🍀

0개의 댓글