본문 바로가기

Programming

까먹은 걸 알고리즘을 풀다가 알게된 얕은 복사 VS 깊은 복사

https://www.acmicpc.net/problem/19236

 

 

 

 

 

static Fish[][] copyMap(Fish[][] map) {
    Fish[][] copy = new Fish[4][4];
    for (int i = 0; i < 4; i++) {
        copy[i] = map[i].clone();
    }
    return copy;
}

 

두 부분에서 차이가 났다.

 

static Fish[][] copyMap(Fish[][] map) {
    Fish[][] copy = new Fish[4][4];
    for (int i = 0; i < 4; i++) {
         for (int j = 0; j < 4; j++) {
            copy[i][j] = new Fish(map[i][j].num, map[i][j].dir);
        }
    }
    return copy;
}

 

 

 

 

두 코드 모두 2차원 배열을 복사하지만, 그 방식이 다릅니다.
copy[i] = map[i].clone()는 얕은 복사,

copy[i][j] = new Fish(map[i][j].num, map[i][j].dir) 깊은 복사를 수행합니다. 


얕은 복사 vs. 깊은 복사
얕은 복사 (Shallow Copy)

static Fish[][] copyMap(Fish[][] map) {
    Fish[][] copy = new Fish[4][4];
    for (int i = 0; i < 4; i++) {
        copy[i] = map[i].clone();
    }
    return copy;
}
map[i].clone()

은 각 1차원 배열을 복사합니다.
얕은 복사에서는 배열의 각 원소인 Fish 객체에 대한 참조만 복사됩니다.
결과적으로, copy와 map은 같은 Fish 객체들을 가리키게 됩니다.


만약 복사된 배열에서 Fish 객체의 상태를 변경하면 원본 배열에서도 해당 객체의 상태가 변경됩니다.

 

 

깊은 복사 (Deep Copy)


static Fish[][] copyMap(Fish[][] map) {
    Fish[][] copy = new Fish[4][4];
    for (int i = 0; i < 4; i++) {
        for (int j = 0; j < 4; j++) {
            copy[i][j] = new Fish(map[i][j].num, map[i][j].dir);
        }
    }
    return copy;
}

 

각 Fish 객체를 새롭게 생성하여 복사합니다.
깊은 복사에서는 원본 배열과 복사된 배열이 서로 다른 Fish 객체를 가리킵니다.
이 방법을 사용하면 copy에서 Fish 객체를 변경해도 map에는 영향을 주지 않습니다.
결론
얕은 복사는 배열의 구조만 복사하고 배열 내 객체의 참조를 그대로 유지하므로 성능이 더 좋지만, 객체의 상태가 공유되기 때문에 독립적인 변경이 불가능합니다.
깊은 복사는 배열의 모든 객체를 새로 생성하므로 더 많은 메모리를 사용하고 시간이 더 걸리지만, 복사된 배열과 원본 배열의 객체가 독립적입니다.

 

특징얕은 복사깊은 복사

특징 얕은 복사 깊은 복사
복사 속도 빠름 느림
메모리 사용 효율적 비효율적
내부 객체 참조 동일 객체 참조 새로운 객체 생성 및 참조
독립성 낮음  높음

 

 

 

얕은 복사 확인

 

public class ShallowCopy implements Cloneable {

    int[] data;

    public ShallowCopy(int[] data){
        this.data = data;
    }

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

    public static void main(String[] args) throws CloneNotSupportedException{
        int[] data = {1, 2, 3};
        ShallowCopy original = new ShallowCopy(data);
        ShallowCopy copy = (ShallowCopy) original.clone();

        // 변경 확인
        copy.data[0] = 99;
        System.out.println(original.data[0]);
    }

}

자바 배열 클래스는 기본적으로 Cloneable 인터페이스를 구현하지만,  Cloneable을 사용하여 객체 복제를 명확히 표현하였습니다.

 

 

copy의 데이터를 변경하면 원본 배열의 데이터도 변경되는 것을 확인할 수 있습니다. 

 

 

 

 

 

 

깊은 복사

class DeepCopy implements Cloneable {
    int[] data;

