glenn-syj / more-effective-java

이펙티브 자바를 읽으며 자바를 더 효율적으로 공부합니다
4 stars 5 forks source link

[MEJ-004] 객체를 깊게 복사하는 4가지 방법 #82

Closed clare-u closed 7 months ago

clare-u commented 7 months ago

Based on: #77 by @FickleBoBo


이번주에도 좋은 글 써주셔서 너무 재밌게 읽었습니다! 얕은 복사와 깊은 복사의 차이를 조사하기 위해 예시를 들어 공부해 본 내용을 공유합니다. 편의상 아래로는 반말로 작성한 점 양해 부탁드리겠습니다.


우선 Student 클래스를 아래와 같이 생성해 보았다. 이름(name)과 잔고(money)를 가지고 있는 학생이다. toString도 임의로 변경하였다.

public class Student {
    public String name;
    public int money;

    public Student() {
    }

    public Student(String name, Integer money) {
        this.name = name;
        this.money = money;
    }

    public String getName() {
        return name;
    }

    public Integer getMoney() {
        return money;
    }

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

    public void setMoney(Integer money) {
        this.money = money;
    }

    @Override
    public String toString() {
        return name + " 의 잔고: " + "money=" + money;
    }

    // 금액 차감
    public void spendMoney(Integer money) {
        this.money -= money;
    }

}

얕은 복사 진행

clare라는 학생1의 통장엔 만원이 들어있다. (생성자로 생성) 그리고 = 연산자로 학생2 객체에 할당하였다. (값을 복사)

이 때 clare가 천원을 썼다면? 둘 다 돈이 차감되어 버린다. 둘은 같은 객체를 가리키고 있기 때문이다. (참조값이 같다)

public class 얕은복사 {
    public static void main(String[] args) {
        Student student1 = new Student("clare", 10000);
        Student student2 = student1;

        student1.spendMoney(1000);
        System.out.println("1: " + student1);
        System.out.println("2: " + student2);

        // 1: clare 의 잔고: money=9000
        // 2: clare 의 잔고: money=9000

    }
}

이런 경우를 얕은 복사라고 한다. 얕은 복사는 주소값을 복사하여 서로 같은 부분을 바라본다. 단, setter메서드를 외부에 공개하지 않을 수도 있기 때문에 다른 방식으로 복사할 필요가 있다. 특히 실무에서 다른 개발자들이 setter를 의도와 다르게 잘못 사용할 수도 있어서 실무에서 쓰일 수 없을 가능성이 높다고 한다.

깊은 복사 4가지 방법

  1. getter setter
public class 깊은복사_getter와_setter {
    public static void main(String[] args) {
        Student student1 = new Student("clare", 10000);
        Student student2 = new Student();
        student2.setName(student1.getName());
        student2.setMoney(student1.getMoney());

        student1.spendMoney(1000);
        System.out.println("1: " + student1);
        System.out.println("2: " + student2);

        // 1: clare 의 잔고: money=9000
        // 2: clare 의 잔고: money=10000

        // student1에서 name과 money를 가져와 setter 메소드로 값만 넣어주는 방식
        // 객체의 참조는 각각 다른 곳을 가리킨다.
    }

}
  1. 복사생성자

    우선 Student 클래스에 아래와 같은 코드 추가가 필요하다.

    // 복사 생성자를 이용한 방법
    public Student(Student student) {
        this.name = student.name;
        this.money = student.money;
    }
public class 깊은복사_복사생성자 {

    public static void main(String[] args) {
        // 복사 생성자를 이용한 방법
        Student student1 = new Student("clare", 10000);
        Student student2 = new Student(student1);

        student1.spendMoney(1000);
        System.out.println("1: " + student1);
        System.out.println("2: " + student2);

        // 1: clare 의 잔고: money=9000
        // 2: clare 의 잔고: money=10000
    }

}
  1. 복사 팩터리 메소드

Student 클래스에 아래와 같은 코드 추가 후 이용이 가능한 방식이다.

    // 복사 팩토리 메소드를 이용한 방법
    public static Student newObject(Student student) {
        // 기본 생성자 필요
        Student s = new Student();
        s.name = student.name;
        s.money = student.money;
        return s;
    }
