우아한테크코스 사다리 미션을 진행하면서 Collection 객체를 전달할 때 방어적 복사를 이용해보는 것을 리뷰어로부터 피드백을 받게되었습니다.
방어적 복사라는 것은 무엇일까요?
방어적 복사란 생성자로 초기화 할 때 혹은 Collection을 다른 객체로 전달할 때 새로운 객체로 감싸서 전달해주는 방법으로 원본과 복사본의 주소 값을 끊기 위해 사용하는 것
입니다. 방어적 복사를 사용하는 이유는 복사본에 의해서 원본이 훼손되는 것을 막기 위함입니다.
방어적 복사의 종류로는 크게 List.copyOf(), new ArrayList<>()를 이용하는 방법이 있습니다. 다음과 같은 객체들이 있을 때 방어적 복사를 하지 않은 경우와 방어적 복사를 한 경우를 나누어서 어떤 결과를 가져다 주는지 살펴보겠습니다.
그전에 앞서서 간단하게 이용할 객체에 대해 정리하겠습니다.
public class Crew {
private final String name;
public Crew(String name) {
this.name = name;
}
}
public class BackEndCrew {
private final List<Crew> crews;
public BackEndCrew(List<Crew> crews) {
this.crews = crews;
}
public List<Crew> getCrews() {
return crews;
}
}
public class DefensiveCopy {
public static void main(String[] args) {
List<Crew> crews = new ArrayList<>();
crews.add(new Crew("아코"));
crews.add(new Crew("비버"));
crews.add(new Crew("스플릿"));
BackEndCrew backEndCrew = new BackEndCrew(crews);
List<Crew> woowahanCrew = backEndCrew.getCrews(); //return crews
}
}
위의 코드와 같이 backEndCrew의 필드(crews)를 그대로 보내게 된다면 다음과 같은 결과를 확인할 수 있습니다.
crews와 woowahanCrew가 모두 같은 주소값을 참조하는 것과 각각의 요소 모두 같은 주소값을 확인할 수 있습니다. 이러면 어떤 문제가 생기게 될까요? 전달된 List 즉, woowahanCrew가 수정이 되면 같은 주소값을 참조하는 crews의 상태 역시 변화하게 되어 치명적인 문제(외부의 수정에 의해 내부의 값이 변경되는 문제
)를 유발할 수 있습니다.
public class DefensiveCopy {
public static void main(String[] args) {
List<Crew> crews = new ArrayList<>();
crews.add(new Crew("아코"));
crews.add(new Crew("비버"));
crews.add(new Crew("스플릿"));
BackEndCrew backEndCrew = new BackEndCrew(crews);
List<Crew> woowahanCrew = backEndCrew.getCrews(); //return crews
woowahanCrew.add(new Crew("우가"));
System.out.println(crews.size()); // 4
}
}
앞선 코드에서 woowahanCrew에 새로운 Crew를 추가하고 원본자료인 crews의 size()를 출력하면 4를 출력하는 것을 확인할 수 있습니다.
이와 같은 문제를 막기 위해 방어적 복사를 사용하는 것입니다.
static <E> List<E> copyOf(Collection<? extends E> coll) {
return ImmutableCollections.listCopy(coll);
}
List.copyOf()은 ImmutableCollections를 반환해주는 static 메소드 입니다. 메소드 설명은 다음과 같습니다.
Returns an unmodifiable List containing the elements of the given Collection, in its iteration order. The given Collection must not be null, and it must not contain any null elements. If the given Collection is subsequently modified, the returned List will not reflect such modifications.
간단하게 핵심을 요약을 하면 List.copyOf()는 수정불가능한 리스트를 반환하고 원본이 수정되더라도 복사본은 이를 반영하지 않는다는 것입니다. 이를 코드를 통해 보도록 하겠습니다.
public class BackEndCrew {
...
public List<Crew> getCrews() {
return List.copyOf(crews);
}
}
BackEndCrew의 getter를 위와 같이 수정하면 다음과 같은 결과를 확인할 수 있습니다.
crews가 참조하는 주소값과 woowahanCrew가 참조하는 주소값이 달라진 것을 확인할 수 있습니다. 그러면 crews에 새로운 멤버가 추가되었을 때에 woowahanCrew가 변하지 않는 것을 확인할 수 있습니다. 또한 woowahanCrew의 경우 unmodifiable List이므로 추가 삭제 수정이 불가능합니다.
public class DefensiveCopy {
public static void main(String[] args) {
List<Crew> crews = new ArrayList<>();
crews.add(new Crew("아코"));
crews.add(new Crew("비버"));
crews.add(new Crew("스플릿"));
BackEndCrew backEndCrew = new BackEndCrew(crews);
List<Crew> woowahanCrew = backEndCrew.getCrews();
crews.add(new Crew("우가"));
System.out.println(woowahanCrew.size()); // 3
}
}
그렇다면 List.copyOf는 완전히 불변이라고 할 수 있을까요? 아닙니다 위의 결과 사진을 자세히 보면 각 요소(Crew)들이 참조하는 주소는 모두 같은 것을 알 수 있습니다. 그렇기에 깊은 복사
라고 할 수 없습니다.
깊은 복사란?
새로운 복합 객체를 만들고, 재귀적으로 원본 객체의 사본을 새로 만든 복합 객체에 삽입을 하는 복사로 쉽게 설명하면 모든 요소까지 참조하는 주소가 원본과 모두 다른 복사 기법입니다.
현재에는 Crew의 String 필드가 final로 되어 있어 내부의 값을 변경할 수 없지만 내부의 필드값이 final이 아니라면 요소들의 불변을 보장할 수는 없습니다.
new ArrayList<>()는 새로운 wrapping을 통해 새로운 참조 값을 가지게 하는 방법입니다.
다음 코드로 확인해보겠습니다.
public class BackEndCrew {
...
public List<Crew> getCrews() {
return new ArrayList<>(crews);
}
}
BackEndCrew의 getter를 다음과 같이 수정하면 다음과 같은 결과를 확인할 수 있습니다.
앞선 List.copyOf()와 같은 결과를 가져왔습니다. crews와 woowahanCrew가 서로 다른 참조 값을 가지고 있지만 각 원소들은 같은 참조 값을 가지고 있습니다. 또한 new ArrayList<>()역시 깊은 복사는 아닙니다. 그렇다면 List.copyOf()와 new ArrayList<>()의 차이는 무엇일까요?
두 방법에 차이는 데이터를 전달받은 쪽에서 수정이 여부입니다. 앞서 말한 것과 같이 List.copyOf는 umodifiable List를 전달해주어 원소를 add, remove, set 등의 기능들이 불가하지만 new ArrayList<>()의 경우 add, remove, set이 허용됩니다.
이 두 방법 외에도 방어적 복사는 아니지만 자주 듣는 방법인 Collection.unmodifiableList()에 대해 알아보도록 하겠습니다.
Collection.unmodifiableList()는 원본 데이터와 같은 주소값을 참조하지만 복사본의 경우 unmodifiable이라 add, remove, set 등은 지원하지 않아 수정은 이루어 질 수 없습니다. 그렇다고 불변을 의미하는 것은 아닙니다.
Collection.unmodifiableList() 역시 원본과 복사본의 원소가 모두 같은 주소를 참조할 수 있어 복사본에서의 원소 수정이 되기 때문입니다. 또한 원본에서 수정이 이루어지면 같은 참조 값을 가지고 있기에 복사본 역시 수정이 됩니다.
public class BackEndCrew {
...
public List<Crew> getCrews() {
return Collections.unmodifiableList(crews);
}
}
public class DefensiveCopy {
public static void main(String[] args) {
List<Crew> crews = new ArrayList<>();
crews.add(new Crew("아코"));
crews.add(new Crew("비버"));
crews.add(new Crew("스플릿"));
BackEndCrew backEndCrew = new BackEndCrew(crews);
List<Crew> woowahanCrew = backEndCrew.getCrews();
//원본이 수정이 되면다면
crews.add(new Crew("우가"));
System.out.println(woowahanCrew.size()); // 4
}
}
위의 코드와 같이 원본 리스트 crews를 추가하면 복사본인 woowahanCrew의 사이즈가 같이 변화하는 것을 확인할 수 있습니다.
List.copyOf | new ArrayList<>() | Collection.unmodifiableList() | |
---|---|---|---|
방어적 복사인가? | O | O | X |
데이터를 수정가능한가?(add, set, remove 등등..) | X | O | X |
깊은 복사인가? | X | X | X |
원본 데이터의 수정에 영향을 받는가? | X | X | O |
위의 세가지 방법은 모두 외부에 변화에 대해서 원본 데이터를 보존하기 위한 방법인데 이를 보장하기 위해서는 Collection의 원소들이 모두 불변 객체로 만들어져야 외부에서 데이터 수정이 일어났을 때 원본 데이터가 수정되는 일을 막을 수 있습니다.
위의 표를 참고하셔서 원하는 상황에 맞게 알맞은 방법을 이용하면 좋을 것 같습니다:)