얕은복사와 깊은복사의 개념에 대해 정리하고 방어적 복사의 중요성과 코드를 정리한 포스트입니다.
객체를 복사할 때, 해당 객체만 복사하여 새 객체를 생성한다.
복사된 객체의 인스턴스 변수는 원본 객체의 인스턴스 변수와 같은 메모리 주소를 참조한다.
따라서, 해당 메모리 주소의 값이 변경되면 원본 객체 및 복사 객체의 인스턴스 변수 값은 같이 변경된다.
import java.util.ArrayList;
import java.util.List;
public class Application {
public static void main(String[] args) {
List<Name> originalNames = new ArrayList<>();
originalNames.add(new Name("Fafi"));
originalNames.add(new Name("Kevin"));
// newNames의 names: Fafi, Kevin
Names newNames = new Names(originalNames);
// newNames의 names: Fafi, Kevin, Sally
// newNames를 건드리지 않았는데 내부 값이 변경됨
originalNames.add(new Name("Sally"));
}
}
객체를 복사 할 때, 해당 객체와 인스턴스 변수까지 복사하는 방식.
전부를 복사하여 새 주소에 담기 때문에 참조를 공유하지 않는다.
깊은복사를 하기 위해서 객체는 Cloneable Interface를 Implement 해야하고 clone 메서드를 오버라이드해야한다.
public class PhysicalInformation implements Cloneable{
int height;
int weight;
@Override
public Object clone() throws CloneNotSupportedException{
return super.clone();
}
}
아래는 clone을 통해 깊은 복사를 하는 예시이다.
PhysicalInformation physicalInformation = new PhysicalInformation();
physicalInformation.height = 180;
physicalInformation.weight = 70;
PhysicalInformation physicalInformationDeepCopy =
physicalInformation.clone();
생성자의 인자로 받은 객체의 복사본을 만들어 내부 필드를 초기화하거나,
getter메서드에서 내부의 객체를 반환할 때, 객체의 복사본을 만들어 반환하는 것.
방어적 복사를 사용할 경우, 외부에서 객체를 변경해도 내부의 객체는 변경되지 않는다.
방어적 복사와 깊은 복사는 비슷해 보이지만 깊은 복사에는 문제점이 존재한다.
깊은 복사의 clone을 사용함에 있어서 문제점이 발생하는데
이 clone메소드가 사용자가 정의한 것이 아닐 수 있다는 점이다.
즉, clone이 악의를 가진 하위 클래스의 인스턴스를 반환할 수도 있어 사용에 주의가 필요하다.
다음은 TOCTOU(Time Of Check / Time Of Use) 취약점이 있는 객체이다
(즉 검사시점/사용시점 공격이 가능하다.)
public final class Period {
private final Date start;
private final Date end;
public Period(Date start, Date end) {
if (start.compareTo(end) > 0) {
throw new IllegalArgumentException("start가 end보다 늦으면 X");
}
this.start = start;
this.end = end;
}
public Date getStart() { return start; }
public Date getEnd() { return end; }
}
객체가 외부에서 변경이 가능해 진다.
Date start = new Date();
Date end = new Date();
Period period = new Period(start, end);
end.setYear(78); // 취약점: period 내부를 수정할 수 있다.
🔹 변경이 가능해 짐으로 멀티 스레딩환경에서 생성자의 검사에서 문제가 발생 할 수 있다.
이를 해결하기 위해 생성자에 방어적 복사를 적용한다.
public Period(Date start, Date end) {
this.start = new Date(start.getTime()); // defensive copy
this.end = new Date(end.getTime()); // defensive copy
if (this.start.compareTo(this.end) > 0) {
throw new IllegalArgumentException("start가 end보다 늦으면 X");
}
}
매개변수의 유효성을 검사하기 전에 방어적 복사본을 만들고, 이 복사본으로 유효성을 검사한다.
순서가 부자연스러워 보이겠지만 반드시 이렇게 작성해야 한다.
멀티스레딩 환경이라면 원본 객체의 유효성을 검사한 후 복사본을 만드는 그 찰나의 취약한 순간에 다른 스레드가 원본 객체를 수정할 위험이 있기 때문이다.
방어적 복사를 매개변수 유효성 검사 전에 수행하면
이런 검사시점/사용시점(time of check/time of use) 공격 혹은 영어 표기를 줄여서 TOCTOU 공격을 방어 할 수있다.
생성자를 통해 setter 취약점은 방어했지만
get으로 인스턴스를 가져와 변경을 할 수 있는 문제가 발생한다.
Date start = new Date();
Date end = new Date();
Period period = new Period(start, end);
period.getEnd().setTime(78); // 취약점: period 내부를 변경할 수 있다.
🔹 위의 취약점을 방어하려면 getter 메소드에도 다음과 같이 defensive copy 기법을 적용해 주면 된다.
public Date getStart() {
return new Date(start.getTime());
}
public Date getEnd() {
return new Date(end.getTime());
}
새로운 인스턴스를 생성해 반환 하기 때문에 참조가 끊어진 인스턴스가 넘어간다.
🔹 혹은 unmodifiable 객체를 사용하여 방어 할 수도있다.
unmodifiable 객체는 defensive copy의 대안으로 함께 고려해볼만한 방법이다.
unmodifiable 객체를 만드는 가장 쉬운 방법은 Collections의 unmodifiable... 시리즈를 사용하는 것이다.
객체 리턴시 아래처럼 unmodifiable 객체를 리턴 해 준다.
return Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES)
private static final Integer[] PRIVATE_VALUES = new Integer[]{ 1,2,3 };
public static final List<Integer> VALUES =
Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUES));
unmodifiableList() 메서드를 통해 리턴되는 리스트는 읽기 용도로만 사용할 수 있다.
set(),add(), addAll() 등의 리스트에 변경을 가하는 메서드를 호출하면 UnsupportedOperationException 이 발생한다.
다만, Unmodifiable과 Immutable은 다르다. Unmodifiable이라는 키워드가 불변을 보장해주지는 않는다.
원본 자체에 대한 수정이 일어나면 unmodifiableList() 메서드를 통해 리턴되었던 리스트 또한 변경이 일어난다.
핵심은 객체 내부의 값을 외부로부터 보호하는 것이라는 것이다.
외부에서 넘겨줬던 객체를 변경해도 내부의 객체는 변하지 않아야 한다.
따라서 방어적 복사가 적절하다.
이 상황에선 방어적 복사를 통해 복사본을 반환해도 좋고, Unmodifiable Collection을 이용한 값을 반환하는 것도 좋다.