얕은 복사, 방어적 복사, 깊은 복사, 불변 컬렉션 반환 총 정리! #JAVA
컬렉션을 반환할 때, 원본의 참조 값을 그대로 반환하는 경우
원하지 않는 곳에서 컬렉션을 직접 변경할 수 있습니다.
값이 추가되거나 수정되거나 삭제되어 문제가 발생할 수 있습니다.
따라서, 수정이 필요하지 않은 경우라면 컬렉션의 원본을 그대로 반환하기보다
복사해서 보내는 것이 좋은 방법입니다.
본 아티클에서는
첫째로, 복사의 여러 종류에 대해 알아보겠습니다.
둘째로, 복사의 종류와 불변 여부를 고려한, 다양한 복사의 사용법에 대해서 알아보겠습니다.
** 편의상 컬렉션의 여러 종류 중 리스트를 중점적으로 다뤄보겠습니다.
복사의 종류
그에 앞서, 예시로 사용할 Person
클래스를 다음과 같이 정의하겠습니다.
class Person {
private final String name;
private final int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}
복수의 Person
을 담고 있는 persons
는 다음과 같습니다.
List<Person> persons = new ArrayList<>();
persons.add(new Person("우진", 20);
persons.add(new Person("체인저", 30);
얕은 복사
얕은 복사는 원본의 참조가 유지되는 복사입니다.
List<Person> newPersons = persons;
newPersons
는 새로운 객체이지만, persons
와 같은 주소를 참조하는 참조형 객체가 됩니다.
Stack
메모리 상에서 persons
와 newPersons
는 별도의 객체이지만, 같은 주소값을 가지고 있습니다.
그리고 Heap
메모리 영역에 있는 하나의 객체를 가리키고 있습니다.
위의 결과처럼, 둘은 동일
한 객체입니다.
방어적 복사
방어적 복사는 원본의 참조가 유지되지 않는 복사입니다.
List<Person> newPersons = new ArrayList<>(persons);
ArrayList
의 생성자는, Collection
타입을 인자로 받으면 해당 컬렉션의 요소들을 가지고 있는 새로운 ArrayList
를 생성합니다. (LinkedList
와 같은 다른 구현체도 마찬가지입니다.)
얕은 복사와 얼핏 보면 비슷하지만, 다른 점이 있습니다.
Stack 영역에서 | Stack 영역에서 가지고 있는 주소 | Heap 영역 | |
얕은 복사 | 분리된 객체 | 동일함 | 하나의 객체 |
방어적 복사 | 분리된 객체 | 서로 다름 | 서로 다른 객체 |
위의 결과처럼, 둘은 동일
한 객체가 아닙니다.
하지만, 방어적 복사도 완벽한 복사 방식은 아닙니다.
내부에 가지고 있는 요소가 참조형이라면, 같은 객체를 참조하고 있을 수 있습니다.
위의 결과처럼, persons
와 newPersons
의 0번째 요소는 같은 객체를 참조합니다.
동일한 객체라고 볼 수 있습니다.
깊은 복사
깊은 복사는 값 자체를 복사하는 것으로, 방어적 복사의 맹점을 해결합니다.
컬렉션 자체도 새로운 객체이면서, 내부에 있는 객체들도 새로운 객체가 됩니다.
원리는 간단합니다.
새로운 틀(컬렉션)을 만들고, 그 안에 다시 새로운 요소들을 생성해서 집어넣는 것입니다.
public Person copy() {
return new Person(this.name, this.age);
}
우선, 위와 같이 Person
클래스 내부에 copy()
라는 메서드를 구현합니다.
내부 상태가 동일한 새로운 Person
인스턴스를 반환합니다.
List<Person> persons = new ArrayList<>();
persons.add(new Person("우진", 20));
persons.add(new Person("체인저", 30));
List<Person> newPersons = new ArrayList<>();
for (Person person : persons) {
newPersons.add(person.copy());
}
그리고 위와 같이 새로운 틀을 만든 후,
원본 컬렉션을 순회하며 새로운 객체를 생성 및 삽입합니다.
위의 결과를 통해,
깊은 복사가 컬렉션, 요소를 새로 만들어 참조 연결을 끊는다는 것을 알 수 있습니다.
하지만 새로 만든 요소들의 동등성은 유지된다는 것도 말이죠.
(여기서 동등성을 판단하는 메서드는 아래 코드와 같이 새롭게 작성했습니다.)
public boolean equals(Person person) {
return this.name.equals(person.name) && this.age == person.age;
}
컬렉션 복사의 사용
참조 할당 - 얕은 복사
List<Person> newPersons = persons;
- 원본과 같은 주소를 참조
- 복사본을 수정하면 원본이 같이 수정
원본을 생성자의 인자로 삽입 - 방어적 복사
List<Person> newPersons = new ArrayList<>(persons);
- 원본과 다른 주소를 참조
- 복사본을 수정해도 원본에 영향이 없음
- 다만, 요소가 참조형이라면 각 요소들은 참조 할당됨
List.copyOf() - 방어적 복사 + 불변
List<Person> newPersons = List.copyOf(persons);
- 원본과 다른 주소를 참조
- 복사본은 수정 자체가 불가능
add
,remove
,set
→UnsupportedOperationException
발생ImmutableCollections
를 생성하여 반환하기 때문List.of()
메서드를 통해 초기화한 리스트도 같은 이유로 수정이 불가능
Collections.unmodifiableList() - 참조 연결 + 불변
List<Person> newPersons = Collections.unmodifiableList(persons);
- 원본과 다른 주소를 참조
- 원본의 변경이 복사본에 그대로 반영
- 복사본은 수정 자체가 불가능
add
,remove
,set
→UnsupportedOperationException
발생
컬렉션 순회하여 새로운 객체 생성 - 깊은 복사
List<Person> newPersons = new ArrayList<>();
for (Person person : persons) {
newPersons.add(person.copy()); // copy() 메서드는 추가 구현 필요
}
- 원본과 다른 주소를 참조
- 참조형의 요소들도 다른 주소를 갖는 새로운 객체
깊은 복사한 리스트를 불변으로 반환하고 싶다면, List.copyOf()
나 Collections.unmodifiableList()
를 사용하면 됩니다.
List<Person> newFinalList = List.copyOf(newPersons);
위의 다양한 복사 방법에 대해 간단한 테스트를 해보았습니다.
@Test
void copy() {
// List 초기화
List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3));
list.add(4);
System.out.println("list : " + list);
// 1. 방어적 복사 - new ArrayList
List<Integer> listNew = new LinkedList<>(list);
listNew.add(5);
System.out.println("listNew : " + listNew);
// 2. 방어적 복사 + 불변 - List.copyOf
List<Integer> listCopyOf = List.copyOf(list);
// listCopyOf.add(6); // UOE 발생
// listCopyOf.set(0, 6);
list.add(5);
System.out.println("listCopyOf : " + listCopyOf);
// 3. 참조 연결 + 불변 - Collections.unmodifiableList
List<Integer> listUnmodifiable = Collections.unmodifiableList(list);
// listUnmodifiable.add(7); // UOE 발생
// listUnmodifiable.set(0, 6); // UOE 발생
list.add(6);
System.out.println("listUnmodifiable : " + listUnmodifiable);
// 4. 불변 - List.of
List<Integer> listOf = List.of(1, 2, 3, 4);
// listOf.add(5); // UOE 발생
System.out.println("list at last : " + list);
}
// 결과
list : [1, 2, 3, 4]
listNew : [1, 2, 3, 4, 5]
listCopyOf : [1, 2, 3, 4]
listUnmodifiable : [1, 2, 3, 4, 5, 6]
list at last : [1, 2, 3, 4, 5, 6]
Process finished with exit code 0
List 아닌 컬렉션
Map
, Set
과 같은 컬렉션도, 위의 내용에 부합하는 메서드들이 지원됩니다.
Collections.unmodifiableMap()
Collections.unmodifiableSet()
Map.copyOf()
Set.copyOf()
// etc..
이상에서 컬렉션의 복사 종류와, 다양한 복사 방법에 대해서 알아보았습니다.
글이 잘못된 점이나 궁금하신 점이 있다면 지적 부탁드리며,
글을 줄이겠습니다.
감사합니다.