반응형

 

컬렉션을 반환할 때, 원본의 참조 값을 그대로 반환하는 경우

원하지 않는 곳에서 컬렉션을 직접 변경할 수 있습니다.

값이 추가되거나 수정되거나 삭제되어 문제가 발생할 수 있습니다.

 

따라서, 수정이 필요하지 않은 경우라면 컬렉션의 원본을 그대로 반환하기보다

복사해서 보내는 것이 좋은 방법입니다.

 

본 아티클에서는

첫째로, 복사의 여러 종류에 대해 알아보겠습니다.

둘째로, 복사의 종류와 불변 여부를 고려한, 다양한 복사의 사용법에 대해서 알아보겠습니다.

 

** 편의상 컬렉션의 여러 종류 중 리스트를 중점적으로 다뤄보겠습니다.

 

 


복사의 종류

그에 앞서, 예시로 사용할 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 메모리 상에서 personsnewPersons는 별도의 객체이지만, 같은 주소값을 가지고 있습니다.

그리고 Heap 메모리 영역에 있는 하나의 객체를 가리키고 있습니다.

 

위의 결과처럼, 둘은 동일한 객체입니다.

 

 

방어적 복사

방어적 복사는 원본의 참조가 유지되지 않는 복사입니다.

List<Person> newPersons = new ArrayList<>(persons);

ArrayList의 생성자는, Collection 타입을 인자로 받으면 해당 컬렉션의 요소들을 가지고 있는 새로운 ArrayList를 생성합니다. (LinkedList와 같은 다른 구현체도 마찬가지입니다.)

얕은 복사와 얼핏 보면 비슷하지만, 다른 점이 있습니다.

  Stack 영역에서 Stack 영역에서 가지고 있는 주소 Heap 영역
얕은 복사 분리된 객체 동일함 하나의 객체
방어적 복사 분리된 객체 서로 다름 서로 다른 객체

위의 결과처럼, 둘은 동일한 객체가 아닙니다.

 

하지만, 방어적 복사도 완벽한 복사 방식은 아닙니다.

내부에 가지고 있는 요소가 참조형이라면, 같은 객체를 참조하고 있을 수 있습니다.

 

 

위의 결과처럼, personsnewPersons의 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, setUnsupportedOperationException 발생
    • ImmutableCollections를 생성하여 반환하기 때문
    • List.of() 메서드를 통해 초기화한 리스트도 같은 이유로 수정이 불가능

 

Collections.unmodifiableList() - 참조 연결 + 불변

List<Person> newPersons = Collections.unmodifiableList(persons);
  • 원본과 다른 주소를 참조
  • 원본의 변경이 복사본에 그대로 반영
  • 복사본은 수정 자체가 불가능
    • add, remove, setUnsupportedOperationException 발생

 

컬렉션 순회하여 새로운 객체 생성 - 깊은 복사

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..

 


이상에서 컬렉션의 복사 종류와, 다양한 복사 방법에 대해서 알아보았습니다.

글이 잘못된 점이나 궁금하신 점이 있다면 지적 부탁드리며,

글을 줄이겠습니다.

감사합니다.

 

 

 

 

반응형

+ Recent posts