자바는 네이티브 메서드를 적극 사용하며, 자바로 작성한 클래스는 불변식을 지키고자 한다. (불변식: 어떤 객체의 유효한 상태를 규정함 / 변하지 않음 / 항상 참이되는 조건)
메모리 전체를 하나의 거대한 배열로 다루고 있는 C나 C++에서는 편하게 누리지 못하는 강점이다
하지만 아무 노력 없이 다른 클래스로부터의 침범을 막을 수 있는 것이 아니다.
항상 불변식이 '어디선가' 깨질 수 있다고 생각하며 방어적으로 프로그래밍 해야 한다.
적절치 않은 상황에 대비하여, 클래스를 보호하기 위한 시간을 써야 한다. 다시말해, 객체의 허락 없이는 내부를 수정하도록 두면 안 되는데 나도 모르게 내부 수정을 허락해버리는 경우를 주의해야 한다.
가변 클래스가 객체에 사용되는 것을 유의하자
예를 들어 이런 상황을 경계해야 한다. (불변식을 지키지 못하는 클래스 예시)
다음의 Period 클래스는 Date 클래스를 멤버로 가지고 있다.
Period 내부에서 아무리 불변식을 지키려 노력해도, Date 자체가 가변이기 때문에 Period의 불변식을 와장창 깨뜨릴 수 있다.
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 + " after " + end);
this.start = start;
this.end = end;
}
public Date start() {
return start;
}
public Date end() {
return end;
}
...
}
이 상황에서 Period 인스턴스 내부를 공격해볼 수 있다.
이렇게. Date가 가변이므로. Date 타입인 end라는 필드를 변경해버릴 수 있다.
즉 불변식을 깨뜨릴 수 있다.
Date start = new Date();
Date end = new Date();
Period p = new Period(start, end);
end.setYear(78); // p의 내부를 수정해버렸다...
∨ 참고로 Date는 낡은 API다! 사용하지 말자!
∨ Instant, LocalDateTime, ZonedDateTime을 사용하도록 하자.
∨ Date 뿐 아니라, 가변인 낡은 값 타입을 사용하는 것을 경계하자.
∨ 여러 API의 내부 구현에 낡은 값 타입의 잔재가 남아있기도 하다.
↓ 그럼 어떡할까?
↓ 이렇게 해보자.
낡은 코드를 방어적으로 복사하기
낡은 타입 (가변 타입)은 사용하지 말아야 하는데, 여러 API에 잔재가 남아있다고 했다.
그렇다면, 그런 낡은 코드는 어떻게 대처해야 할까?
그러니까, 외부 공격으로부터 내부를 어떻게 보호할까?
생성자에서 받은 가변 매개변수 각각을 "방어적으로 복사(defensive copy)" 해야 한다.
인스턴스 안에서는 "원본이 아닌 복사본"을 사용하자는 것이다.
아래와 같이 새로 작성한 생성자를 사용하면 더 이상 낡은 타입이 위협이 되지 않는다.
// start, end 파라미터 원본 그대로 사용하는 것이 아님
// Date의 getTime()을 사용해서 얻은 long값으로 new 하고 있음
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 enw IllegalArgumentException(
this.start + " after " + this.end);
}
∨ 유의1: 매개변수의 유효성을 검사하기 전에, 방어적 복사본을 만들고, 이 복사본으로 유효성을 검사했다는 것을 유의!
└ 반드시 이렇게 작성해야 한다. 멀티스레딩 환경을 생각해보자. 원본 유효성 검사 후 복사본을 만든다면, 그 사이에서 다른 스레드가 원본 객체를 수정할 위험이 있겠지.
└ 참고: 'time-of-check/time-of-use(검사시점/사용시점) 공격', 'TOCTOU 공격' 으로 부르기도!
∨ 유의2: 복사를 할 때, Date의 clone메서드를 사용하지 않았다.
└ 매개변수로 가변 타입을 들였을 경우, 해당 매개변수의 방어적 복사본을 만들 때 clone을 사용해서는 안 된다. 예를 들어 가변타입 Date는 final이 아니므로, Date의 구현체를 받아왔을 때 clone을 악의적으로 재정의된 상태일 위험이 있음. 이럴 경우 그 악의적 구현체가 Date필드의 값에 접근해서 따로 저장해둘 수 있다. 공격자에게 노출된단 말이다.
그러므로 매개변수로 가변 타입을 들였다면, clone이 재정의되었을 위험을 경계해야 한다.
다른 메서드가 가변 타입을 노출중인지도 유의하자.
생성자만 신경쓰면 안 된다.
다음의 접근자 메서드가 내부의 가변 정보를 직접 리턴하여
가변 필드 객체가 그대로 노출되고 있다.
public Date start() {
return start;
}
public Date end() {
return end;
}
그래서 여기서도 마찬가지로 방어적 복사본을 리턴하도록 하자.
원본 객체 리턴 금지 & 복사본을 리턴하자!
public Date start() {
return new Date(start.getTime());
}
public Date end() {
return new Date(end.getTime());
}
*참고: 생성자에서는 clone을 사용하지 않았지만, 접근자 메서드에서는 clone을 사용해도 된다. 이미 이 객체의 Date타입 필드는 Date타입임이 확실하기 때문이다. 매개변수에 정체모를 악의적 하위클래스가 들어올 수 있는 것과는 달리, 객체 내부의 필드는 확실한 클래스 타입이라고 신뢰할 수 있다. 그래도 Item13에 의해, 인스턴스 복사에는 일반적으로 생성자나 정적 팩터리를 쓰는게 좋다고 한다.
∨ 이렇게 완벽한 불변식을 갖추도록 신경을 써야 한다.
∨ 이제야 '시작 시간이 종료 시각보다 나중일 수 없다'는 불변식을 만족하게 된다.
∨ 모든 필드가 객체 안에 완벽하게 캡술화되었다.
매개변수를 방어적으로 복사하는 여러 케이스
─ 위와 같이 '불변 객체를 만들기 위해서'도 있다.
─ '제공받은 객체의 참조를 내부 자료구조에 보관해야 하는 상황에서, 그 객체가 잠재적으로 변경될 수 있는 가능성'을 생각해보아도 그렇다. 만약 내 클래스가 객체를 제공받았는데, 객체가 변경되었다고 하자. 내 클래스는 문제없이 동작할 수 있는가? 확신할 수 없다면, 객체 원본이 아니라 객체의 복사본을 내 클래스의 자료구조에 저장해야 한다. 객체를 담고 있는 자료구조의 불변식을 지켜야 한다.
─ '내부 객체를 클라이언트에 넘겨줄 때'도 그렇다. 특히 내 클래스의 가변 객체를 다른 곳에 넘겨줄 때 특히 조심해야 한다. 그러니 원본을 노출하지 말고, 복사본을 반환하는 쪽으로 생각을 하자. 특히 흔한 상황은 배열이다. 배열의 요소는 직접 참조할 수 있다. 즉 배열 요소는 가변이다. 따라서, 내부에서 사용하는 배열을 반환할 때 "항상 방어적 복사를 수행해야 한다!" Item15를 참고할 수 있다.
방어적 복사시 유의점!
∨ 되도록 불변 객체를 조합해 객체를 조합해야 방어적 복사를 할 일이 줄어든다. (Item17)
∨ 방어적 복사는 성능 저하가 따르고, 항상 방어적 복사가 가능한 것도 아니다.
∨ 호출자에게 "매개변수나 반환값을 수정하지 말라" 라고 명확히 문서화하는 것이 좋다.
방어적 복사를 생략해도 되는 상황은?
∨ 넘겨주는 자가 그 객체의 통제권을 명백히 이전하는 의도를 가진 경우! 이때 넘겨주는 자는 더 이상 가변 객체를 더 이상 직접 수정할 일이 없다고 약속해야 한다. 넘겨주는자, 넘겨받는자 양측의 문서에 확실히 명시해야 한다.
∨ 넘겨준 자와 넘겨받은 자가 상호 신뢰할 수 있을 때
∨ 넘겨준(넘겨받은) 클래스의 불변식이 깨지더라도, 그 영향이 오직 넘겨준 자에게 국한될 때
└ (예를들어 Item18:래퍼 클래스 패턴 ─ 래퍼 클래스 특성상, 넘겨준자는 여전히 래퍼에 넘긴 객체에 직접 접근할 수 있다. 래퍼 클래스의 불변식은 파괴되지만, 그 영향은 오직 넘겨준자만 받게 된다! 구성요소를 수정했을 때의 책임이 오로지 넘겨준자에 있음을 문서에 명시해야 한다)
'JAVA > Effective Java' 카테고리의 다른 글
[이펙티브 자바] null이 아닌, 빈 컬렉션이나 배열을 반환하라 ─ 8장:메서드:Item54 (0) | 2023.10.18 |
---|---|
[이펙티브 자바] 가변인수는 신중히 사용하라 ─ 8장:메서드:Item53 (0) | 2023.10.18 |
[이펙티브 자바] 다중정의(오버로딩)는 신중히 사용하라 ─ 8장:메서드:Item52 (1) | 2023.10.17 |
[이펙티브 자바] 메서드 시그니처를 신중하게 설계하라 ─ 8장:메서드:Item51 (0) | 2023.10.17 |
[이펙티브 자바] 매개변수가 유효한지 확인하라 ─ 8장:메서드:Item49 (2) | 2023.10.16 |