-
네이티브 메서드를 사용하지 않음
> 버퍼 오버런, 배열 오버런, 와일드 포인터 같은 메모리 충돌 오류에서부터 안전 -
자바의 클래스는 불변식이 지켜짐
-
아무리 자바라고 해도 다른 클래스로부터의 침범을 아무런 노력없이 막을 수 있는 것은 아님
-
악의적인 의도를 가진 사람들이 시스템의 보안을 뚫으려는 시도가 늘고 있음
-
평범한 프로그래머도 순전히 실수로 클래스를 오작동하게 만들 수 있음
🌟 클라이언트가 불변식을 깨뜨리려 혈안이 되어 있다고 가정하고 방어적으로 프로그래밍 해야 함
예시 : 시작 시간(start)이 종료 시간(end) 보다 빨라야 하는 Period 객체
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 + "보다 늦다."
);
}
this.start = start;
this.end = end;
}
public Date start() {
return start;
}
public Date end() {
return end;
}
}
Date start = new Date();
Date end = new Date();
Period period = new Period(start, end);
end.setYear(78); // period 내부 변경 성공!
자바 8 이후로 Date
대신 불변 인스턴스를 사용하면 쉽게 해결할 수 있음
(혹은 LocalDateTime
이나 ZonedDateTime
을 사용해도 됨)
Date 는 낡은 API 이니 새로운 코드를 작성할 때는 더 이상 사용하면 안됨
코드를 작성할 때 setYear() 에 취소선과 함께 'setYear(int) is deprecated' 라고 뜸
변경 불가능을 위해 사용을 막은 것인지 찾아보니,
국제화와 잘 맞지 않고, 월(Month) 계산이 불편하고, mutable 하다는 이유로
더 이상 사용되지 않는다고 함
그래서 자바 8에 추가된 것이 LocalDate, LocalTime, LocalDateTime !
Date 는 final 이 아니지만, 해당 객체들은 final 임
그렇지만, 예전에 작성된 낡은 코드들은?
생성자에서 받은 가변 매개변수 각각을 방어적으로 복사
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 + "가 " + this.end + "보다 늦다."
);
}
}
public Date start() {
return start;
}
public Date end() {
return end;
}
}
매개변수의 유효성 검사 전 방어적 복사본을 만들고, 복사본으로 유효성 검사
멀티스레드 환경에서 원본 객체의 유효성 검사 후 복사본을 만드는 찰나에
다른 스레드가 원본 객체를 수정할 위험이 있음
> 검사시점/사용시점(time-of-check/time-of-use) 공격 혹은 TOCTOU 공격이라 함
매개변수가 제 3자에 의해 확장될 수 있는 타입이라면 방어적 복사본을 clone을 사용해 만들면 안됨
Date 는 final이 아니므로 clone 이 Date 가 정의한 것이 아닐 수 있음
즉, clone이 악의를 가진 하위 클래스의 인스턴스를 반환할 수도 있음
제 3자에 의해 확장될 수 있는 타입 = final 클래스가 아닌 경우
clone() 을 사용한 예시
전달 받은 객체가 Date가 아니라 이를 상속한 객체(Data2)의 clone이 실행될 수 있음
public Period(Date start, Date end) {
this.start = (Date)start.clone();
this.end = (Date)end.clone();
if (this.start.compareTo(this.end) > 0) {
throw new IllegalArgumentException(
this.start + "가 " + this.end + "보다 늦다."
);
}
}
class Date2 extends Date {
public Date2() {
super();
}
@Override
public Object clone() {
System.out.println("나지롱");
return super.clone();
}
}
public static void main(String[] args) {
Date start = new Date();
Date2 end = new Date2();
Period period = new Period(start, end);
end.setYear(78);
}
Date start = new Date();
Date end = new Date();
Period period = new Period(start, end);
period.end().setYear(78); // period 내부 변경 성공!
두번째 공격을 막으려면, 단순히 접근자가 가변 필드의 방어적 복사본을 반환하면 됨
public Date start() {
return new Date(start.getTime);
}
public Date end() {
return new Date(end.getTime);
}
새로운 접근자까지 갖추면 Period는 완벽한 불변임
시작시각이 종료시각보다 나중일 수 없다는 불변식을 위배하게 할 방법은 없음
예외 : 네이티브 메서드나 리플렉션 같은 언어 외적인 수단을 동원하는 경우
Java Native method
다른 언어로 작성된 코드를 자바에서 호출하도록 만들어진 규약
현재는 C/C++ 에 대한 호출만을 정확하게 지원
Java Reflection
객체를 통해 클래스의 정보를 분석해 내는 프로그램 기법
자신 말고는 가변 필드에 접근할 방법이 없고, 모든 필드가 객체 안에 완벽히 캡슐화 됨
생성자와 달리 접근자 메서드에서는 방어적 복사에 clone을 사용해도 됨
Period가 가지고 있는 Date 객체가 java.util.Date 임이 확실하기 때문
하지만 인스턴스 복사에는 일반적으로 생성자나, 정적 팩터리를 쓰는 것이 좋음 (아이템 13)
> 클래스가 클라이언트로부터 받는 혹은 클라이언트로 반환하는 구성요소가 가변
이라면
그 요소는 반드시 방어적으로 복사
해야 함
> 복사 비용이 너무 크거나
, 클라이언트가 그 요소를 잘못 수정할 일이 없음을 신뢰
한다면
방어적 복사를 수행하는 대신 구성요소를 수정했을 때의 책임이 클라이언트에 있음을 문서에 명시
해야 함