자동차 경주 미션과 로또 미션을 진행하면서 필연적으로 collection을 많이 사용하게 된다. 그리고 이런 collection을 사용하면서 참조, 복사 등과 관련된 문제가 종종 발생하곤 한다.
특히나 객체의 불변성을 유지하고자 할 때 이 문제는 두드러진다. 불변 객체를 만들기 위해 필드를 private final로 유지하더라도 collection을 사용하는 경우에는 불변성이 보장되지 않는다. 참고
그래서 collection의 복사본을 만드는 일은 중요하다. 복사본을 만듦으로써 참조를 끊고, 의도하지 않은 수정이 영향을 끼치는 일이 없도록 만들 수 있다.
물론 이 복사는 아니다.
collection을 복사하는 방법에는 여러 가지가 있다. 가장 간단하게는 new 키워드를 사용하여 새로운 객체를 만들어 복사해주는 방법이 있다. (앞으로 편의상 collection의 복사에 대한 모든 설명은 리스트를 기준으로 설명하기로 하자.)
List<String> list1 = new ArrayList<>();
List<String> list2 = new ArrayList<>(list1);
list1.add("Hello World!")
System.out.println(list2.size()); // 0 출력
// list1에 값을 추가하더라도 list2에 영향을 끼치지 않는다.
new 키워드를 통한 복사는 기존 리스트, 즉 파라미터로 넣어준 리스트와의 참조를 끊어준다. 이 때 새로 생긴 리스트는 객체의 추가, 제거, 변경이 가능한 평범한 리스트로, 단지 원본 리스트와의 참조만 끊어진 리스트일 뿐이다. 따라서 이 리스트는 가변이다.
new 키워드 외에 collection의 자체적인 정적 메서드로 copyOf라는 메서드가 존재한다. 메서드 이름에서 알 수 있듯이 이 메서드는 collection의 복사본을 반환한다.
List<String> list1 = new ArrayList<>();
List<String> list2 = List.copyOf(list1);
list1.add("Hello World!");
System.out.println(list2.size()); // 0 출력
// list1에 값을 추가하더라도 list2에 영향을 끼치지 않는다.
list2.add("Hello World!"); // 에러가 발생한다.
copyOf 메서드 역시 new 키워드를 통한 복사처럼 기존 객체와의 참조를 끊어주는 역할을 한다. 하지만 copyOf는 단순히 참조를 끊는 것을 넘어 한 가지 역할을 더 해주는데, 반환하는 collection을 Immutable로 만들어준다. copyOf 메서드를 뜯어보자.
static <E> List<E> copyOf(Collection<? extends E> coll) {
return ImmutableCollections.listCopy(coll);
}
copyOf 메서드는 ImmutableCollections라는 객체의 listCopy 메서드를 호출한다. 이름에서 알 수 있듯이, ImmutableCollections는 불변이다.
static <E> List<E> listCopy(Collection<? extends E> coll) {
if (coll instanceof AbstractImmutableList && coll.getClass() != SubList.class) {
return (List<E>)coll;
} else {
return (List<E>)List.of(coll.toArray());
}
}
한번 더 들어가서 ImmutableCollections.listCopy를 뜯어보면, 복사 원본인 coll이 AbstractImmutableList면서 SubList가 아니면 coll을 그대로 반환하고, 아닌 경우에는 coll을 배열로 변환해 준 뒤 List.of를 호출해서 반환한다. List.of는 메서드로 들어온 배열을 불변 리스트로 만들어준다.
coll이 AbstractImmutableList의 구현체인 경우 List.of를 호출하지 않는 것을 볼 수 있는데, 이는 AbstractImmutableList에서 add, remove 등의 메서드를 호출할 경우 UnsupportedOperationException을 던지도록 이미 설계되어 있기 때문이다.
어? 그렇다면 AbstractImmutableList의 instance인 경우 복사 시에 참조를 끊지 않는 것이 아닌가? 라고 생각할 수 있다. 그렇다. coll을 그대로 반환하기 때문에 참조를 끊지 않는다. 하지만 복사 시에 참조를 끊어주는 이유를 생각해 본다면 이해할 수 있다.
복사 시 참조를 끊지 않으면 참조 원본이 변했을 때 복사한 리스트가 변할 수 있다.
따라서 복사 전 리스트가 이미 불변이므로 굳이 참조를 끊어줄 필요가 없다. 때문에 ImmutableList를 List.copyOf로 복사할 때는 따로 참조를 끊어주지 않는다.
즉, List.copyOf는 리스트를 복사해서 "불변"으로 던지는 것을 핵심으로 한다. 물론, 복사 시 복사 원본이 AbstractImmutableList의 인스턴스일 경우가 흔하지 않으므로 일반적으로는 원본과의 참조 역시 끊어준다고 볼 수 있다.
리스트를 getter 등으로 반환 시에 리스트에 수정을 가해서 필드 값이 변하도록 하지 못하도록 Collections.unmodifiableList() 메서드를 통해 unmodifiable로 만들어서 반환 해주는 경우가 종종 있다. unmodifiableList와 같은 unmodifiable collection 역시 add, remove 등의 리스트 수정 메서드를 호출할 경우 UnsupportedOperationException이 발생한다. 그렇다면 unmodifiableList와 List.copyOf를 사용하는 데 차이가 없지 않느냐는 의문이 생길 수 있다.
하지만 둘은 결정적인 차이점을 가지고 있는데, List.copyOf는 (불변성이 보장되지 않을 경우) 원본과의 참조를 끊어주지만, Collections.unmodifiableList()로 만든 리스트는 참조가 끊어져 있지 않다. 따라서, Collections.unmodifiableList()로 반환한 리스트를 직접 수정할 수는 없지만, 참조 원본을 수정할 수 있다면 불변성을 깨뜨릴 수 있다. "unmodifiable"이지 "immutable"이 아님에 주의하자.