달달한 스토리

728x90
반응형

 

 

이 글은 잭 코딩님의 글을 참조했습니다.

 

https://jackjeong.tistory.com/100

 

[Java] Shallow copy(얕은 복사) vs Deep copy(깊은 복사)

안녕하세요~ 잭코딩입니다! 이번 내용에서는 Shallow copy(얕은 복사)와 Deep copy(깊은복사)를 살펴봅시다 코드를 짜다보면 객체를 복사해야할 경우가 생깁니다 이 때 실수로 복사를 잘못하면 큰 이

jackjeong.tistory.com

 

코딩 중에 갑자기 문득 궁금한 게 생겼었다.

 

나 같은 경우는 리사이클러뷰 어뎁터를 사용하다가

 

검색창 기능을 만들기 위해

 

같은 ArrayList를 두개 만들어주고, 똑같은 값을 이런 식으로 넣어 주었다.

 

그런데 이렇게 값을 넣어주는데,

 

검색을 해도 아이템이 나오지 않았다.

 

구글링을 해보니

 

이렇게 하면 될 것이라고 나왔다.

 

그런데 정말 놀랍게도 아이템이 출력이 되었다.

 

이유가 뭘까?

 

그냥 그러니 넘기면 안될 것 같은 예감이 들어 찾아보니

 

얕은 복사와 깊은 복사에 대해서 알게 되었다.

 

이게 뭔지 한 번 알아보자.

 


Shallow copy(얕은 복사) vs Deep copy(깊은 복사)

 

얕은 복사는 주소값을 복사하는 것이고, 깊은 복사는 실제값을 새로운 메모리 공간에 복사하는 것이다.

 

이 둘의 차이가 무엇일까?

 

얕은 복사는 같은 주소를 사용하기 때문에

 

A의 값이 변경되면 A와 같은 주소를 가지고 있는 다른 객체도

 

같이 값이 변경된다는 것이다.

 

글로는 어려우니 예제를 통해 이해해 보도록 하자.

 

public class Human {
    String name;
    int age;

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

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

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

 

Human이라는 클래스를 하나 준비해준다.

 

728x90

1. 얕은 복사(Shallow Copy)

class Main {
    public static void main(String[] args) {
        shallowCopy();
    }

    public static void shallowCopy() {
        Human human = new Human("sooyeol", 26);
        Human humanCopy = human;

        human.changeName("minsoo");
        human.changeAge(30);

        System.out.println(human.age);
        System.out.println(human.name);
        System.out.println(humanCopy.age);
        System.out.println(humanCopy.name);
    }
}

다음과 같이 실행해보자.

 

sooyeol이라는 이름에 26살 Human클래스를 생성하고,

 

이 인스턴스를 humanCopy에다가 복사합니다.

 

그리고 human 인스턴스에 name과 age를 다른 값으로 바꾸어줍니다.

 

그렇다면

 

프린트를 찍어보면 어떻게 나올까요??

 

human은 minsoo, 30가 나오고,

 

humanCopy는 sooyeol 26이 나오겠지..라고

 

생각하실 수 있습니다.

 

하지만, 결과는 아래와 같습니다.

 

System.out.println(human.age); //30
System.out.println(human.name); //"minsoo"
System.out.println(humanCopy.age); //30
System.out.println(humanCopy.name); //"minsoo"

 

두 인스턴스 모두 같은 값을 가지고 있는 것입니다.

 

어째서 같은 값을 가지는 것일까요?

 

아래 그림을 통해 보시면 이해가 쉬우실 겁니다.

humanCopy는 Heap에 따로 값을 할당받는 게 아니라,

 

human의 값을 같이 참조하고 있습니다. 

 

human과 humanCopy는 서로 같은 주소값을 참고하기 때문에,

 

둘 중 한명이 값이 바뀐다면, heap에 있는 해당 주소 값에 데이터가 바뀌는 것이기 때문에

 

두 값 모두 값이 바뀌는 것입니다.

 

하지만 이런식으로 복사하는 경우도 있지만,

 

보통은 실제값을 복사해야 할 것입니다.

 

저렇게 같은 주소값을 공유하는 것이 아닌 복사를 통해

 

독립된 인스턴스를가진 상태 말입니다.

 

그러려면 우리는 깊은 복사를 해야 합니다. 

 

2. 깊은 복사(Deep Copy)

깊은 복사를 하는 방법은 여러가지 있습니다.

 

1. 복사 생성자 혹은 복사 팩토리를 이용해서 복사합니다.

 

2. 직접 객체 생성을 하여 복사합니다.

 

3. Cloneable을 구현하여 clone() 메서드로 재정의

(clone() 재정의는 final 인스턴스나 배열이 아닌 경우 사용을 권하지 않는 다고 합니다.)

 

2-1 복사생성자, 복사 팩토리

우선 Human클래스의 변화를 주겠다.

 

public class Human {
    String name;
    int age;

    //복사 생성자
    public Human(Human human) {
        this.name = human.name;
        this.age = human.age;
    }

    public Human() {
        this("sooyeol", 26);
    }

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

    //복사 팩토리
    public static Human newInstance(Human human) {
        Human h = new Human();
        h.name = human.name;
        h.age = human.age;
        return h;
    }
}

복사 생성자와 복사 팩토리를 각각 만들어 주었다.

 

복사 생성자는 다음과 같이 사용한다.

 

Human 모델 클래스에 this는 안에 포함된 파라미터에 맞는

 

생성자를 호출해줍니다.

 

ex)

