parameter
로 Collection
이나 Array
같이 참조형을 받거나, 반환하는 경우 반드시 방어적 복사를 고려해야한다. 객체 외부에서 참조값을 통해 자유롭게 내부 값을 변경할 수 있기 때문이다.
public class Cars {
private final List<Car> cars;
public Cars(List<Car> cars) {
this.cars = cars;
}
public List<Car> getCars() {
return cars;
}
}
Cars
객체는 List<Car>
를 parameter
로 받아 final
cars 필드의 참조 값에 할당한다. 또한 getCars
메소드를 통해 List<Car>
를 반환한다. 얼핏 보면 별다른 문제가 없을 듯한 간단한 클래스인데, 어떤 문제가 있을까? 아래 코드를 잠시 생각해보자.
public static void main(String[] args) {
List<Car> carGroup = new ArrayList<>(Arrays.asList(new Car("pobi"), new Car("sojukang")));
// 변경 가능한 List를 위해 ArrayList 내부에 Arrays.asList로 초기화
Cars cars = new Cars(carGroup);
carGroup.add(new Car("beer"));
System.out.println(cars.getCars().toString());
}
코드를 실행시키면 어떻게 될까? add
메소드에서 예외를 터트릴까? 아니면 carGroup
과 cars
의 List
는 별개의 객체라서 두 개의 Car
를 담은 리스트를 출력할까? 그것도 아니면...
[Car{name='pobi'}, Car{name='sojukang'}, Car{name='beer'}]
놀랍게도 cars
외부에 존재하는 carGroup
에서 호출한 add
메소드에의해 cars
의 List
에도 영향이 미친 것을 알 수 있다. 그렇다! carGroup
과 cars
내부의 List
는 동일한 참조 값을 가진다.
carGroup
과 cars
의 List
의 참조 값이 같다. 심지어 원소들의 참조 값도 같다. final
을 사용해서 필드를 불변으로 만드려 했고, 객체 변경을 위한 메서드를 만들지 않았음에도 객체는 완전히 변경 가능한 못 믿을 객체가 되었다. 참조 값을 주고받을 경우 변경에 취약하게 되고, 외부의 잘못된 사용에 그대로 노출될 수 있다.
문제 해결을 위해선 생성자에서 방어적 복사
를 하고, 복사된 객체를 필드에 할당하면 된다. Cars
를 다음과 같이 수정하고 동일한 테스트를 해본다.
public Cars(List<Car> cars) {
this.cars = List.copyOf(cars); // 7 버전부터 제공하는 방법. Immutable collection을 반환한다.
// this.cars = new ArrayList<>(cars); 와 같은 방법도 된다. 그러나 변경 가능하다.
}
위와 동일한 테스트를 통해 객체의 주소를 관찰해보면,
carGroup
과 cars
의 List
가 다른 참조 값을 가지고, cars
에는 변화가 없는 것을 볼 수 있다. 이로써 외부의 변경이 객체에 미치지 않게 되어 더 믿을만한 객체가 되었다. 또한 생성자의 검증 메서드를 복사 이후에 호출하면 멀티 스레드 환경에서의 복사본을 만드는 사이 일어나는 변화에도 안전하게 객체를 생성할 수 있다. getter
메소드도 동일하게 방어적 복사
된 결과를 반환하면 될 것이다.
클라이언트가 여러분의 불변식을 깨뜨리려 혈안이 되어 있다고 가정하고 방어적으로 프로그래밍 해야한다. 생상자에서 받은 가변 매개변수 각각을 방어적으로 복사(defensive copy)해야 한다.
- Effective java Item 50, 개앞맵시 역
위의 방어적 복사엔 문제점이 있다. List
의 참조 값은 복사되어 다른 참조 값을 가지지만 내부 원소 객체의 참조 값은 동일하다. carGroup
의 원소인 Car
를 변경하면 cars
도 변경될 수밖에 없다. 이러한 상황까지 막기 위해 내부 원소까지 완벽하게 깊은 복사하려면 어떻게 해야할까? 안타깝게도 java
기본 라이브러리에서는 지원하지 않는다. 개발자가 상황에 맞춰 직접 해주어야 한다. Cars
를 다음과 같이 수정해보자.
public Cars(List<Car> cars) {
List<Car> copiedCars = List.copyOf(cars);
this.cars = elementsCopy(copiedCars);
}
private List<Car> elementsCopy(List<Car> carsInput) {
return carsInput.stream()
.map(Car::new)
.collect(toList());
}
Car
를 복사하기 위해 Car
를 받아 복사하는 생성자를 새로 정의하였다. 테스트 결과는 다음과 같다.
List
의 참조 값은 물론, 내부 원소인 Car
들의 참조 값까지 새롭게 생성된 것을 알 수 있다.
이제 Cars
객체는 외부의 접근에서 안전할까? 그렇지 않다. 아직 객체 내부에서 일어나는 변경에는 안전하지 못하며, getCars
를 통해 반환된 List
는 외부에서 수정할 수 있을 것이다. 이 문제를 해결하기 위해 Unmodifiable Collection
사용을 고려해봐야한다.
Unmodifiable Collection
을 통해 만들어진 Collection
은 읽기 용도로만 사용할 수 있으며, set
, add
, addAll
등을 통해 Collection
에 변경을 가하는 메서드를 호출하면 UnsupportedOperationException
이 발생한다. 이를 사용해서 Cars를 다음과 같이 수정해볼 수 있다.
private List<Car> elementsCopy(ArrayList<Car> carsInput) {
return carsInput.stream()
.map(Car::new)
.collect(toUnmodifiableList());
}
public List<Car> getCars() {
return unmodifiableList(cars);
}
이제 Cars
객체의 List
에 내부, 외부의 변경 시도가 일어날 경우 예외를 던질 것이고, 보다 불변성을 유지하기 쉬운 객체가 되었다.
방어적 복사와 Unmodifiable Collection
[JAVA] List 객체 복사 방법과 Collections.copy()에 관한 고찰
[Java] 방어적 복사(Defensive copy)
조슈아 블로크, 개앞맵시 역, 이펙티브 자바, 3판