포스트

[Java] 깊은 복사 vs 얕은 복사

깊은 복사 vs 얕은 복사

얕은 복사 = 주소값만 복사

얕은 복사의 경우, 오리지널이 바뀌면 얕은 복사한 개체도 값이 바뀜.

깊은 복사의 경우, 실제 값을 복사 후, 새로운 메모리 공간에 복사

깊은 복사의 경우, 오리지널이 바뀌어도 깊은 복사를 한 개체는 값이 바뀌지 않음 (아예 서로 다른 개체임)

1. 자바 데이터 타입

기본 자료형(Primitive type)

종류: byte, char, short, int, long 등등…

특징: 실제 값을 변수 안에 저장한다. 즉 그 변수는 고유한 값을 가짐, 소문자로 시작함. null 로 시작하지 못함.

참조 자료형(reference type)

종류: 배열, 열거형, 클래스, 인터페이스

특징: 실제 값이 아닌, 메모리 주소를 값으로 가짐. 메모리 주소를 “참조” 하기 때문에 참조형임. null로 초기화 가능

2. 깊은 복사가 필요한 이유

깊은 복사는 주로 참조 자료형에서 사용된다.
기본 자료형의 경우, 복사를 하더라도 원본값에 주소가 아니라 실제 값이 들어있기 때문에 얕은 복사를 한 후, 원본을 수정하더라도 복사된 값이 수정되지 않는다.
대부분의 경우에서 두 객체를 비교할 때, 실제로 같은 주소값을 참조하는 리얼 True 같은 객체인지보다 가지고 있는 필드 값의 동등성을 볼 것 같기 때문에 필요한듯 하다.

예를 들어 nameage를 필드로 갖고있는 Man이라는 클래스가 있다고 해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class Man{
    private int age;
    private String name;

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

    public int getAge() {
        return age;
    }

    public String getName() {
        return name;
    }

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

    public void setName(String name) {
        this.name = name;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
public class Main {
    public static void main(String[] args) {
        Man man1 = new Man(10, "안녕");
        Man man2 = man1;

        System.out.println(String.format("%d, %s", man1.getAge(), man1.getName()));

        man1.setAge(20);
        man1.setName("그래");

        System.out.println(String.format("%d, %s", man2.getAge(), man2.getName()));
    }
}

= 으로 얕은 복사했을 시, 원본이 바뀌면 복사본에도 영향을 준다.

이런 상황은 객체가 독립적이지 못하게 하고, 데이터가 의도치 않게 변경되는 상황이 올 수 있다.

또한 객체 생성 후, 상태가 변경될 수 있다.

이것들을 피하기 위해 깊은 복사가 필요하다.

3. 깊은 복사를 하는 방법

1. Cloneable

Object.clone() 을 오버라이드해서 구현 가능하다. 재밌는 점은 Cloneable interface 내부엔 정의된 메서드가 없다. (왜 이렇게 만들어둔거지?)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class Man implements Cloneable{
    private int age;
    private String name;

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

    public int getAge() {
        return age;
    }

    public String getName() {
        return name;
    }

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

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

    @Override
    public Man clone() {
        try {
            return (Man) super.clone();
        } catch (CloneNotSupportedException e) {
            throw new AssertionError();
        }
    }
}

man2man1.setName() 의 영향을 받지 않게 됐다.

2. 복사 팩토리 메서드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class Man{
    private int age;
    private String name;

    public Man(int age, String name) { // 일반적인 생성자
        this.age = age;
        this.name = name;
    }

// ---------------- 복사 팩토리 메서드를 위해 필요한 코드---------------
    public Man(Man originalMan){
        this.age = originalMan.age;
        this.name = originalMan.name;
    }

    public static Man newInstance(Man originalMan){
        return new Man(originalMan);
    }
// ---------------- 복사 팩토리 메서드를 위해 필요한 코드---------------
    public int getAge() {
        return age;
    }

    public String getName() {
        return name;
    }

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

    public void setName(String name) {
        this.name = name;
    }
}
1
Man man2  = Man.newInstance(man1);

(사진자리2)

역시 영향을 받지 않게 된다.

4. Cloneable vs 복사 팩토리 메서드

위 코드만 놓고 보면, Cloneable을 사용하는게 압도적으로 편리한데 굳이 복사 팩토리 메서드를 사용해야할 이점이 있을까?

만약 객체의 필드가 기본자료형이 아니라 참조 자료형이면 어떨까?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Age {
    private int age;

    public Age(int age) {
        this.age = age;
    }

    public int getAge() {
        return age;
    }

    public void addAge(){
        age++;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class Man implements Cloneable{
    private Age age;
    private String name;

    public Man(Age age, String name) { // 일반적인 생성자
        this.age = age;
        this.name = name;
    }
    
    public Age getAge() {
        return age;
    }

    public String getName() {
        return name;
    }

    public void addAge() {
        age.addAge();
    }

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

    @Override
    public Man clone() {
        try {
            return (Man) super.clone();
        } catch (CloneNotSupportedException e) {
            throw new AssertionError();
        }
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Main {
    public static void main(String[] args) {
        Age age1 = new Age(10);

        Man man1 = new Man(age1, "안녕");
        Man man2 = man1.clone();

        System.out.println(String.format("변경 전 man1: %d, %s", man1.getAge().getAge(), man1.getName()));
        System.out.println(String.format("변경 전 man2: %d, %s", man2.getAge().getAge(), man2.getName()));

        man1.addAge();
        man1.setName("그래");

        System.out.println("----------------------------");
        System.out.println(String.format("변경 후 man1: %d, %s", man1.getAge().getAge(), man1.getName()));
        System.out.println(String.format("변경 후 man2: %d, %s", man2.getAge().getAge(), man2.getName()));
    }
}

addAge()를 통해 man1의 나이를 바꿔보았다.

놀랍게도 man2의 나이도 함께 바뀐다.

Cloneable을 사용시, 내부에 참조자료형 객체를 필드로 가지고 있다면 진정한 깊은 복사는 되지 않는다. 해당 참조자료형 객체는 얕은 복사가 된다.

참조 자료형까지 깊은 복사를 하는 방법

  1. 내부 참조자료형 객체까지 깊은 복사를 하고 싶으면 Age 에도 Cloneable을 붙여줘야 한다.
  2. 복사 팩토리 메서드를 사용한다.

    1
    2
    3
    4
    5
    6
    7
    8
    
     public Man(Man originalMan){
             this.age = new Age(originalMan.age.getAge());
             this.name = originalMan.name;
         }
        
         public static Man newInstance(Man originalMan){
             return new Man(originalMan);
         }
    

    새 객체를 만들어주는 식으로 해결하면 된다.

객체의 숫자가 늘어나면 계속 Cloneable을 써주는것 보다 복사 팩토리 메서드를 만드는게 품이 덜 드는 것 같다.



이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.