[얕은복사 / 깊은복사] 방어적 복사란 ?

Jae Eon·2021년 7월 4일
2

백엔드 공부

목록 보기
7/17
post-thumbnail

들어가며

얕은복사와 깊은복사의 개념에 대해 정리하고 방어적 복사의 중요성과 코드를 정리한 포스트입니다.

🍊얕은 복사란?

  • 객체를 복사할 때, 해당 객체만 복사하여 새 객체를 생성한다.

  • 복사된 객체의 인스턴스 변수는 원본 객체의 인스턴스 변수와 같은 메모리 주소를 참조한다.

  • 따라서, 해당 메모리 주소의 값이 변경되면 원본 객체 및 복사 객체의 인스턴스 변수 값은 같이 변경된다.

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; }
}

⛔️ setter 변경 취약점

객체가 외부에서 변경이 가능해 진다.

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 공격을 방어 할 수있다.


⛔️ getter 취약점

생성자를 통해 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() 메서드를 통해 리턴되었던 리스트 또한 변경이 일어난다.


🍒 방어적 복사 vs Unmodifiable Collection

  • 방어적 복사와 Unmodifiable Collection 각각을 언제 어떻게 사용해야 할까?

핵심은 객체 내부의 값을 외부로부터 보호하는 것이라는 것이다.

🔹 생성자의 인자로 객체를 받았을 때

외부에서 넘겨줬던 객체를 변경해도 내부의 객체는 변하지 않아야 한다.

따라서 방어적 복사가 적절하다.

🔹 getter를 통해 객체를 리턴할 때

이 상황에선 방어적 복사를 통해 복사본을 반환해도 좋고, Unmodifiable Collection을 이용한 값을 반환하는 것도 좋다.

profile
🖋정리를 안하면 잊어버린다.👣한 발자국씩 가보자!

0개의 댓글