생성자를 통해 초기화 할 때, 새로운 객체로 감싸서 복사해주는 방법이다. 외부와 내부에서 주소값을 공유하는 인스턴스의 관계를 끊어주기 위함이다.(예를 들어 외부에서 add했는데, 내부에 add가 반영되는 경우)
public Cars(Racers racers) {
this.cars = new ArrayList<>(generateCars(racers)); // 외부와의 관계 끊기
}
만약 생성자에서 유효성 검증이 필요하다고 하자. 일반적으로는 아래와 같이 넘어온 값을 검증하고 객체 내부변수에 검증된 값을 할당해주는 순서가 맞다고 생각할 수 있다.
public Period(Date start, Date end) {
if (validation(start, end)) {
throw new IllegalArgumentException("");
}
this.start = start;
this.end = end;
}
하지만 Date같은 경우는 값이 변경될 수 있으므로 생성자에서 매개변수의 유효성을 검사하기 전에 방어적 복사본을 만들고, 이 복사본을 이용해서 유효성을 검증한다. 만약 멀티쓰레딩 환경일 경우 유효성 검사부터 후 복사본을 만드는 사이에 원본 값이 변경될 위험이 있기 때문이다.
public Period(Date start, Date end) {
this.start = new Date(start.getTime()); // 방어적 복사
this.end = new Date(end.getTime()); // 방어적 복사
if (validation(this.start, this.end)) {
throw new IllegalArgumentException("");
}
}
clone을 사용해서 본사본을 만들 수도 있지만, 생성자의 매개변수가 final 클래스가 아니어서 확장될 수 있는 타입이면 방어적 복사본을 만들때 clone을 사용하면 안된다.(재정의된 clone이 호출될 수 있기 때문)
하지만 생성자에서만 복사본을 만든다고 내부 값을 변경할 수 없는 것은 아니다.
getter로 얻은 객체를 통해 얼마든지 내부 값이 변경될 수 있다.
List<Car> carList = cars.getList();
carList.add(); // 값이 변경될 수 있다.
period.getStart().setTime(); // getter를 통해 가져온 값으로 내부 값 변경 가능
따라서 getter에서도 복사본을 리턴해줘야한다.
public List<Car> toList() {
return Collections.unmodifiableList(cars);
}
public Date getStart() {
return new Date(start.getTime());
}
생성자의 매개변수로 프리미티브 타입이 넘어온다면 애초에 값이 복사되어 전달되므로 유효성검사 후 그냥 할당해줘도 되지만, 생성자의 매개변수로 객체가 넘어온다면 먼저 복사본을 만든 후 유효성 검사를 해주는 것이 좋다.
즉, 불변성이 유지된다면 검증부터 해줘도 되지만 그걸 보장할 수 없다면 복사본을 만든 후, 복사본으로 검증해주는 것이 좋다.
또 getter도 복사본을 리턴해줘야 완벽한 캡슐화라고 볼 수 있다.
뭐야 왜 또 너야