C나 C++ 같은 언어에서 흔히 보이는 버퍼 오버런, 배열 오버런, 와일드 포인터와 같은 메모리 충돌 오류에서 안전하고, 자바로 작성한 클래스는 시스템의 다른 부분에서 무슨 짓을 하든 불변식이 지켜지기 때문이다
그러나, 아무리 자바라도 다른 클래스로부터의 침범을 다 막을 수 있는 것은 아니다
불변식을 지키지 못한 클래스
보통 객체의 허락 없이는 외부에서 내부를 수정하는 일은 불가능하다
하지만, 의도치 않게 외부에서 내부를 수정하도록 허락하는 상황이 생길 수도 있다
Period 클래스는 값이 한 번 정해지면 변하지 않게 할 의도로 만들었다고 가정해보자
import java.util.Date;
class Period {
private final Date start;
private final Date end;
public Period(Date start, Date end) {
if(start.compareTo(end) > 0) {
try {
throw new IllegalAccessException(this.start + "after" + this.end);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
this.start = start;
this.end = end;
}
public Date start() { return start; }
public Date end() { return end; }
// ... 생략
}
class Item50 {
public void test() {
Date start = new Date();
Date end = new Date();
Period period = new Period(start, end);
// period의 내부를 수정
end.setYear(3);
}
public static void main(String[] args) {
Item50 main = new Item50();
main.test();
}
}
- Period 클래스는 불변처럼 보이는데, 하지만 자바에서 제공하는 `Date` 클래스는 가변이어서 쉽게 불변식을 깨뜨릴 수 있다
```java
// Period 인스턴스의 내부를 공격하는 첫 번째 방법
Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
end.setYear(78); // p의 내부를 수정한다
Period 인스턴스의 값들이 한 번 정해지면 변하지 않게 할 의도로 만들었지만, 너무 쉽게 내부 값이 변경된다
가변인 Date 클래스로 인한 사항이다
Date 클래스는 오래된 API이므로, Date 대신 불변인 Instant 혹은 LocalDateTime 이나, ZonedDateTime 을 사용하면 문제는 쉽게 해결된다
해결 방안
생성자 매개변수의 방어적 복사본을 만든다
생성자에서 받은 가변 매개변수 각각을 방어적으로 복사하면 된다
즉 , Period 내부에서는 원본이 아닌 복사본을 사용해야 한다
public final class Period {
private final Date start;
private final Date end;
// 수정한 생성자 - 매개변수의 방어적 복사본을 만든다.
public Period(Date start, Date end) {
this.start = new Date(start.getTime());
this.end = new Date(end.getTime());
if (this.start.compareTo(this.end) > 0)
throw new IllegalArgumentException(this.start + "after" + this.end);
}
public Date start() {
return start;
}
public Date end() {
return end;
}
}
멀티스레딩 환경이라면, 무조건 이렇게 작성해야 한다
유효성 검사를 수행한 수 복사본을 만드는 찰나에 다른 스레드가 원본 객체를 수정할 위험이 있기 때문이다
또한, 방어적 복사에는 Date의 clone() 메서드를 사용하지 않는다
Date는 final 클래스가 아니라서 매개변수가 제 3자에 의해 확정될 수 있는 타입이라면 방어적 복사에 clone을 사용해서는 안된다
Period getter를 이용하여 공격
// Period 인스턴스의 내부를 공격하는 두 번째 방법
Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
p.end().setYear(78); // getter 메서드를 이용해 값을 변경한다.
가변 필드의 방어적 복사본을 반환한다
public Date start() {
return new Date(start.getTime());
}
public Date end() {
return new Date(end.getTime());
}
Period 자신만이 가변 필드에 접근 할 수 있고, 모든 필드가 객체 안에서 완벽히 캡슐화 된다
아이템 50. 적시에 방어적 복사본을 만들어라
불변식을 지키지 못한 클래스
class Period { private final Date start; private final Date end;
}
class Item50 { public void test() { Date start = new Date(); Date end = new Date(); Period period = new Period(start, end);
}
Date
클래스로 인한 사항이다Date
클래스는 오래된 API이므로, Date 대신 불변인Instant
혹은LocalDateTime
이나,ZonedDateTime
을 사용하면 문제는 쉽게 해결된다해결 방안
생성자 매개변수의 방어적 복사본을 만든다
즉 , Period 내부에서는 원본이 아닌 복사본을 사용해야 한다
clone()
메서드를 사용하지 않는다Period getter를 이용하여 공격
가변 필드의 방어적 복사본을 반환한다