알다시피 자바는 가비지컬렉터로 인해 메모리 충돌 오류에서 안전하다.
하지만 클라이언트가 불변식을 깨뜨리려 혈안이 되어있다고 가정하고 방어적으로 프로그래밍 해야한다.
어떤 객체던 그 객체의 허락 없이는 외부에서 내부를 수정하는 일은 불가능하다.
하지만 주의를 기울이지 않으면 허락되는 경우가 생길 수 있다.
자바에서 불변식이 깨진다는 것은, 객체의 상태가 예상치 못한 방식으로 변경되어 객체가 의도한 동작만을 수행할 수 없게 된다는 것이다.
- 데이터 무결성이 손실 될 수 있다 : 객체가 가지고 있는 데이터가 더이상 유효하지 않게 된다. 예를 들어, 금융 애플리케이션에서 계좌잔액이 음수가 되지 않도록 하는 불변식이 깨질 경우, 잘못된 잔액 정보가 발생할 수 있다.
- 예기치 않은 예외 발생 : 객체의 상태가 불안정하다면, 객체를 사용하는 메서드 호출 시 예기치 않은 동작이 발생할 수 있다.
NullPointerException이 발생하면 다행이고 객체가 변경된 시점에서 한참 로직이 진행되어 어디가 문제점인지 찾기 힘들어질 수 있다.
코드의 규모가 커질 수록 에러가 발생했을 때 행복도 지수가 크게 상승 할 수 있다...^^ 이게 개발이지~
또한 협업 시에 범인으로 특정되면 호감작과 명예작을 할 수도 있겠다.
- 멀티스레드 환경에서 동기화 문제 : 멀티스레드 환경에서 불변 객체는 동기화 걱정 없이 안전하게 공유될 수 있다. 그러나 불변식이 깨질 경우, 여러 스레드가 객체의 상태를 동시에 변경할 수 있어 경쟁 상태나 데이터 일관성 문제가 발생 할 수 있다.
진행했던 프로젝트에서는 모델을 만들 때 불변 객체로 생성하는 것이 코드 컨벤션 중 하나였기 때문에,
후에 멀티스레드를 사용하는 부분에서 일관성에 대한 안전성을 높일 수 있었다.
/ 코드 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 가 존재하지 않으니 불변으로 보이지만 Date는 가변이다.
Date 클래스의 인스턴스는 수정이 가능하고, 이 클래스의 메서드를 통해 내부 상태를 변경할 수 있다.
final 키워드는 참조 자체의 불변성만을 보장할뿐, 참조된 객체의 불변성은 보장하지 않는다.
Java8 이후로는 Date 대신 불변인 Instant/LocalDateTime/ZonedDateTime 를 사용하자.
그러나 문제는 오래된 API에 Date가 많이 사용되어 있다는 것이다.
외부 공격으로부터 내부를 보호하려면 생성자에서 받은 기본 매개변수 각각을 방어적으로 복사해야 한다.
public Period(Date start, Date end) {
this.start = new Date(start.getTime());
this.end = new Date(end.getTime());
if (this.start.compareTo(this.end) > )
throw new IllegalArgumentException(this.start + " after " + this.end);
}
방법은 간단하다. 기존 값을 생성자를 이용해 새로 생성하자.
매개변수의 유효성을 검사하기 전에 방어적 복사본을 만들고, 이 복사본으로 유효성을 검사해야 한다.
멀티스레딩 환경이라면 원본 객체의 유효성을 검사한 후 검사본을 만드는 찰나의 순간에 다른 스레드가 원본 객체를 수정할 위험이 있기 때문이다.
매개변수가 제3자에 의해 확장될 수 있는 타입이라면 clone을 사용하면 안된다.
Date는 final이 아니므로 clone이 Date가 정의한게 아닐 수도 있다.
즉 clone이 악의를 가진 하위 클래스의 인스턴스를 반환할 수 있다.
// 수정한 접근자 - 필드의 방어적 복사본 반환
public Date start() {
return new Date(start.getTime());
}
public Date end() {
return new Date(end.getTime());
}
때문에 접근자에 가변 필드의 방어적 복사본을 반환해야 한다.
Date와 같은 객체, 길이가 1이상인 배열 등 항상 가변의 특징을 가진 필드를 사용할 때는 주의를 기울이자.
또한 방어적 복사는 성능 저하가 따르고 항상 사용할 수 있는 것도 아니기 때문에,
생략할 때에는 해당 매개변수의 반환값을 수정하지 말아야 함을 명확히 문서화 하도록 하자.
'JAVA > 이펙티브 자바' 카테고리의 다른 글
Java. 오버로딩, 오버라이딩은 신중히 사용하자 ( 다중 정의 ) (0) | 2024.03.06 |
---|---|
Java. 명명규칙은 통용되는 것으로 지키자. (1) | 2024.03.06 |
Java. 스트림이란? (2) | 2024.01.02 |
43. 람다보다는 메서드 참조를 사용하라 ( 람다 표현식과 메서드 참조 ) (0) | 2023.08.02 |
42. 익명 클래스보다는 람다를 사용하라 ( 람다 표현식 ) (0) | 2023.07.31 |