자바는 안전한 언어다. 네이티브 메서드를 사용하지 않으니 메모리 충돌 오류에서 안전하다.
그래도 클라이언트가 불변식을 깨뜨리려 혈안이 되어있다고 가정하고 방어적으로 프로그래밍해야 한다.
어떤 객체든 그 객체의 허락 없이는 외부에서 내부를 수정하는 일은 불가능하다. 하지만 주의를 기울이지 않으면 허락하는 경우가 생긴다.
/ 코드 50-1 기간을 표현하는 클래스 - 불변식을 지키지 못했다. (302-305쪽)
public final class Period {
private final Date start;
private final Date end;
/**
* @param start 시작 시각
* @param end 종료 시각. 시작 시각보다 뒤여야 한다.
* @throws IllegalArgumentException 시작 시각이 종료 시각보다 늦을 때 발생한다.
* @throws NullPointerException start나 end가 null이면 발생한다.
*/
public Period(Date start, Date end) {
if (start.compareTo(end) > 0)
throw new IllegalArgumentException(
start + "가 " + end + "보다 늦다.");
this.start = start;
this.end = end;
}
public Date start() {
return start;
}
public Date end() {
return end;
}
public String toString() {
return start + " - " + end;
}
final로 선언을했고, setter도 없으니 불변처럼 보인다. 그래서 시작 시각이 종료 시각보다 늦으면 안된다는 불변식이 지켜질 것 같다. private final Date
로 선언된 필드이지만 문제는 Date
클래스의 가변성 때문이다. Date
객체 자체가 가변적이기 때문에 내부 값이 변경될 수 있는 문제가 있다.
즉, 필드 자체가 불변인 것은 맞지만 그 내부 필드들은 불변이 아니다.
Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
end.setYear(78); // p 내부 수정
자바 8이후로는 Date
대신 불변인 Instant
를 사용하면 된다. (혹은 LocalDateTime
또는 ZonedDateTime
). 문제는 이미 오래된 API에 Date
가 많이 사용되어 있다.
외부 공격으로 부터 내부를 보호하려면 생성자에서 받은 기본 매개변수 각각을 방어적으로 복사해야 한다.
Date
클래스는 가변이기 때문에, start()
또는 end()
메서드에서 반환된 객체를 통해 내부 값을 변경할 수 있다. 예를 들어 Period
객체의 end
필드를 통해 반환된 Date
객체를 수정하면, Period
클래스 내부의 end
값도 변경된다. 이로 인해 의도하지 않은 내부 상태 변경이 발생할 수 있다.
자바 8 이후로는 이러한 문제를 간단히 해결하기 위해 LocalDateTime
또는 ZonedDateTime
과 같은 불변(immutable) 날짜 객체를 사용할 수 있다. 이러한 클래스들은 변경 불가능한 특성을 가지므로, 내부 상태가 외부에서 수정될 가능성이 없다.
외부 공격으로 부터 내부를 보호하려면 생성자에서 받은 기본 매개변수 각각을 방어적으로 복사해야 한다. 그런 다음 Period 인스턴스 안에서는 원본이 아닌 복사본을 사용한다.
public Period(Date start, Date end, int version) {
if (start.compareTo(end) > 0) {
throw new IllegalArgumentException(start + "가 " + end + "보다 늦다.");
}
if (version == 1) {
this.start = start;
this.end = end;
} else {
// 방어적 복사본 만들기
this.start = new Date(start.getTime());
this.end = new Date(end.getTime());
}
}
버전이 1일 경우 원래 객체를 사용하지만, 그렇지 않으면 방어적 복사본을 만들어 필드에 저장한다. 이로 인해 원래 객체를 외부에서 변경하더라도 Period
클래스의 내부 필드에는 영향을 주지 않게 된다.
새로 작성한 생성자를 사용하면 방어할 수 있다.
매개변수의 유효성을 검사(아이템49)하기 전에 방어적 복사본을 만들고, 이 복사본으로 유효성을 검사한 점에 주목하자. 반드시 이렇게 작성해야 한다.
멀티스레딩 환경이라면 원본 객체의
유효성
을 검사한 후 검사본을 만드는 찰나의 순간에 다른 스레드가 원본 객체를 수정할 위험이 있기 때문이다.
매개변수가 제3자에 의해 확장될 수 있는 타입이라면 방어적 복사본을 만들 때 clone을 사용해서는 안된다. Date
는 final
이 아니므로 clone
이 Date
가 정의한게 아닐 수도 있다. 즉, clone
이 악의를 가진 하위 클래스의 인스턴스를 반환할 수도 있다. 예를 들어 이 하위 클래스는 start
와 end
필드의 참조를 private
정적 리스트에 담아뒀다가 공격자에게 이 리스트에게 접근하는 길을 열어줄 수도 있다.
// Period 인스턴스를 향한 두 번째 공격
Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
p.end().setYear(78);
접근자 메서드에서도 마찬가지로 방어적 복사를 사용하면 불변성을 유지할 수 있다. 두 번째 공격을 막으려면 접근자가 가변 필드의 방어적 복사본을 반환하면 된다.
// 수정한 접근자 - 필드의 방어적 복사본 반환
public Date start() {
return new Date(start.getTime());
}
public Date end() {
return new Date(end.getTime());
}
접근자 메서드가 방어적 복사본을 반환하면, 반환된 객체를 수정하더라도 Period
클래스의 내부 상태에는 영향을 미치지 않는다. 필드 자체를 직접 반환하면, final
로 선언했더라도 내부 메서드를 통해 해당 객체가 변경될 가능성이 있다는 점을 항상 인지해야 한다.
이제 Period 자신 말고는 가변 필드에 접근할 방법이 없으니 모든 필드가 캡슐화되었다.
생성자와 달리 접근자 메서드에서는 방어적 복사에
clone
을 사용해도 된다.Period
가 가지고 있는Date
객체는java.util.Date
임이 확실하기 때문이다.
메서드든 생성자든 클라이언트가 제공한 객체의 참조를 내부의 자료구조에 보관해야 할 때면 항시 그 객체가 잠재적으로 변경될 수 있는지를 생각해야 한다. 확신할 수 없다면 복사본을 만들어 저장해야 한다.
길이가 1이상인 배열은 무조건 가변임을 잊지말자.
방어적 복사에는 성능 저하가 따르고 항상 쓸수 있는 것도 아니다. 방어적 복사를 생략할 때는 해당 매개변수가 반환값을 수정하지 말아야함을 명확히 문서화해야 한다.
방어적 복사를 위해 생성자에서 clone()
메서드를 사용하는 것은 위험할 수 있다. 악의적인 사용자가 clone()
메서드를 재정의한 하위 타입을 생성하고, 이를 이용해 방어적 복사를 우회할 가능성이 있기 때문이다. 따라서 생성자에서 clone()
사용은 권장되지 않는다.
반면, 접근자 메서드에서 clone()
을 사용하는 것은 괜찮다. new Date()
를 사용해 방어적 복사본을 반환함으로써, 해당 객체가 안전한 Date
타입임을 보장할 수 있기 때문이다. 하지만 여전히 clone()
은 예측하기 어려운 상황을 야기할 수 있으므로, 생성자나 정적 팩토리 메서드를 통한 방어적 복사가 더 권장된다.
- 클래스와 클라이언트를 상호 신뢰할 수 있을 때
- 불변식이 깨지더라도 그 영향이 오직 클라이언트로 국한될 때
- 래퍼 클래스 패턴은 클라이언트는 래퍼에 넘긴 객체에 여전히 직접 접근할 수 있지만 그 영향을 오직 클라이언트 자신만 받게 된다.
- 불변 객체 사용: 가변 객체 대신 불변 객체(
LocalDate
,LocalDateTime
) 사용을 우선 고려하자. 불변 객체는 이러한 문제를 원천적으로 방지할 수 있다. - 객체의 깊은 복사: 방어적 복사 시, 필드가 객체 참조를 포함하는 경우 깊은 복사가 필요할 수 있다. 얕은 복사는 필드 내부의 참조를 그대로 복사하므로, 여전히 원본 객체가 변경될 위험이 있다.
- 인터페이스 설계 시 가변성 고려: API 설계 시 외부에서 객체의 내부 상태를 변경할 수 없도록 설계하는 것이 중요하다. 특히 외부에 노출되는 데이터는 불변성을 보장하거나 복사본을 제공하는 방식을 사용하는 것이 좋다.
- 통제권 이전 명시: 때로는 메서드나 생성자의 매개변수로 넘기는 행위가 그 객체의 통제권을 명백히 이전함을 뜻하기도 한다. 이 경우, 클라이언트가 해당 객체를 더 이상 직접 수정하지 않는다고 약속해야 하며, 그 사실을 문서화하는 것이 중요하다.
- 방어적 복사 생략 조건: 호출자가 컴포넌트 내부를 수정하지 않으리라 확신하거나, 불변식이 깨지더라도 그 영향이 호출한 클라이언트로 국한될 경우 방어적 복사를 생략할 수 있다. 그러나 이러한 경우에도 클라이언트가 해당 매개변수나 반환값을 수정하지 않도록 명확히 문서화해야 한다.
이러한 원칙을 지킴으로써 객체의 불변성을 보장하고, 예기치 않은 변경으로부터 시스템을 안전하게 보호할 수 있다.
- 객체를 제공받을 때는 받아들인 객체가 변경되어도 정상적으로 작동할지 생각해보고 정상적으로 작동하지 않을 것 같다면, 잠재적으로 변경되지 않도록 방어적 복사를 하자.
- 이러한 객체가 Set 혹은 Map에 들어간 뒤에 필드 값이 변경된다면, 불변식이 깨질 수도 있다.
- 되도록 기본 지원 불변 객체를 활용하자. (LocalDateTime)
- 방어적 복사를 하기에는 복사 비용이 너무 크다면, 문서에서 해당 요소를 수정했을 때의 책임은 클라이언트에게 있음을 명시해놓자.
참고 및 출처