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

Java

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

Alex96 2022. 3. 5. 19:04

우아한테크코스 두번 째 미션을 진행하던 중...

제 미션의 리뷰를 맡아주신 리뷰어분이 깊은 복사와 방어적 복사에 대해 고민하고 적용시켜보라는 말을 해주셔서 포스팅을 해보려 합니다😄

 

깊은 복사

 

깊은 복사는 객체를 담는 컬렉션 몸통과 그 안의 객체 모두 새로운 주소를 가진 것으로 새로 만들어내는 것을 말합니다.

간단히 말하면 원본과의 관계를 완전히 끊어내는 복사라고 볼 수 있습니다.

그림으로 예시를 들면,,

깊은 복사

 

위 그림처럼 새로운 컬렉션을 만들고, 그 안의 객체도 값이 같은 새로운 객체로 만들어서 복사한 객체를 담아서 원본과 전혀 관계가 없는 컬렉션을 만드는 것을 말합니다.

 

코드를 작성하면 이렇게 되겠네요

class Car {
    private String name;

    public Car(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    // equals and hashCode ...
}

위처럼 Car 클래스가 있을 때

@Test
@DisplayName("깊은 복사")
void deepCopy() {
    List<Car> cars1 = List.of(new Car("alex"), new Car("kei"),
            new Car("cris"), new Car("roma"));

    List<Car> cars2 = new ArrayList<>();
    cars1.forEach(car -> cars2.add(new Car(car.getName())));
    cars2.get(0).setName("새 이름");

    assertThat(cars1).isNotEqualTo(cars2);
}

이런 테스트 코드가 통과합니다. (Car 클래스는 name이 같으면 동등한 객체로 보도록 equals, hashCode가 정의되어있음)

왜냐면 cars2는 새로운 컬렉션 통에 이름만 같은 새로운 Car 객체를 담은 컬렉션이기 때문에, 복사된 값을 변경해도 원본에는 변화가 전혀 일어나지 않거든요.

하지만 방어적 복사는 조금 다릅니다.

 


 

방어적 복사

 

방어적 복사는 간단히 말하면 컬렉션 통만 바꿔주는 것을 말하는데요,

컬렉션만 새걸로 만들고 그 안의 원소의 주소들은 그대로 옮겨담아서 같은 객체를 참조하도록 하는걸 말합니다.

그림으로 보면,

방어적 복사

이렇게 되어있다. 새로운 컬렉션에 원본 객체들의 주소를 그대로 담아 가지므로,

컬렉션의 요소를 추가하거나, 삭제하거나, 바꿔넣어도 원본 컬렉션이 변하지는 않습니다.

다만, 그 컬렉션 요소의 객체가 원본과 동일한 객체이기 때문에 요소객체가 변하면 원본 요소객체도 변해요.

 

코드로 살펴보면,

위와 같은 Car클래스의 경우

@Test
@DisplayName("방어적 복사")
void defensiveCopy() {
    List<Car> cars1 = List.of(new Car("alex"), new Car("kei"),
            new Car("cris"), new Car("roma"));

    List<Car> cars2 = new ArrayList<>(cars1);
    cars2.get(0).setName("새 이름");
    assertThat(cars1).isEqualTo(cars2);
}

이런 테스트가 통과하게 됩니다.

복사된 컬렉션의 요소객체에 setter메서드를 호출해서 바꿨더니 원본도 같이 바뀌었기 때문에 isEqualTo가 통과되는 것이죠.

 

그럼 방어적 복사 위험한거 아니야?

 

방어적 복사로 불변성을 얻고자 한다면, 요소 클래스를 불변으로 설계하면 가능합니다.

Car를 불변으로 설계하면,

class Car {
    private final String name;

    public Car(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    // equals and hashCode ...
}

이렇게 되겠네요. name에 final을 붙여서 객체 생성시에만 값을 넣어주고 이후에 변경이 불가능하게 했습니다.

당연히 setter 메서드도 없구요.

 

이렇게 컬렉션에서 다루는 요소 객체가 불변이라면 값을 바꿀 수 없으니 방어적 복사를 해도 원본 객체에 지장이 가지 않는 새로운 컬렉션을 만들 수 있습니다.

 

개인적으로 방어적 복사를 훨씬 많이 사용하게 되더라구요,

깊은 복사는 컬렉션을 새로 만들고 객체를 하나하나 새로 만들어서 담아줘야하는 장황한 코드가 필요하다 보니...

반면에 방어적 복사는 짧은 코드로 구현이 가능하구요.(생성자나 정적 팩토리 메서드에 파라미터로 원본 컬렉션을 넘겨주면 끝...)

그래도 불변으로 설계할 수 없는 클래스를 다루는 컬렉션이라면 깊은 복사를 하는게 안정성이 있지 않을까 합니다..🥸