    public DeepCopy(int[] data) {
        this.data = data;
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        int[] dataCopy = new int[this.data.length];
        System.arraycopy(this.data, 0, dataCopy, 0, this.data.length);
        return new DeepCopy(dataCopy);
    }

    public static void main(String[] args) throws CloneNotSupportedException {
        int[] data = {1, 2, 3};
        DeepCopy original = new DeepCopy(data);
        DeepCopy copy = (DeepCopy) original.clone();

        // 변경 확인
        copy.data[0] = 99;
        System.out.println(original.data[0]); // 1
    }
}

 

 

자바 배열은 기본적으로 Object를 상속받아 .clone() 메서드가 얕은 복사로 구현되어있지만,

Cloneable interface를 상속받아 clone() 메서드를 Override 했습니다.

arraycopy를 이용해서 깊은 복사를 수행하도록 했습니다. 

 

 

copy의 데이터를 변경해도 원본 배열의 데이터는 변경되지 않는 것을 확인할 수 있습니다. 

 

 

 

 

복사 속도 비교

public static void measureShallowCopyTime() throws CloneNotSupportedException {
    int[] data = new int[1000000];
    for (int i = 0; i < data.length; i++) {
        data[i] = i;
    }
    ShallowCopy original = new ShallowCopy(data);

    long startTime = System.nanoTime();
    ShallowCopy copy = (ShallowCopy) original.clone();
    long endTime = System.nanoTime();

    System.out.println("얕은 복사 : " + (endTime - startTime) + " ns");
}

public static void measureDeepCopyTime() throws CloneNotSupportedException {
    int[] data = new int[1000000];
    for (int i = 0; i < data.length; i++) {
        data[i] = i;
    }
    DeepCopy original = new DeepCopy(data);

    long startTime = System.nanoTime();
    DeepCopy copy = (DeepCopy) original.clone();
    long endTime = System.nanoTime();

    System.out.println("깊은 복사: " + (endTime - startTime) + " ns");
}

 

100만개를 복사하는 시간을 비교해보면 약 330배 정도 차이가 나지만 깊은 복사도 0.5ms 걸린 정도입니다 

 

 

메모리 사용 측정


메모리 사용량을 확인하기 위해서는 Runtime 객체를 사용하여 JVM의 메모리 상태를 확인해야 합니다.

가비지 컬렉터를 실행해서 메모리를 비우고, 전후 메모리를 비교하면됩니다.

 

JVM과 가비지 컬렉터에 대해서는 다음 글에서 자세히 다뤄보겠습니다.

public static void measureMemoryUsage() throws CloneNotSupportedException {
    Runtime runtime = Runtime.getRuntime();
    runtime.gc(); // 가비지 컬렉터 실행

    int[] data = new int[1000000];
    for (int i = 0; i < data.length; i++) {
        data[i] = i;
    }

    ShallowCopy originalShallow = new ShallowCopy(data);
    DeepCopy originalDeep = new DeepCopy(data);

    long beforeUsedMem = runtime.totalMemory() - runtime.freeMemory();
    ShallowCopy shallowCopy = (ShallowCopy) originalShallow.clone();
    long afterUsedMemShallow = runtime.totalMemory() - runtime.freeMemory();

    System.out.println("얕은 복사 메모리 사용: " + (afterUsedMemShallow - beforeUsedMem) + " bytes");

    beforeUsedMem = runtime.totalMemory() - runtime.freeMemory();
    DeepCopy deepCopy = (DeepCopy) originalDeep.clone();
    long afterUsedMemDeep = runtime.totalMemory() - runtime.freeMemory();

    System.out.println("깊은 복사 메모리 사용: " + (afterUsedMemDeep - beforeUsedMem) + " bytes");
}

 

 

근데 왜 100만 length 의 int 배열을 깊은 복사를 했는데 1,000,000 * 4 인 400만 byte가 나오지 않고 3650352 byte만 차이가 날까요?

-> 바로 다음글에서 서술하도록 하겠습니다

 

 

'Programming' 카테고리의 다른 글

Git add 란?  (0) 2022.02.21
Git 사용법  (0) 2022.02.21