우아한 테크코스를 진행하면서 받았던 리뷰와 찾아서 공부했던 내용을 바탕으로 적었다.
공부하는 건 많아지는데 쉽게 까먹을까 봐 블로그에 쓰기로 다짐..
총 3가지의 리스트 복사에 대해서 다룰 것이다!
- Collections.unmodifiableList
- List.copyOf
- stream을 활용한 deepCopy
오늘은 첫번째로 Collections.unmodifiableList에 대해 다룰 것이다.
문제 상황
public List<Car> getCars() {
return Collections.unmodifiableList(cars);
}
처음 리뷰요청을 하였을 때 나의 코드는 위와 같았다.
사실 저렇게 쓴 이유는... 다들 저렇게 써서.. 였던 것 같다.
리뷰어님은 저 코드를 보고 이렇게 말씀하셨다.
내부의 cars를 그대로 담아 노출하네요. 외부에서 cars에 직접 접근해도 문제 없을까요?
처음에는 이게 무슨 소리지?라고 생각했다..ㅋㅋ
그래서 Collections.unmodifiableList를 뜯어보기로 하였다!! (인텔리제이를 통해)
1. Collections.unmodifiableList
Collections에서 static method로 제공되고 있는 기능이다. List타입을 인자로 받고 있다.
조건문을 읽어보니 인자로 넘긴 List의 클래스가 이미 UnmodifiableList.class이거나 UnmodifiableRandomAccessList.class 이면 그대로 다시 반환해 주는 모습을 볼 수 있다. 이미 UnmodifiableList이기 때문에 전달받은 List를 다시 감쌀 필요가 없기 때문인 것 같다!
그다음 인자로 넘긴 리스트가 RandomAccess으 instance이면 UnmodifiableRandomAccessList <>(list)를 반환하고 그게 아니라면 UnmodifiableList <>(list)를 반환한다.
여기서 봐야 될 것은 RandomAccess란 무엇일까라는 것이다.
Marker interface used by List implementations to indicate that they support fast (generally constant time) random access.
docs에 따르면 RandomAccess란 일반적으로 constant time내에 빠른 access를 지원한다고 나타내는 Marker interface이다.
우리가 자주 사용하는 리스트 중 ArrayList <>는 RandomAccess를 implements 하고 있다.
ArrayList <>의 내부 구조상 데이터를 배열에 보관하여 관리하기 때문에, 상수 시간 내에 데이터에 접근할 수 있기 때문인 것 같다.
반대로, LinkedList <>는 노드를 연결하여 데이터를 보관하기 때문에, RandomAccess가 불가능한 Sequential Access이다.
ArrayList <>는 index를 통해 Constant time 즉, O(1)으로 바로 접근할 수 있지만, LinkedList <>는 처음 노드부터 차례대로 타며 접근해야 해서 시간이 O(N)만큼 걸린다.
딴 길로 많이 간 것 같지만, 정리하자면 Collections.unmodifiableList를 통해 전달해 준 리스트 인자가 ArrayList <>와 같은 RandomAccess가 가능한 리스트라면 밑에와 같은 로직을 타고,
LinkedList <>와 같은 RandomAccess가 가능하지 않은 리스트라면 밑과 같은 로직을 탄다.
이제 대망의 UnmodifiableList를 탐험할 시간이 되었다!
중요한 것만 보자!
UnmodifiableList를 써본 사람이라면 알고 있겠지만, set, add, remove와 같은 리스트에 원소를 변경, 추가, 삭제와 같은 메서드가 막혀있다. 위와 같은 메서드를 호출하면 UnsupportedOperationException을 던지도록 한 것이다!
제일 중요한 것은 생성자를 통해서 전달받은 list를 그대로 멤버 변수로 갖고 있다는 것이다...!
즉, Collections.unmodifiableList를 통해 전달 받은 리스트는, 내부적으로 원본 리스트와 같은 리스트인 것이다.
반환받은 리스트는 변경, 추가, 삭제와 같은 기능들이 막혀있어서 안전한 것 아닌가?라는 생각이 들지만 여기에는 두 가지 문제점이 있다.
Collections.unmodifiableList의 문제점
1. 원본 리스트에서 변경, 추가, 삭제와 같은 기능을 사용하면 Collections.unmodifiableList를 통해 반환 받은 리스트가 변경될 수 있다.
다음과 같은 코드가 있다.
List<Car> cars = new ArrayList<>();
List<Car> unmodifiableCars = Collections.unmodifiableList(cars);
// 원본에 원소 추가
cars.add(new Car("firstCar"));
System.out.println(unmodifiableCars.size());
빈 cars로 Collections.unmodifiableList를 만든 후 원본에 firstCar라는 이름을 가진 Car 객체를 삽입하였다.
그렇다면 unmodifiableCars의 사이즈는 몇일까?
원본에 삽입하여 unmodifiableCars는 빈 리스트라고 생각할 수 있겠지만 놀랍게도 사이즈는 1이라고 출력된다.
그 이유는, cars와 unmodifiableCars는 리스트를 공유하고 있기 때문이다.
unmodifiableCars를 통해 수정, 삽입, 삭제를 못할 뿐 원본을 통한 수정은 가능하다!
원본이 수정될 일이 있다면 Collections.unmodifiableList를 통해 반환받은 리스트도 수정될 수 있음을 명심해야 한다.
2. 리스트 안의 객체에 대한 복사가 이루어지지 않았기 때문에 원본과 같은 객체를 참조하여 Collections.unmodifiableList를 통해 받은 리스트에서 객체에 접근하여 수정하면, 원본의 수정이 이루어질 수 있다.
다음과 같은 코드가 있다.
List<Car> cars = new ArrayList<>();
List<Car> unmodifiableCars = Collections.unmodifiableList(cars);
cars.add(new Car("firstCar"));
// 객체를 수정
unmodifiableCars.get(0).setName("updatedFirstCar");
// 원본의 객체의 정보를 출력
System.out.println(cars.get(0).getName());
원본을 통해 unmodifiableCars를 만들어 주었고, 그 후 원본에 firstCar라는 이름을 가진 객체를 삽입해 주었다.
문제점 1에서 말했던 것처럼 unmodifiableCars에도 하나의 객체가 들어가있다.
그리고 unmodifiableCars를 통해 객체를 얻어 name field를 수정하게 되면, 원본 객체도 수정이 되게 된다.
즉, 출력은 firstCar가 아닌 updatedFirstCar이다.
여기서 얻을 수 있는 교훈은 만약 리스트의 객체가 수정이 될 수 있는 가변 객체라면, 객체들의 복사도 고려해야 한다는 것이다.
회고
Collections.unmodifiableList는 리스트의 복사라고 보기 힘들 것 같다.
내부적으로 원본 리스트를 가지고 있기 때문이다. 그렇다면 어떤 상황에서 Collections.unmodifiableList를 써야하는 것일까?
1. 원본 리스트가 수정될 일이 없다.
사실 원본이 수정되는 일은 흔치 않을 것 같다. 우리가 걱정해야 하는 상황은 복사본을 통해 원본이 수정되는 일이 대다수이다.
그렇지만, 특정 상황에서 넘겨준 원본이 수정되면 Collections.unmodifiableList도 수정이 되므로, 이 상황을 인지하고 있어야 한다.
2. 리스트안의 객체가 불변 객체이다.
Collections.unmodifiableList를 통해 반환된 리스트는 수정, 삭제, 삽입을 할 수 없다.
하지만 get을 통하여 객체를 얻어온 뒤 객체의 수정은 가능하다.
하지만 객체가 수정이 불가능한 불변 객체라면, 이러한 걱정은 하지 않아도 될 것이다.
다음은 List.copyOf에 대해서 다뤄볼 것이다.
잘못된 내용이 전파되는 것을 경계하고 있기 때문에 최대한 Oracle docs와 내부 구현 코드를 보고 작성한 글입니다. 혹시라도 틀린 내용이 있다면 댓글 부탁드립니다. 감사합니다:)
출처
1. https://docs.oracle.com/javase/7/docs/api/java/util/RandomAccess.html
'프로그래밍 언어 > Java' 카테고리의 다른 글
Java - List의 복사 (3): Stream API을 활용한 DeepCopy (0) | 2023.03.26 |
---|---|
Java - List의 복사 (2): List.copyOf (0) | 2023.03.01 |
Java - Iterator (0) | 2022.11.29 |
Java - Collections Class (0) | 2022.11.28 |
Java - Map과 HashMap (0) | 2022.11.24 |