JAVA/Effective Java

[아이템 50] 적시에 방어적 복사본을 만들라

꾸준함. 2024. 3. 9. 13:51

자바의 안전성

자바는 안전한 언어입니다.

  • 자바로 작성한 클래스는 불변식이 지켜짐
  • 자바는 c, c++와 달리 네이티브 메서드를 사용하지 않음
    • c, c++는 네이티브 메서드를 사용하기 때문에 버퍼 오버런, 배열 오버런, 와일드 포인터 같은 메모리 충돌 오류로부터 안전하지 않음

 

방어적 프로그래밍을 해야 하는 이유

자바가 아무리 안전한 언어라고 해도 다음과 같은 이유 때문에 방어적 프로그래밍을 해야 합니다.

  • 다른 클래스로부터의 침범을 아무런 노력 없이 막을 수 있는 것은 아님
  • 악의적인 의도를 가진 사람들이 시스템 보안을 뚫으려는 시도가 늘고 있는 추세
  • 휴먼 에러로 인해 클래스가 오동작하게 생성될 수 있음

 

따라서, 클라이언트가 불변식을 깨드리려고 노력한다고 가정하고 방어적으로 코드를 작성해야 합니다.

 

객체가 자기도 모르게 내부를 수정하도록 허락하는 케이스

어떤 객체든 그 객체의 허락 없이는 외부에서 내부를 수정하는 일은 불가능합니다.

그러나 방어적으로 코드를 작성하지 않으면 다음과 같이 자기도 모르게 내부를 수정하도록 허락하는 경우가 발생할 수 있습니다.

이를 예시로, 시작 시간이 종료 시간보다 빨라야 하는 Period 클래스를 들어보겠습니다.

 

Version 1

 

 

 

얼핏 보면 위 클래스는 불변처럼 보이고, 시작 시간이 종료 시간보다 늦을 수 없다는 불변식이 지켜질 것처럼 보입니다.

하지만 Date는 가변 객체이기 때문에 period 인스턴스의 필드를 수정할 경우 불변식이 깨집니다.

 

 

위 문제는 Date 대신 자바 8+ 이후로 도입된 LocalDateTime 혹은 ZoneDateTime을 사용하면 쉽게 해결할 수 있습니다.

 

Version 2

 

앞선 버전에서는 생성자에서 방어적 복사를 하지 않았기 때문에 파라미터로 넘겨받은 Date 내부를 수정하면 클래스 불변식이 깨지는 문제가 발생했습니다.

따라서 해당 버전에서는 생성자에 전달된 파라미터를 그대로 필드에 적용하지 않고 방어적 복사를 수행했습니다.

 

 

새로 작성한 생성자를 사용하면 앞서의 공격은 더 이상 위협이 되지 않습니다.

위 생성자의 핵심은 TOCTOU 공격을 의식해 매개변수의 유효성 검사를 수행하기 전에 방어적 복사본을 만들고 해당 복사본을 토대로 유효성을 검사한다는 점입니다.

  • 멀티 쓰레드 환경에서 원본 객체의 유효성 검사 후 복사본을 생성하는 찰나에 다른 쓰레드가 원본 객체를 수정할 위험이 있음
  • 위 내용을 TOCTOU 공격이라고 지칭
    • 검사 시점(Time Of Check)
    • 사용 시점(Time Of Use)

 

하지만 위 코드도 start와 end에 대한 getter 메서드를 제공하고 Date 객체가 setter 메서드를 지원하기 때문에 불변이 깨질 수 있습니다.


 

 

주의: 방어적 복사를 수행할 때 파라미터로 전달된 객체가 상속이 가능한 클래스일 경우 방어적 복사본을 clone() 메서드를 통해 만들면 안 됩니다.

  • Date는 LocalDateTime과 달리 불변 객체가 아님
  • Date가 불변이 아니기 때문에 clone() 메서드로 인해 악의를 가진 하위 클래스의 인스턴스가 반환될 수 있음


 

Version 3

드디어 불변식이 보장되는 최종 버전입니다.

이번 버전에서는 getter 메서드들에 대해서도 방어적 복사를 수행했기 때문에 getter로 받은 Date 필드를 수정해도 Period 객체에 영향이 없습니다.

Period 자신 말고는 가변 필드에 접근할 방법이 없으므로 아무리 악의적인 혹은 부주의한 개발자라도 시작 시간이 종료 시간보다 빨라야 한다는 불변식을 위배할 방법이 없습니다.

  • native 메서드 혹은 reflection 같이 언어 외적인 수단을 동원하면 뚫릴 수는 있겠지만...

 

 

 

코드 부연 설명

  • 생성자와 달리 접근자 메서드에서는 Period가 가지고 있는 Date 객체는 java.util.Date임이 확실하기 때문에 방어적 복사에 clone을 사용해도 됨
    • 그럼에도 불구하고 일반적으로 생성자나 정적 팩토리를 쓰는 것을 권장

 

정리

메서드든 생성자든 클라이언트가 제공한 객체의 참조를 내부의 자료구조에 보관해야 할 때면 항시 그 객체가 잠재적으로 변경될 수 있는지를 고려해야 합니다.

변경될 수 있는 객체라면 그 객체가 클래스에 넘겨진 뒤 임의로 변경되어도 클래스 불변식이 보장되는지 따져보고 확신할 수 없을 경우 반드시 복사본을 만들어 저장해야 합니다.

자신이 작성한 클래스가 불변이든 가변이든, 가변인 내부 객체를 클라이언트에 반환할 때 또한 원본을 노출하지 말고 Collections.unmodifiableList()와 같이 불변 뷰를 반환하거나 방어적 복사본을 반환하는 것을 권장합니다.

만약 복사 비용이 너무 크거나 클라이언트가 그 요소를 잘 못 수정할 일이 없음을 보장할 수 있다면 방어적 복사를 수행하는 대신 해당 구성요소를 수정했을 때의 책임이 클라이언트에 있음을 문서화하는 것을 권장합니다.

 

참고

이펙티브 자바

반응형