카프카 컨슈머 코드리뷰를 하다가 'Unmodifiable Collection'이라는 것을 보게 되었다. 카프카에서 본 소스코드는 다음과 같았다.
private List<ConsumerRecord<K, V>> drainRecords(int n) {
if (isDrained() || position >= records.size()) {
drain();
return Collections.emptyList();
}
// using a sublist avoids a potentially expensive list copy (depending on the size of the records
// and the maximum we can return from poll). The cost is that we cannot mutate the returned sublist.
int limit = Math.min(records.size(), position + n);
List<ConsumerRecord<K, V>> res = Collections.unmodifiableList(records.subList(position, limit));
position = limit;
if (position < records.size())
fetchOffset = records.get(position).offset();
return res;
}
주목할 코드 라인은 'Collections.unmodifiableList(records.subList(position, limit))'라는 부분이다. 기존의 List를 인자로 받아서 새로운 리스트를 리턴받는 메소드다.
코드에 쓰여 있는 주석을 읽어보면, 불필요한 리스트 복사 동작을 피하기 위함이라고 적혀있다. 'Collections.unmodifiableList()' 메소드를 열어보면 다음과 같다.
/**
* Returns an <a href="Collection.html#unmodview">unmodifiable view</a> of the
* specified list. Query operations on the returned list "read through" to the
* specified list, and attempts to modify the returned list, whether
* direct or via its iterator, result in an
* {@code UnsupportedOperationException}.<p>
*
* The returned list will be serializable if the specified list
* is serializable. Similarly, the returned list will implement
* {@link RandomAccess} if the specified list does.
*
* @param <T> the class of the objects in the list
* @param list the list for which an unmodifiable view is to be returned.
* @return an unmodifiable view of the specified list.
*/
public static <T> List<T> unmodifiableList(List<? extends T> list) {
return (list instanceof RandomAccess ?
new UnmodifiableRandomAccessList<>(list) :
new UnmodifiableList<>(list));
}
요약을 하자면, unmodifiableList() 메소드에서 리턴되는 리스트 레퍼런스는 'Read-Only' 용도로만 사용할 수 있으며, 수정하려는 메소드(가령 set(), add(), addAll() 등)를 호출하면 UnsupportedOperationException 이 발생한다는 것이다.
'Unmodifiable(수정 할 수 없다)'라는 특징은 마치 Immutable을 떠올리게 한다. unmodifiable과 immutable의 차이는 뭘까?
Unmodifiable
위 예제에서 본 것처럼 Collection.unmodifiableList() 같은 메소드에서 리턴되는 레퍼런스는 원본 컬렉션으로의 수정 메소드를 호출 할 수 없다. (만약 호출한다면 UnsupportedOperationException이 발생한다.)
하지만 원본 리스트 자체가 수정되지 않도록 보장해주지는 않는다. 즉, 'Collection.unmodifiableList()' 메소드로 리턴받은 레퍼런스 이외의 다른 레퍼런스로는 리스트를 수정할 수 있다.
다음 예제를 살펴보자
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class Test {
public static void main(String []args) {
List<String> list = new ArrayList<String>();
list.add("a");
list.add("b");
list.add("c");
List<String> unmodifiableList = Collections.unmodifiableList(list);
try {
unmodifiableList.add("d");
System.out.println("cannot be reached here");
} catch (UnsupportedOperationException e) {
System.out.println("Cannot modify unmodifiable list");
}
list.add("d");
System.out.println(unmodifiableList.get(3));
}
}
이 코드를 수행하면 다음과 같은 결과를 얻을 수 있다.
Cannot modify unmodifiable list
d
이처럼 unmodifiable 레퍼런스로 수정은 못하지만 원본 자체에 대한 수정을 막을 수는 없다.
Immutable
리스트가 Immutable 하다고 하면 어떤 레퍼런스를 이용해서라도 수정할 수 없어야 한다. 위에서 본 unmodifiableList는 원본 리스트로의 레퍼런스는 수정을 할 수 있기 때문에 Immutable을 만족하지 않는다.
기존에 존재하는 컬렉션을 Immutable로 만들기 위해서는 기존 컬렉션의 데이터를 새로운 컬렉션으로 복사한 다음, 새로운 컬렉션으로의 수정(modify) 접근을 제한하는게 일반적이다.
예를들어
List<String> immutableList = Collections.unmodifiableList(new ArrayList<String>(list));
이런 코드는 기존의 컬렉션인 list의 내용을 복사해서 unmodifiableList를 만들기 때문에 여기서 리턴되는 List는 immutable이다.
댓글