devseop08 님의 블로그

[OOP] 얕은 복사와 깊은 복사 본문

Language/Java

[OOP] 얕은 복사와 깊은 복사

devseop08 2025. 7. 20. 18:54

얕은 복사(Shallow Copy)

얕은 복사: 참조의 대상은 복사해서 늘리지 않고 참조자만 늘리는 형태

  • 사이드 이펙트 발생의 원인이 된다.
  • 대상 인스턴스는 그대로 두고 참조만 늘어나는 경우
  • 참조자에 참조자를 대입하여 두 개의 참조자가 동일한 하나의 인스턴스를 참조하게 하는 방식
  • 참조자가 참조하는 인스턴스 안의 값(내용)을 복사해오는 것이 아니다.
  • 여러 참조자가 하나의 인스턴스를 공유하게 된다. => 공유하는 하나의 인스턴스 상태를 변경 시 변경으로 인한 영향이 전파된다.

  • 얕은 복사 예시
class Address {
    public String addr;
    public String phone; 

    public Address(String addr, String phone) {
        this.addr = addr;
        this.phone = phone;
    }
}
class User {
    private String name; 
    private Address addr; 

    User(String name, String addr, String phone) {
        this.name = name;
        this.addr = new Address(addr, phone); // 원본 생성
    } 

    public String getName() {
        return name;
    }

    public Address getAddr() {
        return addr;
    }

    public void copy(User rhs) {
        this.name = rhs.name;
        this.addr = rhs.addr; // 얕은 복사: 인스턴스 내용이 아닌 참조값 복사
    }
}
public class Main() {
    public static void main(String[] args) {
        User user1 = new User("Hosung", "Hanam", "010-1111-1111");
        User user2 = new User("Hoon", "Seoul", "010-2222-2222");

        System.out.println(user1.getName());
        System.out.println(user1.getAddress().address);
        System.out.println(user1.getAddress().phone);

        user1.copy(user2);// user1의 addr과 user2의 addr이 같은 Address 인스턴스 참조
        user2.getAddress().address = "Busan";
        user2.getAddress().phone = "010-3333-3333";

        System.out.println(user1.getName());
        System.out.println(user1.getAddress().address);
        System.out.println(user1.getAddress().phone);
    }
}
// 결과
Hosung
Hanam
010-1111-1111
Hoon
Busan
010-3333-3333
  • user1.copy(user2) 실행 전(앝은 복사 발생 전)의 user1의 addr이 참조하는 인스턴스와 user2의 addr이 참조하는 인스턴스는 다르다.

  • user1.copy(user2) 실행 후(앝은 복사 발생 후)의 user1의 addr이 참조하는 인스턴스와 user2의 addr이 참조하는 인스턴스는 동일한 인스턴스이다.

  • 그렇기 때문에 user2.getAddress().address = ..., user2.getAddress().phone = ...로 user2의 addr이 참조하는 인스턴스의 내용을 변경하면
  • user1의 addr을 통해 상태 변경을 하지 않았음에도 user1의 addr을 통해 addr이 참조하는 인스턴스의 상태를 조회하면 인스턴스의 상태가 변경되어 있는 것이다.
  • 이는 예상치 못한 사이드 이펙트 문제가 발생할 수 있음을 의미한다.

깊은 복사: 원본 자체를 새로 할당하고 내용을 복사하는 방식

  • Shallow Copy와 달리 두 개의 원본 두 개의 참조가 각각 별도로 존재
  • 사이드 이펙트 오류 가능성이 없음
  • 참조를 멤버로 가지며 인스턴스를 동적 할당하는 경우 복사 생성자 구현 필요(clone 메서드)

  • user1의 addr과 user2의 addr을 얕은 복사가 아닌 깊은 복사 방식으로 변경
public void copy(User rhs) {
        this.name = rhs.name;
        this.addr.addr = rhs.addr.addr; // 깊은 복사: 인스턴스 안의 addr 값이 복사
        this.addr.phone = rhs.addr.phone; // 깊은 복사: 인스턴스 안의 phone 값이 복사
}
public class Main() {
    public static void main(String[] args) {
        User user1 = new User("Hosung", "Hanam", "010-1111-1111");
        User user2 = new User("Hoon", "Seoul", "010-2222-2222");

        System.out.println(user1.getName());
        System.out.println(user1.getAddress().address);
        System.out.println(user1.getAddress().phone);

        user1.copy(user2);// user1의 addr과 user2의 addr은 다른 Address 인스턴스 참조
        user2.getAddress().address = "Busan"; 
        user2.getAddress().phone = "010-3333-3333"; 
        // user2의 addr이 참조하는 Address 인스턴스 내용만 변경
        // user1의 addr이 참조하는 Address 인스턴스 내용엔 영향 없음

        System.out.println(user1.getName());
        System.out.println(user1.getAddress().address);
        System.out.println(user1.getAddress().phone);
    }
}
// 결과
Hosung
Hanam
010-1111-1111
Hoon
Seoul
010-2222-2222
  • user1.copy(user2) 실행 후(깊은 복사 발생 후), user2의 addr이 참조하는 Address 타입 객체의 내용(addr 값과 phone 값)이 user1의 addr이 참조하는 Address 타입 객체의 내용으로 복사된다.
  • 참조가 복사되는 것이 아닌, 내용(값)이 복사되므로 user2의 addr이 참조하는 Address 타입 객체의 내용을 변경하더라도 user1의 addr이 참조하는 Address 타입 객체의 내용은 해당 변경에 대해 영향을 받지 않는다.

