티스토리 뷰

Java

방어적 복사에서 clone은 안전한가

woo'^'chang 2022. 7. 12. 15:04

주제

Effective Java 스터디에서 아이템 15. 클래스와 멤버의 접근 권한을 최소화하라는 주제에 관해 얘기하던 중 다음과 같은 질문이 나오게 되었습니다.

private static final Thing[] PRIVATE_VALUES = { ... };
public static final Thing[] values() {
    return PRIVATE_VALUES.clone();
}
원본 배열의 불변성을 지키기 위해 방어적 복사를 사용하여 복사 배열을 반환하는 메서드를 생성하였는데 배열 원소 내부의 값을 조작하게 되면 원본 배열의 불변성이 깨지지 않을까요?

clone 메서드를 사용하게 되면 새로운 메모리 공간에 값을 복사하게 되지만, 원소가 기본 타입이 아닌 참조 타입이기에 해당 주소값도 동일하게 복사되고 따라서 복사 배열을 통해 해당 참조에 접근하게 되면 원본 배열에도 영향을 주게 됩니다.

 

코드를 통해 하나씩 알아보도록 하겠습니다.

문제

방어적 메서드를 생성하게 된 이유는 원본 배열에 접근하는 것을 막기 위함입니다.

public class ThingNotSafe {

  public static final Thing[] VALUES = { new Thing("tv"), new Thing("phone") };

}

클래스에서 public으로 필드가 정의되어 있다면 외부에서 원본 배열의 접근이 가능해지고 원본 배열이 수정될 가능성이 매우 큽니다.

public class ThingMain {

    public static void main(String[] args) {    
        System.out.println(ThingNotSafe.VALUES[0]); // tv
        ThingNotSafe.VALUES[0] = new Thing("book");
        System.out.println(ThingNotSafe.VALUES[0]); // book
    }

}

이런 방법은 변경 가능성을 최대한 줄이라는 자바 코드의 원칙과 어긋나는 방법입니다. 따라서 원본 배열의 접근은 막고 원본 배열의 변경을 막을 수 있는 배열을 반환하는 방법을 선택해야 합니다. 그러기 위해서는 아래와 같은 방어적 복사의 방법을 사용할 수 있습니다.

public class ThingSafeCopy {

    private static final Thing[] PRIVATE_VALUES = { new Thing("tv"), new Thing("phone") };

    public static final Thing[] values() {
        return PRIVATE_VALUES.clone();
    }

    public Thing[] getThings() {
        return PRIVATE_VALUES;
    }

}

getThings 메서드는 원본 배열의 변경이 발생하는지를 확인하기 위해 만든 임의의 메서드입니다. 실제 사용 시 해당 메서드는 삭제가 필요합니다.

public class ThingMain {

    public static void main(String[] args) {
        Thing[] copyThings = ThingSafeCopy.values();
        Thing[] originalThings = new ThingSafeCopy().getThings();

        copyThings[0] = new Thing("book");

        System.out.println(copyThings); // Thing;@75bd9247
        System.out.println(originalThings); // Thing;@7d417077

        System.out.println(copyThings[0]); // book
        System.out.println(originalThings[0]); // tv
    }

}

실제 값을 확인해보면 복사된 배열의 참조는 다른 메모리 주소를 가리키고 있기에 원소 참조를 변경하더라도 원본 배열에 영향을 주지 않습니다. 방어적 복사를 적절히 수행했다고 할 수 있습니다.

 

하지만 원소 참조 값은 동일하게 복사되었기에 원소 참조의 내부 필드를 변경하게 된다면 다시 원본 배열에 영향을 줄 수 있다는 문제가 존재합니다. 여기서는 Thing 클래스의 내부 필드를 변경할 수 있음을 전제로 진행하고 있습니다.

public class ThingMain {

