말하는 컴공감자의 텃밭

Java clone - 얕은복사 깊은복사 본문

백엔드/Java

Java clone - 얕은복사 깊은복사

현콩 2024. 2. 20. 18:22
728x90

Clone

인스턴스가 스스로를 복사하기 위해 사용.

 

Cloneable 인터페이스 구현 권장. 깊은 복사는 직접 오버라이드하여 구현해야 함.

 

👀 얕은 복사깊은 복사가 존재하는데 간단하게 설명 후 예시를 통해 코드로 확인해보자.

얕은복사 깊은복사
흐흐후훟흐

  • 얕은 복사는 객체의 필드 값을 새 객체로 복사하는 과정에서, 기본 타입의 필드는 그 값이 직접 복사되지만, 참조 타입의 필드는 메모리 주소(참조)만 복사된다. 이는 참조 타입의 필드가 가리키는 객체는 복사되지 않고, 원본 객체와 복사된 객체가 같은 객체를 참조하게 된다.
  • 결과적으로, 얕은 복사를 통해 생성된 객체에서 참조 타입의 필드를 수정하면, 그 변경사항이 원본 객체에도 영향을 미친다. 두 객체가 동일한 참조 타입의 인스턴스공유하기 때문이다.
  • 깊은 복사는 객체를 복사할 때, 참조 타입의 필드가 가리키는 객체까지도 새로운 객체로 복사하는 방식이다. 이렇게 하면, 복사된 객체는 원본 객체의 기본 타입 필드뿐만 아니라 참조 타입 필드가 가리키는 객체의 "복제본"을 가지게 되므로, 원본과 복사본은 완전히 독립적인 인스턴스로 존재하게 된다.
  • 깊은 복사를 수행한 후에는, 복사된 객체에서 참조 타입의 필드를 수정해도 원본 객체에는 아무런 영향을 미치지 않는다.  두 객체가 서로 다른 인스턴스를 참조하기 때문이다.

앞서 말했듯 얕은 복사는 clone() 을 이용하여 간단하게 구현 가능하지만, 깊은 복사를 위해선 오버라이드가 필수적이다.

 

✏ 얕은 복사 예제

먼저 얕은 복사를 확인해보자.

// 얕은 복사 - 인스턴스 한단계만 복사 ex)사람의 이름과 나이만 복사
public class Employee implements Cloneable {
    private String name; // 원시값인 이름과 나이
    private int age;

    // 생성자
    public Employee(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // 게터
    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    // Overriding 클론 메소드
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone(); // 원시값은 제대로 복사 됨.
    }

    public static void main(String[] args) {
        try {
            Employee original = new Employee("John Doe", 30);
            Employee cloned = (Employee) original.clone(); // 복제

            System.out.println("Original: " + original.getName() + ", " + original.getAge());
            System.out.println("Cloned: " + cloned.getName() + ", " + cloned.getAge());
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
    }
}

 

결과값 

Original: John Doe, 30
Cloned: John Doe, 30

 

이처럼 얕은 복사는 손쉽게 구현할 수 있다. 하지만 배열이나 참조값이 포함된다면 어떻게 될까?

다른 객체의 배열을 참조하는 경우에는 단순히 그 참조 주소를 가져올뿐 데이터를 복사하진 못한다.

예시로 들면 집은 그대로 있고 집문서만 복사하게 되는 꼴이다.

깊은 복사는 결국 새롭게 똑같은 집을 만들고 해당하는 문서를 만들어주는 과정이라고 비유를 들어도 되겠다.

 

✏ 깊은 복사 예제

깊은 복사를 코드로 확인해보자

class Person implements Cloneable {
    private String name;
    private Person[] friends; // Person 객체의 배열 참조

    // 생성자
    public Person(String name, Person[] friends) {
        this.name = name;
        this.friends = friends;
    }

    // name 필드의 getter
    public String getName() {
        return name;
    }

    // friends 필드의 getter
    public Person[] getFriends() {
        return friends;
    }

    // 깊은 복사를 위한 clone() 메소드 오버라이딩
    @Override
    protected Object clone() throws CloneNotSupportedException {
        // 먼저 얕은 복사를 수행
        Person cloned = (Person) super.clone();
        
        // friends 배열의 깊은 복사 수행
        if (this.friends != null) {
            cloned.friends = new Person[this.friends.length];
            for (int i = 0; i < this.friends.length; i++) {
                // 배열의 각 요소도 깊은 복사 수행
                cloned.friends[i] = (Person) this.friends[i].clone();
            }
        }
        
        return cloned;
    }

    public static void main(String[] args) {
        try {
            // 친구 객체 생성
            Person friend1 = new Person("친구1", null);
            Person friend2 = new Person("친구2", null);
            // 친구 배열 생성
            Person[] friends = {friend1, friend2};
            // Person 객체 생성
            Person person = new Person("본인", friends);

            // 깊은 복사 수행
            Person clonedPerson = (Person) person.clone();

            // 복사된 객체의 친구 이름 변경
            clonedPerson.getFriends()[0].name = "새 친구1";

            // 원본 객체와 복제 객체의 친구 이름 출력
            System.out.println("원본 객체의 첫 번째 친구 이름: " + person.getFriends()[0].getName());
            System.out.println("복제 객체의 첫 번째 친구 이름: " + clonedPerson.getFriends()[0].getName());
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
    }
}

 

원본 객체의 첫 번째 친구 이름: 친구1
복제 객체의 첫 번째 친구 이름: 새 친구1

 

원본 객체 친구 1을 복사하여 새 친구1 이라는 새로운 객체를 생성했다.

원본 객체에는 전혀 영향이 없는 것을 확인 할 수 있는데

 

 // 깊은 복사를 위한 clone() 메소드 오버라이딩
    @Override
    protected Object clone() throws CloneNotSupportedException {
        // 먼저 얕은 복사를 수행
        Person cloned = (Person) super.clone();
        
        // friends 배열의 깊은 복사 수행
        if (this.friends != null) {
            cloned.friends = new Person[this.friends.length];
            for (int i = 0; i < this.friends.length; i++) {
                // 배열의 각 요소도 깊은 복사 수행
                cloned.friends[i] = (Person) this.friends[i].clone();
            }
        }
        
        return cloned;
    }

이 오버라이딩 부분에서 friends 배열의 깊은 복사를 구현해두었기 때문이다.

 

흐름을 자연어로 적어보면

 

먼저 super.clone() 으로 얕은 복사를 수행하고, Person 객체의 모든 필드 값을 새로운 객체로 복사한다.

원시 데이터 타입 필드는 그 값이 직접 복사되어 가져와지고, 참조한 필드주소만 복사된다.

'cloned''this' 는 동일한 데이터 값 이지만 참조 타입 필드는 같은 객체인 상황이다.

 

이후 freind 배열이 실제로 참조하는 Person 객체에 접근하여 새로운 Person 배열을 생성한다.

원본과 크기가 동일하다. 이 배열에 기존 Person의 모든 값을 순환하며 복사한다.

따라서 완전히 독립적인 개체가 생성이 되었다.

 

참고 : 제대로 파는 자바 - 얄코 인프런 강의

728x90

'백엔드 > Java' 카테고리의 다른 글

Java - BufferedReader, StringTokenizer, BufferedWriter  (1) 2023.12.02
Comments