public class 깊은복사_복사팩터리메소드 {
    public static void main(String[] args) {
        // 복사 팩토리 메소드를 이용한 방법
        Student student1 = new Student("clare", 10000);
        Student student2 = Student.newObject(student1);

        student1.spendMoney(1000);
        System.out.println("1: " + student1);
        System.out.println("2: " + student2);

        // 1: clare 의 잔고: money=9000
        // 2: clare 의 잔고: money=10000

    }

}
  1. Cloneable

    clone 메서드를 사용하기 위해 , Cloneable 인터페이스를 처음 구현하면 clone 메소드는 접근제어자가 protected에 Object 타입을 반환하는 메소드가 생성되는데, 이를 적절히 변경해주어야 한다.

    이러한 clone 메소드를 작성하여 호출하는데 만약 Cloneable 인터페이스를 구현하지 않는 경우 CloneNotSupportedException이 발생하므로 유의하도록 한다.

public class Student2 implements Cloneable {

...

@Override
    public Student2 clone() throws CloneNotSupportedException {
        return (Student2) super.clone();
    }

}
public class 깊은복사_Cloneable {

    public static void main(String[] args) {
        Student2 student1 = new Student2("clare", 10000);
        try {
            Student2 student2 = student1.clone();

            student1.spendMoney(1000);
            System.out.println("1: " + student1);
            System.out.println("2: " + student2);

            // 1: clare 의 잔고: money=9000
            // 2: clare 의 잔고: money=10000
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }

    }

}

참고 자료 https://developia.tistory.com/entry/Java-객체-복사에-대한-고찰

FickleBoBo commented 7 months ago

복사생성자와 복사팩터리메서드에 대해 조사를 해주셔서 더욱 풍성하게 복사를 이해할 수 있는 글이었던 것 같습니다.

본 교재를 학습하며 복사생성자와 복사팩터리메서드 등을 활용하는 것이 clone()보다 권장되는 것으로 이해했었는데, 구현 측면에서는 clone()과 마찬가지로 개발자의 의도에 맞게 깊은 복사가 되었는지 꼼꼼히 구현을 해야 해서 큰 차이가 없는 것 같다는 생각도 듭니다.

올려주신 코드에서 4번 Cloneable 파트에 대해 super.clone()으로 바로 return을 하는 게 student의 속성이 전부 기본 타입이라서 진행된 코드인지 아니면 일반적인 상황에서는 ...으로 표현된 부분에 추가 구현이 필요함을 의미한 것인지 궁급합니다. 저는 super.clone()에 대해서 객체 자체는 새롭게 깊은 복사가 되지만 내부 속성은 같은 값을 참조한다고 생각했습니다.(기본 타입이라 상관X) 두 코드블록과 맨 위 student 코드 블록을 통해 테스트를 하려고 했는데 실행이 어려워서 질문드립니다...

clare-u commented 7 months ago

올려주신 코드에서 4번 Cloneable 파트에 대해 super.clone()으로 바로 return을 하는 게 student의 속성이 전부 기본 타입이라서 진행된 코드인지 아니면 일반적인 상황에서는 ...으로 표현된 부분에 추가 구현이 필요함을 의미한 것인지 궁급합니다. 저는 super.clone()에 대해서 객체 자체는 새롭게 깊은 복사가 되지만 내부 속성은 같은 값을 참조한다고 생각했습니다.(기본 타입이라 상관X) 두 코드블록과 맨 위 student 코드 블록을 통해 테스트를 하려고 했는데 실행이 어려워서 질문드립니다...

... 부분에는 글 위쪽의 Student 코드를 그대로 사용해 주시고 아래에 오버라이드 부분을 추가해 주시면 됩니다!! 중복되는 부분이 너무 길 것 같아 생략했는데 설명을 달지 않았네요.

FickleBoBo commented 7 months ago

작성해주신대로 코드를 넣으니 잘 실행이 됩니다!! CloneNotSupportedException에 대한 코드를 추가해 try catch로 작성해주셔서 더욱 수준 높은 clone() 활용을 보여주신 것 같습니다!!