주니어 개발자 성장기

방어적 복사, 얕은 복사, 깊은 복사 본문

Java/이펙티브 자바

방어적 복사, 얕은 복사, 깊은 복사

Junpyo Lee 2023. 9. 2. 19:56

방어적 복사란?


개요

방어적 복사(Defensive Copy)란 Java에서 불변 객체(immutable objects)에서 쓰이는 용어이다. 불변 객체는 일단 생성이 된 뒤에는 객체 내부의 상태가 변경되어서는 안된다. 그리고, 방어적 복사는 불변 객체 내부의 상태(즉, 필드)에 변경을 방지하기 위해서 사용하는 기법이라고 할 수 있다.



문제 상황

다음과 같은 FooCalendar라는 클래스가 있다고 가정해보자.

public final class FooCalendar {
  private final Date standardDate;
 
  public FooCalendar(Date date) {
    standardDate = date;
  }
 
  public Date getStandardDate() {
    return standardDate;
  }
}

위 클래스는 private final 필드로 Date 타입의 standardDate를 정의하고 있다. 이런 구현 방식인 표면적으로 보기에는 객체가 생성되고 나면, setter도 없을 뿐더러 private final로 필드가 선언되어 있어 필드가 바뀔 염려가 없다고 착각하기 쉽다. 하지만 다음 예시 클라이언트 코드를 보자


Date originalDate = new Date();
FooCalendar fooCalendar = new FooCalendar(originalDate);
originalDate.setYear(150);

FooCalendar의 생성자에 전달된 매개변수 originalDatefooCalendar 인스턴스 내부의 standardDate 필드에 할당된다. 당연히 Date는 레퍼런스 타입이므로, 두 변수는 메모리상에 있는 같은 인스턴스를 참조하며 originalDate의 내부를 변경하면 standardDate 역시 변하게 된다. 즉, FooCalendar의 불변성(immutability)을 보장할 수가 없다. 한편, getter에서도 같은 문제가 발생할 수 있다.


Date dateReference = fooCalendar.getStandardDate();
dateReference.setYear(150);

getStandardDate()를 통해 반환 받은 마찬가지로 Date는 내부의 standardDate의 참조를 그대로 반환하는 것이며 이는 외부에서 수정이 가해지면 그대로 fooCalendar에도 반영됨을 의미한다.





해결책

해결법은 간단하다. 우선, 생성자로 받은 매개변수를 필드로 바로 대입하는 것이 아니라, 다음과 같이 완전히 같은 객체를 복사해서 대입하는 것이다.

// FooCalendar.class
public FooCalendar(Date date) {
  standardDate = new Date(date.getTime());
}

생성자를 위와 같이 작성하게 되면 각각 standardDateoriginalDate가 아니라 새로 생성된 인스턴스를 가리키게 되며 불변성을 보장할 수 있게 된다.



다음, getter를 통해 객체를 반환할 때도 필드를 그대로 반환 하는 것이 아니라 새로운 객체를 복사해서 반환해준다.

public Date getStandardDate() {
  return new Date(standardDate.getTime());
}

위와 같은 사항들을 지키면 필드가 레퍼런스 타입이더라도 객체의 불변성을 보장할 수 있으며, 이것을 방어적 복사(Defensive Copying or Copy)라고한다.



*String과 래퍼 클래스들도 불변 객체라고 한다.



얕은 복사? 깊은 복사? 방어적 복사?

얼마 전, 이펙티브 자바스터디를 진행하면서 아이템 6에 관해 토론했는데, 한 스터디원 분께서 특정 블로그를 통해 참고한 내용을 말씀해주셨다. 내용인 즉, 방어적 복사는 객체의 껍데기를 복사하는 것에 불과하다는 것이다. 다시 말해서, 복사되는 객체 내부의 필드값(그러니까 레퍼런스 타입이라면 참조를)은 모두 동일하다는 것이다.
그 당시에는 내가 알고 있는 것이 잘 정리가 되지 않아서(아이템 50인 방어적 복사는 아직 범위를 훨씬 앞서 있었다.🤣) 막연히 “그럼 방어적 복사가 왜 ‘Defensive’라는 워딩을 쓰는거지?”하는 의문이 들었다. 나는 그래서 나름대로 반박(?)을 해봤지만 확신이 서지 않았고 덕분에 과연 차이점이 무엇인지 숙제로 조사를 맡게 되었다.


해당 블로그의 글도 살펴보고 여러가지 해외 레퍼런스들을 참조한 결과 나는 다음과 같은 결과를 도출해냈다.

얕은 복사(Shallow Copy)와 깊은 복사(Deep Copy) 서로 대비되는 개념으로 서로 비교 분석하기 좋은 개념이다. 하지만, 그 둘과 방어적 복사와는 관심을 갖는 주제가 다르기 때문에 둘을 단순히 비교하는 것은 바람직하지 않은 아니, 틀린 것이다.


쉽게 비유를 들자면 우리는 디자인 패턴에서 종종 그 디자인 패턴을 어떻게 구현하는 지에만 몰두하는 경우가 있다.(내가 처음 배울 때는 그랬었다.) 하지만 우리는 디자인 패턴의 구현보다는 의도목적을 더 중요시해야 한다. 디자인 패턴은 특정 문제 상황에서 선험적으로 자주 사용되는, 효율적인 패턴들을 모아 놓은 것이다. 물론, 구현이 중요하지 않은 것은 아니지만 구현에 앞서 우리는 ‘문제 상황’이라는 맥락과 궁극적으로 이 ‘패턴이 문제를 (객체지향적으로) 해결하고자 하는 방향’을 이해해야만 한다.
얕은 복사, 깊은 복사, 방어적 복사의 비교도 마찬가지다. 얕은 복사와 깊은 복사는 같은 관심사(변수의 값 대입)에 대해서 서로 다른 상황을 의미하는 것이다. 반면, 방어적 복사는 ‘불변 객체의 관리’라는 관심사를 가지며 이 과정에서 프로그래머가 얕은 복사를 사용해도 깊은 복사를 사용해도 상관이 없으며 완벽히 다른 관심사에 있는 용어와 개념을 비교하는 것은 오해를 낳는 것이다. 마치 강아지의 품종(말티즈, 리트리버, 불독, 허스키 등)을 비교 하다가 발음이 비슷하다고 해서 말티즈와 게(Crab)를 비교하는 것이다.
이 개념을 동시에 적용하여 객체의 불변을 보장하기위한 방어적 복사를 할 때 내부의 필드가 가진 필드들 까지 깊은 복사를 할지 말지는 오직 코드를 작성하는 프로그래머의 몫이다. 어떤 객체는 1차적으로 필드로 갖고 있는 배열 자체의 형태를 방어하고자 할 수도 있으며 어떤 객체는 완전한 Deep Copy가 필요할 수도 있다.


마지막으로, 해당 블로그 포스팅의 의견을 갖고 와주신 스터디원 분께 깊은 감사를 표한다.




참조: Java Defensive Copying
Can someone explain the difference between a deep copy and a defensive copy?