public Human() {
    this("sooyeol", 26);
}

=> public Human(String name, int age) {
        this.name = name;
        this.age = age;
    }
    //이것과 같다.

 

복사 생성자와 복사 팩토리에 사용법은 다음과 같다.

 

 public static void main(String[] args) {
        //복사 생성자
        Human human = new Human();
        Human human2 = new Human(human);

        human2.age = 30;
        human2.name = "minsoo";

        System.out.println(human.age); //26
        System.out.println(human2.age); //30
        System.out.println(human.name); //sooyeol
        System.out.println(human2.name); //minsoo


        //복사 팩토리
        Human human3 = new Human();
        Human human4 = Human.newInstance(human3);

        human4.age = 30;
        human4.name = "minsoo";

        System.out.println(human3.age); //26
        System.out.println(human4.age); //30
        System.out.println(human3.name); //sooyeol
        System.out.println(human4.name); //minsoo
    }

두 가지 방법 모두 다른 주소 값을 가지게 만들어 주는 방법을 사용하여,

 

한쪽에서 값을 바꾸어도, 기존에 복사받아왔던 값은 원래 값을 가지고 있다.

 

반응형

 

2-2 직접 객체를 생성하여 복사

class Human {
    String name;
    int age;

    public Human() {
        this("sooyeol", 26);
    }

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

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

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

}

class Main {
    public static void main(String[] args) {
        deepCopy();
    }

    static void deepCopy() {
        Human human = new Human("minsoo", 30);
        Human humanCopy = new Human();
        humanCopy.setName(human.getName());
        humanCopy.setAge(human.getAge());

        humanCopy.name = "sooyeol";
        humanCopy.age = 26;

        System.out.println(human.getName()); //minsoo
        System.out.println(human.getAge()); //30
        System.out.println(humanCopy.getName()); //sooyeol
        System.out.println(humanCopy.getAge()); //26
    }
}

객체를 하나 생성하고, 해당 인스턴스를 파라미터로 넘겨주는 복사 방법이다.

 

이 역시 humanCopy를 다른 값으로 바꾸어주니,

 

둘 다 다른 인스턴스라는 것을 프린트로 찍어보면 알 수가 있다.

 

2-3 Cloneable을 구현하여 clone()메서드로 재정의 (비추천)

class Human implements Cloneable {
    String name;
    int age;

    @NonNull
    @Override
    protected Human clone() throws CloneNotSupportedException {
        return (Human) super.clone();
    }

    public Human() {
        this("sooyeol", 26);
    }

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

}

class Main {
    public static void main(String[] args) throws CloneNotSupportedException {
        deepCopy();
    }

    private static void deepCopy() throws CloneNotSupportedException {
        Human human = new Human("sooyeol", 26);
        Human humanCopy = human.clone();

        humanCopy.name = "minsoo";
        humanCopy.age = 30;

        System.out.println(human.name);
        System.out.println(human.age);
        System.out.println(humanCopy.name);
        System.out.println(humanCopy.age);
    }
}

우선 Human 클래스에 Cloneable을 implements해주고,

 

윈도는 컨트롤 + O를 누르고,  맥은 command + o를 누르면

 

선택할 수 있는 override와 implement 선택창이 뜨게 된다.

 

여기서 clone() 메서드를 선택해서 구현한다.

 

 

@NonNull
@Override
protected Human clone() throws CloneNotSupportedException {
    return (Human) super.clone();
}

 

그리고 다음과 같이 휴면 클래스를 클론 하여 반환한다.

 

여기서 CloneNotSupportedException은 이름 그대로,

 

Cloneable이 지원이 되는 버전이 아니면 예외처리가 난다는 뜻이다.

 

그리고 deepCopy 메서드로 오게 되면, 

 

새로 만든 human 인스턴스를 클론 메서드를 호출하여,

 

humanCopy 변수에 넣어준다.

 

그리고 humanCopy에 변수만 수정하여,

 

각각 호출하게 되면,

 

System.out.println(human.name); //sooyeol
System.out.println(human.age); //26
System.out.println(humanCopy.name); //minsoo
System.out.println(humanCopy.age); //30

이렇게 각각 다른 인스턴스라는 것 동시에,

 

그전에 값이 완벽히 깊은 복사가 되었다는 것을 증명할 수 있다.

원래 있던 그림으로 다시 그려 조잡한 그림인 점 이해 바라겠습니다ㅠㅜㅠ

 

얕은 복사를 할 때는 Heap 같은 참조값을 참조했지만,

 

clone을 포함하여 위에 깊은 복사를 하게 되면

 

Heap 영역에 또 하나의 참조값이 생기게 됩니다.

 

고로, 깊은 복사를 하게 되면 Heap 영역에 새로운 참조값이 생겨,

 

각각에 값을 가지게 되는 복사를 할 수 있게 되는 것이지요.

 

여기까지가 얕은 복사와 깊은 복사에 대한 이야기였습니다.

 

감사합니다!

 

실시간 업데이트 요소수 앱 "요소수 주유소"

구글 플레이 스토어에서 만나보세요.

https://play.google.com/store/apps/details?id=com.diesel.gasstation

 

요소수 주유소 - 실시간 요소수 재고 조회 - Google Play 앱

요소수 거점 주유소 위치, 운영시간, 5분 마다 알 수 있는 요소수 재고량, 요소수 가격(전화 문의) 매일 업데이트

play.google.com

 

728x90
반응형

공유하기

facebook twitter kakaoTalk kakaostory naver band
loading