복사 생성자

  • 자바에서 새롭게 생성하는 인스턴스에 기존 인스턴스의 내용을 깊은 복사하기 위한 방법으로, Object 클래스의 clone 메서드를 오버라이딩하여 사용하는 방법이 있다.
  • 하지만 Object 클래스의 clone 메서드는 오버라이딩하여 사용하기가 어렵다.
  • Object 클래스의 clone 메서드
    • java.lang.Object 클래스에 정의된 protected Object clone() 메서드는 객체의 필드 값을 복사해서 새 인스턴스를 반환
    • 기본적으로는 얕은 복사(shallow copy)만 수행
    • Cloneable 인터페이스를 구현해야 clone() 호출 시 CloneNotSupportedException이 발생하지 않는다.'
  • clone()을 오버라이딩해 사용하기 어려운 이유들
      1. Cloneable 인터페이스가 마커 인터페이스다 (문제가 되는 이유)
          • Cloneable은 메서드가 전혀 없는 "마커 인터페이스"
        • 단순히 clone() 호출 시 예외 발생 여부만 제어할 뿐, 진짜 복제 동작은 Object.clone()에만 정의
        • 즉, 복제 로직은 Object가 알고 있음에도, 사용자가 구현 책임을 져야 함
      1. Object.clone()protected` 메서드다
        • clone() 메서드는 Object에서 protected로 선언되어 있어, 외부 클래스에서 바로 호출할 수 없음
        • 즉, public으로 오버라이딩하지 않으면 사용 자체가 제한됨.
      1. 얕은 복사(shallow copy)의 한계
        • 기본 clone()은 객체의 필드 값을 그대로 복사
        • 하지만 필드가 참조 타입일 경우 내부 객체의 주소만 복사되므로 원본과 공유하게 됨.
      1. 깊은 복사를 구현하기 까다로움
        • 객체 내에 다른 참조 타입이 있고, 그 내부에도 참조 타입이 있으면 재귀적 깊은 복사 필요.
        • 따라서 모든 관련 클래스가 clone()을 적절히 구현하고 있어야 함 → 결합도 증가, 오류 가능성 증가**
      1. Checked Exception: CloneNotSupportedException
        • Object.clone()CloneNotSupportedException이라는 checked 예외를 던짐.
        • 예외 처리를 위해 try-catch 강제 → 코드 간결성 떨어짐
        • Cloneable 구현을 깜빡하면 런타임에 터짐
      1. 불변 객체나 복잡한 상속 구조와 맞지 않음
        • 불변 객체는 복사해서 변경하는 방식이 어울리나, clone()은 상태 변경 가능성을 열어둠
        • 상속이 깊은 경우, clone()을 제대로 구현하려면 모든 하위 클래스에서 오버라이딩이 필요 → 유지보수성 저하]
  • 이와 같은 이유들로 Object의 clone 메서드를 재정의하여 새롭게 생성한 인스턴스에 깊은 복사를 하기 위한 메서드를 구현하기 보다는 새롭게 생성한 인스턴스에 깊은 복사를 하기 위한 복사 생성자를 직접 구현할 것을 권장한다.
  • 새롭게 생성한 인스턴스에 기존 인스턴스의 내용을 깊은 복사 하기 위한 복사 생성자 형식(자신 자신의 타입으로 정의된 기존 인스턴스의 참조자를 매개변수로 전달받아야 한다. 즉, 매개변수 타입이 자기 자신이어야만 한다.)
class Person {
    String name;
    Address address;

    public Person(String name, String addr, String phone){
        this.name = name;
        this.address = new Address(addr, phone);
    }

    // 복사 생성자
    public Person(Person other) {
        // 내용(값) 복사
        this(other.name, other.address.addr, other.address.phone);
    }
}
  • 클래스의 인스턴스 변수로 배열이 포함될 때, 새롭게 생성한 인스턴스에 해당 클래스 타입의 인스턴스 안의 배열을 깊은 복사하는 복사 생성자 구현
public class MyTest {
    private int[] array = null;
    public MyTest(int size) {
        array = new int[size];
    }

    public MyTest(MyTest rhs) { // 자신 자신의 타입을 매개변수 타입으로 선언
        this(rhs.array.length);
        this.deepCopy(rhs); // 새로 생성한 인스턴스에 기존 인스턴스 내용 깊은 복사
    }

    public int getAt(int index) {
        return array[index];
    }

    public void setAt(int index, int value) {
        array[index] = value;
    }

    public void shallowCopy(MyTest rhs) {
        array = rhs.array;  // 참조값 대입으로 얕은 복사
    }

    public void deepCopy(MyTest rhs) {
    //  array = rhs.array.clone(); 배열 타입 안의 깊은 복사를 위한 멤버 메서드 clone
        for(int i=0; i < rhs.array.length; i++){
            // array는 private 멤버이지만 자신의 클래스 안이므로 접근 가능
            array[i] = rhs.array[i];
        } 
    }
}
public class Main() {
    public static void main(String[] args) {
        MyTest t1 = new MyTest(3);
        t1.setAt(0, 10);
        t1.setAt(1, 20);
        t1.setAt(2, 30);

        MyTest t2 = new MyTest(3);
        t2.shallowCopy(t1);
        MYTest t3 = new MyTest(3);
        t3.shaalowCopy(t1);
        MyTest t4 = new MyTest(t1);

        t1.setAt(0, -1);
        System.out.println("t1[0]: " + t1.getAt(0));
        System.out.println("t2[0]: " + t2.getAt(0));
        System.out.println("t3[0]: " + t3.getAt(0));
        System.out.println("t4[0]: " + t4.getAt(0));
    }
}
// 결과
t1[0]: -1
t2[0]: -1
t3[0]: 10
t4[0]: 10