    public static void main(String[] args) {
        Thing[] copyThings = ThingSafeCopy.values();
        Thing[] originalThings = new ThingSafeCopy().getThings();

        copyThings[0].setName("book");
        System.out.println(copyThings[0]); // book
        System.out.println(originalThings[0]); // book
    }

}

위의 문제를 해결해보도록 하겠습니다.

해결

1. 배열 원소의 참조 클래스를 불변으로 만든다.

가장 간단한 방법으로는 배열 원소의 참조 클래스를 불변으로 만드는 것입니다. final 키워드를 사용하여 내부의 값을 변경할 수 없게 만든다면 변경 가능성에 조금이라도 신경 쓸 필요가 없습니다.

public class Thing {

    private final String name;

    public Thing(String name) {
        this.name = name;
    }

}

생성자에 의해 값이 정해진 뒤로는 값 변경 가능성이 존재하지 않습니다. 그렇기에 원본 배열의 불변성을 유지할 수 있습니다.

2. 클래스에 clone을 구현하여 깊은 복사가 이루어지도록 한다.

필요에 의해 클래스 필드가 변경되어야 하는 상황일수도 있습니다. 개발이라는 것은 유동적이기에 어떠한 상황에서도 대처할 방법이 필요합니다.

 

복사 배열이 생성될 때 원본 배열 원소의 주소값을 그대로 복사하는 것이 아닌 필드는 동일한 새로운 인스턴스를 생성해 복사 배열 원소로 참조되도록 합니다. 원본 배열 원소와 복사 배열 원소가 다른 참조값을 가지기에 복사 배열 원소의 필드가 변경되더라도 원본 배열에는 영향을 주지 않게 됩니다.

public class Thing implements Cloneable {

    private String name;

    public Thing(String name) {
        this.name = name;
    }

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

    @Override
    public Thing clone() {
        return new Thing(this.name);
    }

}

Cloneable 인터페이스 구현을 위해 clone 메서드를 오버라이딩합니다. 나의 필드와 같은 값을 가지는 새로운 인스턴스를 반환하도록 재정의합니다.

public class ThingSafeCopy {

    private static final Thing[] PRIVATE_VALUES = { new Thing("tv"), new Thing("phone") };

    public static final Thing[] values() {
        Thing[] things = new Thing[PRIVATE_VALUES.length];
        for (int i = 0; i < PRIVATE_VALUES.length; i++) {
            things[i] = PRIVATE_VALUES[i].clone();
        }
        return things;
    }

    public Thing[] getThings() {
        return PRIVATE_VALUES;
    }

}

방어적 복사를 위한 메서드도 수정이 필요합니다. 반복문을 통해 배열 원소의 복사본을 복사 배열 원소로 가지도록 메서드를 수정합니다.

public class ThingMain {

    public static void main(String[] args) {
        Thing[] copyThings = ThingSafeCopy.values();
        Thing[] originalThings = new ThingSafeCopy().getThings();

        copyThings[0].setName("book");
        System.out.println(copyThings[0]); // book
        System.out.println(originalThings[0]); // tv
    }

}

위에서 확인했던 문제와 달리 복사 배열 원소의 필드가 변경되어도 원본 배열에 영향을 주지 않습니다. 불변일 때와 달리 메모리 측면에서 성능이 저하될 수 있는 문제가 발생할 수 있습니다.

마치며

필요에 의해 위와 같은 복사 방법을 사용해도 되지만 충분히 고려가 필요하고 불변을 통해 변경 가능성을 최소화하는 것이 가장 합리적인 방법이라는 생각이 들었습니다.

'Java' 카테고리의 다른 글

[Java] JDBC 개념 정리  (4) 2023.06.08
[Java] 상속을 언제 사용해야 할까  (8) 2023.03.26
[Java] 자바의 특징  (0) 2022.11.26
Java 정규표현식  (0) 2022.06.23
Java Enum  (0) 2022.06.16
댓글
최근에 올라온 글
최근에 달린 댓글
«   2024/11   »
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
Total
Today
Yesterday