백엔드 개발자라면 대답해야 할 100가지 질문

18. Deep Copy vs Shallow Copy

ignuy 2023. 10. 5.

알고리즘을 공부하며 깊은 복사(Deep Copy)와 얕은 복사(Shallow Copy), 이 둘의 차이점을 뼈저리게 느낀 JAVA 사용자가 꽤 많을 것이라 생각한다. 따라서 백 마디 말보다 코드 몇 줄이 더 효과적일 것이라는 생각으로 아래와 같은 코드를 작성하였다.

List<Integer> list = new ArrayList<>();
list.add(1);

List<Integer> copyList = list;
copyList.add(2);

System.out.println(list);
System.out.println(copyList);

/**
 * list : [1, 2]
 * copyList : [1, 2]
 */

위와 같이 주소 값을 복사하는 의미로 Shallow Copy, 실제 값을 복사하는 의미로 Deep Copy를 구분하여 사용한다.

자바의 데이터 타입

자바는 데이터 타입을 Primitive typeReference type으로 나눈다.

Primitive Type은 기본값이 존재하는 데이터 타입으로 null을 지원하지 않고 실제 데이터의 값을 Stack 영역에 저장하고 있다. 그 종류로는 boolean, int, char, byte 등이 있다.

Reference Type은 Primitive Type을 제외한 모든 타입을 말한다. null을 지원하긴 하지만 아무런 값을 참조하지 않는다는 의미이지 빈 값을 가지고 있다는 의미가 아니기 때문에 NullPointerException의 원인이 된다.

"I call it my billion-dollar mistake…" _ null을 만든 Tony Hoare는 null의 탄생이 자신의 10억 달러의 실수라고 말한다.

Reference Type의 객체가 가지는 실제 값은 Heap 영역에 저장되고 Stack에 그 객체를 가리키는 주소값을 저장하여 포인터값을 찾아보며 힙 영역의 데이터에 접근하게 된다.

주소값의 복사

이제 위의 코드를 데이터 영역 측면에서 이해해보자. 우리가 제일 윗줄에 선언한 list가 Heap의 1000번지를 가리키고 있다고 가정해보자.

List<Integer> copyList = list;
copyList.add(2);

우리가 list의 값을 가져오겠다고 수행했던 이 코드라인은 의도와는 전혀 다른 결과를 낳는다. list에 대응되는 값은 [1, ]이 아닌 1000번지를 가리키는 주소값이다.

우리는 이처럼 주소값을 복사하는 과정을 얕은 복사, Shallow Copy라고 부른다.

깊은 복사, Deep Copy 구현

그럼 주소값이 아닌 실제 값을 복사해 오는 과정인 깊은 복사, Deep Copy에 대해 알아보자.

첫번째 방법, Cloneable inteface 구현

“A class implements the Cloneable interface to indicate to the Object.clone() method.” 위 인터페이스를 구현하는 클래스는 반드시 Object.clone() 메서드를 재정의하여 구현하라고 권장하고 있습니다.

Cloneable 인터페이스는 복제해도 되는 클래스임을 명시하는 용도의 인터페이스이다. 몇 가지 주의할 문제점이 내재되어 있지만 일단 지금은 자세한 설명은 생략하고 Cloneable 인터페이스를 사용하는 방식이 있다는 것만 알아두자.

두번째 방법, 복사 생성자, 복사 팩터리 사용

public class tempObject {
    private String name;
    private int value;
    
    public tempObject() {}     
    
    // 복사 생성자
    public tempObject(tempObject original) {
        this.name = original.name;
        this.value = original.value;
    }
    
    // 복사 팩터리
    public static tempObject copy(tempObject original) {
        tempObject copy = new tempObject();
        copy.name = original.name;
        copy.value = original.value;
        
        return copy;
    }
}

일반적으로 사용자가 생성한 객체에 대해 Deep Copy를 시도할 때 사용하는 방식은 복사 생성자 또는 복사 팩터리를 사용하는 것이다. 클래스 내에 위 두 가지 방식을 구현하고 활용한다면 Heap 영역에 새로운 객체 인스턴스를 만들고 이 새로운 주소를 Stack에 간직하므로 Shallow Copy를 피할 수 있다.

댓글