무지를 아는 것이 곧 앎의 시작

Java

Collections.unmodifiableXX()는 방어적 복사가 아니다

Alex96 2022. 3. 6. 02:30

바로 전에 작성했던 깊은 복사와 방어적 복사에 대한 포스팅에 이어서...

제가 복사에 대한 내용을 다루는 이유는 사실 제가 딱 하루 전까지만 해도 Collections.unmodifiableXX()이 방어적 복사라고 잘못 알고 있었기 때문이죠...

깊은 복사와 방어적 복사라는 개념을 잘 모르시겠다면 아래 포스팅을 보고 오시면 됩니다!

2022.03.05 - [개발 이야기/Java] - Collection의 깊은 복사와 방어적 복사

 

Collection의 깊은 복사와 방어적 복사

우아한테크코스 두번 째 미션을 진행하던 중... 제 미션의 리뷰를 맡아주신 리뷰어분이 깊은 복사와 방어적 복사에 대해 고민하고 적용시켜보라는 말을 해주셔서 포스팅을 해보려 합니다😄 깊

alexander96.tistory.com

 

Collections.unmodifiableXX()

 

자 이번 포스팅의 주제 Collections.unmodifiableXX() 이라는 Collections클래스의 팩토리 메서드인데요, 각 컬렉션 종류마다 있어요.

ex) Collections.unmodifiableList(), Collections.unmodifiableMap(), Collections.unmodifiableSet() ...

 

이 친구들은 컬렉션에 요소를 삽입하거나 삭제하거나 바꿔끼울 수 없는 컬렉션을 반환하는데요

주로 getter로 내부의 리스트를 반환할 때 그 리스트를 변경하지 못하게 할 목적으로 저 메소드로 한번 감싸서 리턴하는 용도로 사용합니다.

 

'메서드로 감싸서 새로운 변화가 불가능한 새 컬렉션 객체를 리턴한다.'라고 생각해서 저는 이게 방어적 복사를 같이 해준다고 생각하고 사용했었죠.

그런데, 우아한테크코스 미션을 진행하다 리뷰어분께서 방어적 복사에 대해 공부해보라고 그러시더라구요.

그걸 보고 저는 '잉..? 나 방어적 복사 하고 있는데..?' 하고 생각했는데... 하면서 혹시나 내가 잘못 알고 있는게 있나..? 있다면 가장 먼저 의심되는 부분이 저 Collections.unmodifiableXX()을 사용하는 곳이었죠.

 

코드를 까보면서 저 친구의 동작을 살펴볼까요?

Collections.unmodifiableList()

Collections.unmodifiableList()를 들어가봤더니 'UnmodifiableList'라는 클래스를 반환하는군요.

바로 아래에 있는 클래스네요. 생성자를 보면 매개변수로 받은 리스트를 얕은 복사해서 보관하는걸 볼 수 있어요.

 

즉 원본 컬렉션을 완전히 참조하고 있기 때문에 요소를 수정하는 메소드 호출만 막았을 뿐 원본과의 관계가 있다는거죠...

 

그럼 이렇게 생각할 수 있어요. '아 원본 참조하는게 뭐..? 어차피 변경이 막혔다며?'

변경이 막힌건 반환받은 UnmodifiableList이고 원본은 리스트는 변경이 가능합니다.

 

코드로 살펴볼까요?

@Test
@DisplayName("unmodifiableList 테스트")
void collectionUnmodifiableList() {
    List<String> strs = new ArrayList<>();
    strs.add("a");
    strs.add("b");
    strs.add("c");
    List<String> copy = Collections.unmodifiableList(strs);
    strs.add("d");
    assertThat(strs).isEqualTo(copy);
}

학습 테스트를 작성해봤습니다. Collections.unmodifiableList()UnmodifiableList를 받은 후에 원본에 요소를 추가했어요.

만약 방어적 복사를 했다면 원본과 copy가 동등성을 만족할 수 없었을 텐데, 저 테스트는 isEqualTo를 통과합니다.

바로 원본에 요소가 추가되면서 UnmodifiableList은 그 원본을 참조하니 같이 요소가 추가되기 때문이죠.

UnmodifiableList는 

 

아... 그럼 나는 방어적 복사도 하면서 수정을 불가능하게 하려면 방어적 복사 한 후에 Collections.unmodifiableXX()까지 써줘야하는거야?

 

하실 수 있는데, 이 기능을 하는 정적 팩토리 메서드가 있습니다.

 

XX.copyOf()

 

위에서 말한 '방어적 복사 + unmodifable'을 동시에 해주는 메서드들이에요 마찬가지로 컬렉션별로 있어요

unmodifable이라면 요소를 추가하거나 삭제하거나 변경하는 메서드를 호출하면 'UnsupportedOperationException'을 던져야겠죠?

 

코드로 알아봅시다.

@Test
@DisplayName("copyOf 테스트")
void collectionCopyOf() {
    List<String> strs = new ArrayList<>();
    strs.add("a");
    strs.add("b");
    strs.add("c");
    List<String> copy = List.copyOf(strs);
    strs.add("d");
    assertThat(strs).isNotEqualTo(copy);
    assertThatThrownBy(() -> copy.add("e"))
            .isInstanceOf(UnsupportedOperationException.class);
}

자 위에 Collections.unmodifiableList()을 쓸때와 똑같이 원본에 사용해서 새 List를 얻은 후에 원본을 수정했습니다.

이번엔 원본과 동등하지 않음을 검사하는 isNotEqualTo를 통과합니다. 바로 방어적 복사가 이루어졌기 때문에 원본을 수정해도 새 List에 요소 추가가 없던 것이죠.

 

그리고 바로 다음 부분은 복사된 List에 요소를 추가하려 하면..? 

UnsupportedOperationException이 발생하네요.🤓

 

결론.

Collections.unmodifiableXX() 메서드는 방어적 복사를 하지 않는다.

방어적 복사를 원하면서 동시에 변경을 막고싶다면 XX.copyOf()를 사용하라!