방어적 복사, unmodifiable, 불변...
너무 어려워서 정리해봅니다!
생성자의 파라미터로 리스트를 받는다면❓
루피로부터 생성자 내부 검증 및 할당 과정에 대해 피드백을 받았습니다!
실은 피드백을 받았던 당시에는 루피의 이야기가 어떤 의미인지 이해하지 못했어요.
리스트를 선언하고, 그걸 생성자에 전달한 이후에,
생성자에 전달했던 스코프에서 리스트를 변경시킬 경우,
unmodifiableList로 할당한 리스트에도 변경이 적용됩니다.
이 내용을 알지 못했어요.
사실 이것만 모르던 게 아니라,
그래서 여러가지 리스트의 복사에 대해 한 번 정리해보기로 했습니다.!
1. = anotherList;
완전히 동일한 컬렉션에 대해 참조변수를 하나 더 만드는 작업입니다.
List<Person> original = new ArrayList<>();
original.add(new Person("rich", 20));
original.add(new Person("pobi", 21));
original.add(new Person("okay", 22));
List<Person> copiedWithItself = original;
항목 | 복사 결과 |
컬렉션 주소 | 동일함 |
요소 주소 | 동일함 |
원본 컬렉션에 요소 추가 삭제 | 컬렉션 주소가 같기 때문에 영향 받음 |
요소 내 값 변경 | 요소 주소가 같기 때문에 영향 받음 |
완전히 동일한 컬렉션으로서,
컬렉션 레벨의 수정, 요소 레벨의 수정 모두 공유됩니다.
2. new ArrayList<>(anotherList);
컬렉션의 주소값은 달라지지만, 요소들의 주소값은 동일합니다.
List<Person> original = new ArrayList<>();
original.add(new Person("rich", 20));
original.add(new Person("pobi", 21));
original.add(new Person("okay", 22));
List<Person> copiedWithConstructor = new ArrayList<>(original);
항목 | 복사 결과 |
컬렉션 주소 | 달라짐 |
요소 주소 | 동일함 |
원본 컬렉션에 요소 추가 삭제 | 컬렉션 주소가 달라졌기 때문에 영향 받지 않음 |
요소 내 값 변경 | 요소 주소가 같기 때문에 영향 받음 |
컬렉션의 주소값은 달라지지만, 요소들의 주소값은 동일합니다.
따라서, 컬렉션 레벨의 요소 추가 / 삭제 는 공유되지 않지만,
요소 레벨의 요소 값 수정 등은 공유됩니다.
3. copied.addAll(anotherList);
컬렉션의 주소값은 달라지지만, 요소들의 주소값은 동일합니다.
List<Person> original = new ArrayList<>();
original.add(new Person("rich", 20));
original.add(new Person("pobi", 21));
original.add(new Person("okay", 22));
List<Person> copiedWithAddAll = new ArrayList<>();
copiedWithAddAll.addAll(original);
항목 | 복사 결과 |
컬렉션 주소 | 달라짐 |
요소 주소 | 동일함 |
원본 컬렉션에 요소 추가 삭제 | 컬렉션 주소가 달라졌기 때문에 영향 받지 않음 |
요소 내 값 변경 | 요소 주소가 같기 때문에 영향 받음 |
new ArrayList<>(anotherList)와 동일합니다.
컬렉션의 주소값은 달라지지만, 요소들의 주소값은 동일합니다.
따라서, 컬렉션 레벨의 요소 추가 / 삭제 는 공유되지 않지만,
요소 레벨의 요소 값 수정 등은 공유됩니다.
4. anotherList.stream().collect(toList());
컬렉션의 주소값은 달라지지만, 요소들의 주소값은 동일합니다.
List<Person> original = new ArrayList<>();
original.add(new Person("rich", 20));
original.add(new Person("pobi", 21));
original.add(new Person("okay", 22));
List<Person> copiedWithStream = original.stream()
.collect(Collectors.toList());
항목 | 복사 결과 |
컬렉션 주소 | 달라짐 |
요소 주소 | 동일함 |
원본 컬렉션에 요소 추가 삭제 | 컬렉션 주소가 달라졌기 때문에 영향 받지 않음 |
요소 내 값 변경 | 요소 주소가 같기 때문에 영향 받음 |
new ArrayList<>(anotherList), new ArrayList<>() + addAll() 과 동일합니다.
컬렉션의 주소값은 달라지지만, 요소들의 주소값은 동일합니다.
따라서, 컬렉션 레벨의 요소 추가 / 삭제 는 공유되지 않지만,
요소 레벨의 요소 값 수정 등은 공유됩니다.
아마 여기까지는 모두 익숙한 내용이실 겁니다.
문제는 다음부터입니다 !
5. Collections.unmodifiableList(anotherList);
@return an unmodifiable view of the specified list.
List<Person> original = new ArrayList<>();
original.add(new Person("rich", 20));
original.add(new Person("pobi", 21));
original.add(new Person("okay", 22));
List<Person> collect = Collections.unmodifiableList(original);
항목 | 복사 결과 |
컬렉션 주소 | 달라짐 |
요소 주소 | 동일함 |
복사된 컬렉션에 요소 추가 삭제 | UOE 발생 |
원본 컬렉션에 요소 추가 삭제 | 컬렉션 주소가 다름에도 불구하고 복사된 컬렉션에도 똑같이 요소 추가 삭제가 일어남 |
요소 내 값 변경 | 요소 자체에서 변경시 원본 컬렉션 복사본 컬렉션 모두 영향, 원본 컬렉션, 복사본 컬렉션에서 요소를 꺼내서 값 변경해도 동일하게 양쪽 모두에 영향을 미침 |
2,3,4번에서 살펴봤던 것처럼 컬렉션 주소는 달라지고, 요소 주소는 동일합니다.
그치만 이 녀석은 좀 특이합니다. unmodifiableList 라는 메서드 이름에 속으면 안됩니다 ㅠ.ㅠ
절대로 불변 컬렉션 이라는 이름을 줄 수 없는 녀석이거든요.
우선 컬렉션 레벨의 수정을 살펴보면
unmodifiableList 로 선언한 참조변수, 즉 복사본 컬렉션에다가
요소를 추가거나 삭제하려고 할 경우, UnsupportedOperationException이 발생하며,
복사본 컬렉션을 통한 컬렉션 레벨의 수정이 막혀있습니다.
그러나 원본 컬렉션에 요소를 추가 / 삭제할 경우, 복사본 컬렉션에서도 그대로 적용이 됩니다!!
이는 주석을 살펴보면 이해가 쉬워집니다. 일부만 발췌 -> 번역해봤습니다.
unmodifiableList 메서드는 매개변수로 전달된 리스트에 대한 unmodifiable view 를 반환합니다.
반환된 리스트 내부는 최초 매개변수로 전달됐던 리스트를 통해서 보여줍니다. (read through)
반환된 리스트에 대한 수정 시도 시, UnsupportedOperationException이 발생합니다.
번역이 신통치는 않은데 ㅋㅋ 암튼;; 그렇습니다..
중요한 점은, read through 입니다.
마치 특정 파일에 대해 읽기 전용 권한으로 공유를 받았다고 생각하면 됩니다.
수정 권한이 있는 사람이 수정할 경우, 그 수정된 내용을 보게 되는 거죠.
그러나 내가 수정하려 할 땐 권한이 없어서 실패합니다 -> UOE 발생.
그래서 unmodifiable view를 반환한다 라고 표현한 것입니다.
결론적으로 unmodifiableList(anotherList) 로 반환되는 리스트의 특징은 다음과 같습니다.
1. 원본 컬렉션과 주소값이 다르며 컬렉션 내부 요소들의 주소값은 동일하다.
2. 원본 컬렉션에서 요소 추가, 삭제가 일어날 경우, 복사본 컬렉션에서도 적용된다.
3. 복사본 컬렉션에서 요소 추가, 삭제를 시도할 경우 UOE 이 발생되며 실패한다.
4. 원본 요소, 원본 컬렉션에서 꺼낸 요소, 복사본 컬렉션에서 꺼낸 요소에서 내부 값을 바꿀 경우,
셋 다 영향을 받는다. (주소값이 같으니깐!)
6. List.copyOf(anotherList);
Returns an unmodifiable List
containing the elements of the given Collection, in its iteration order
List<Person> original = new ArrayList<>();
original.add(new Person("rich", 20));
original.add(new Person("pobi", 21));
original.add(new Person("okay", 22));
List<Person> people = List.copyOf(original);
항목 | 복사 결과 |
컬렉션 주소 | 달라짐 |
요소 주소 | 동일함 |
복사된 컬렉션에 요소 추가 삭제 | UOE 발생 |
원본 컬렉션에 요소 추가 삭제 | 원본 컬렉션에서 요소 추가/삭제를 해도, 복사본 컬렉션에 영향을 미치지 않음. 물론 복사본 컬렉션에선 요소 추가/삭제가 불가능 (UOE) |
요소 내 값 변경 | 요소 자체에서 변경시 원본 컬렉션 복사본 컬렉션 모두 영향, 원본 컬렉션, 복사본 컬렉션에서 요소를 꺼내서 값 변경해도 동일하게 양쪽 모두에 영향을 미침 |
짜잔! 드디어 copyOf입니다.
방어적 복사! 를 하려면 copyOf를 해야 합니다.
unmodifiable view가 아닌, unmodifiable list를 반환한다는 설명에서 이미 눈치채셨죠?!
컬렉션의 주소값은 달라지고, 요소 주소값은 동일하다는 점은 공통적입니다.
복사본 컬렉션에 대한 요소 추가 / 삭제 시도 시 UOE가 발생한다는 점은 unmodifiableList와 동일합니다.
그러나 원본 컬렉션에서 요소 추가 / 삭제 가 일어나도, 영향을 받지 않습니다! 주석을 볼까요?
copyOf(anotherList)를 수행한 이후에, anotherList에 대한 요소 추가, 삭제가 일어나더라도
copyOf(anotherList)가 반환한 리스트는 그 수정을 반영하지 않습니다.
(이해를 돕기 위해 메소드 이름과 변수명을 넣는 등 일부 각색을 하였습니다.)
컬렉션 레벨의 수정을 방지하면서도,
원본 컬렉션에 대한 컬렉션 레벨의 수정이 일어나도 영향을 받지 않습니다!👏👏👏
이쯤 되면 방어적 복사 라는 이름을 줘도 괜찮겠죠?!
7. 이래도 불변이 아니라구??
불변의 길은 멀고 험하군요..
그럼에도 불구하고 copyOf()로 만들어낸 컬렉션이 완전한 불변이라고 볼 수는 없습니다.
왜냐하면 copyOf의 반환값으로 전달된 unmodifiable list에서 객체를 꺼내서,
그 객체 내부의 값을 변경할 경우, 아니면 외부에서 그 객체의 값을 변경할 경우에는
요소 까지 깊은 복사를 하지는 않았기 때문에 값이 변경되기 때문입니다.
API 레벨에서는 깊은 복사를 지원하지 않는다고 합니다.
따라서 요소 레벨까지 깊은 복사가 필요시 forEach 문 등을 사용해서 직접 생성을 해야 할 것입니다.
그리고 이 지점 때문에 바로 불변 객체, VO를 제대로 설계하는 것이 더 중요하겠죠?!
컬렉션의 요소가 되는 값 객체들이 불변을 보장한다면, copyOf와 만나서 완전 불변이 가능해질테니까요!
움.. 그래서 맨 처음으로 돌아와서, 루피의 피드백을 적용한다면,
생성자 내부에서 파라미터로 전달된 컬렉션을 copyOf로 재생성하여,
원본 컬렉션과의 연결을 끊어내는 동시에 컬렉션 레벨의 수정을 막는다면
더욱 안전한 생성자 설계가 가능할 것 같습니다! 적용하러 가야겠습니다!
'우아한테크코스 4기' 카테고리의 다른 글
💻 코딩을 지탱하는 기술을 읽었습니당 ! (4) | 2022.03.08 |
---|---|
🏎️자동차 경주와 💸로또에서 배운 것들 (2) | 2022.03.08 |
for-loop vs IntStream, 그리고 멀티 스레딩 (4) | 2022.02.28 |
EnumMap, 니가 그렇게 빨라?? (4) | 2022.02.26 |
첫 오프라인 페어 프로그래밍! (6) | 2022.02.24 |