티스토리 뷰

반응형

플러터는 다트 언어로 개발한다. 개발 시 얕은 복사와 깊은 복사를 잘 알아야 원치 않는 부작용을 방지할 수 있다. 

 

 

 

불변 타입과 가변 타입

다음 코드에서 intOrigin과 myIntOrigin.value은 무슨 값이 출력될까?

class MyInt {
  int value = 0;
  MyInt(this.value);
}

void main() {
  int intOrigin = 0;
  int intCopied = intOrigin;
  intCopied = 1;
  
  MyInt myIntOrigin = MyInt(0);
  MyInt myIntCopied = myIntOrigin;
  myIntCopied.value = 1;
  
  print('intOrigin: $intOrigin');
  print('intCopied: $intCopied');
  print('myIntOrigin: ${myIntOrigin.value}');
  print('myIntCopied: ${myIntCopied.value}');
}
intOrigin: 0
intCopied: 1
myIntOrigin; 1
myIntCopied; 1

동일하게 =연산자로 할당을 하고 수정을 했는데, 왜 intOrigin은 값이 변하지 않았을까?

 

그 이유는 다트의 기본 자료형인 int, double, bool, String, null은 불변이기 때문에 할당시 복사가 이루어진다. 즉 서로 다른 값을 참조하게 된다. 그러나 불변이 아닌 다른 자료형들은 참조가 복사되어 같은 값을 가르키게 된다. 따라서 위의 예제에서 intOrigin은 불변이기 때문에 복사가 이루어져서 intCopied과 서로 다른 참조를 가지게 된다. 반면 myIntOrigin은 불변이 아니기 때문에 참조가 복사되어서 myIntCopied와 동일한 참조를 가지게 된다. 

 

이처럼 불변인 데이터 타입들을 불변 타입이라고 하고, 그 외에 타입들은 가변 타입이 된다. 불볍 타입은 할당 시 값이 복사되고, 가변 타입은 할당 시 같은 참조가 된다는 것을 꼭 기억하자.

 

 

 

얕은 복사? 깊은 복사?

프로그래밍 관점에서 얕은 복사과 깊은 복사는 다음과 같은 개념을 가진다.

얕은 복사는 객체의 참조만 복사하는 방식이다. 참조만 복사하기 때문에 한 객체를 수정하면 다른 객체도 동일하게 수정된다. 얕은 복사의 장점은 복사 작업이 빠르고 메모리를 절약하게 되기 때문에 성능이 좋아진다. 단점은 다른 객체의 수정으로 인해 원치 않는 결과가 발생할 수 있다.

 

깊은 복사는 객체의 내용을 완전히 복사하는 방식이다. 복사된 객체는 원본 객체와 완전히 별개의 객체로 취급한다. 따라서 한 객체를 수정해도 다른 객체는 영향을 받지 않는다. 깊은 복사의 장점은 모든 객체를 완전히 독립적으로 다룰 수 있기 때문에 안전성과 예측 가능성이 높아진다. 단점은 중첩된 객체의 복사 작업이 필요하기 때문에 시간과 메모리를 더 많이 소모된다.

 

 

 

다트는 얕은 복사만 제공한다

다트의 콜렉션 타입은 명명된 from 생성자가 제공되기 때문에 List.from() 같은 얕은 복사만 제공한다. 그 외에도 다트가 제공하는 자료형들은 얕은 복사가 제공되기도 하니 레퍼런스 문서를 확인해보는 것이 좋다. 하지만 깊은 복사를 수행하려면 개발자가 직접 객체의 구조를 확인하고 복사해야 한다.

 

위에서는 얕은 복사가 참조만 복사한다고 했지만, 다트 컬렉션의 얕은 복사는 조금 다르게 적용된다. 우선 다트 컬렉션의 얕은 복사는 컬렉션 인스턴스를 새로 만들지만 내부 요소들은 참조를 복사하게 된다. 그래서 다음 코드를 실행하면 콜렉션 인스턴스의 참조는 다르지만, 콜렉셔 내부 요소들은 같은 참조인 것을 확인할 수 있다.

class MyInt {
  int value = 0;
  MyInt(this.value);
}

void main() {
  List<int> originIntList = [0];
  List<int> copiedIntList = List.from(originIntList);
  List<MyInt> originMyIntList = [MyInt(0)];
  List<MyInt> copiedMyIntList = List.from(originMyIntList);
  
  print(originIntList.hashCode);  // 1032263997
  print(copiedIntList.hashCode);  // 675503418
  print(originIntList[0].hashCode);  // 0
  print(copiedIntList[0].hashCode);  // 0
  
  print(originMyIntList.hashCode);  // 846102734
  print(copiedMyIntList.hashCode);  // 218521006
  print(originMyIntList[0].hashCode);  // 1004552072
  print(copiedMyIntList[0].hashCode);  // 1004552072
}

 

 

 

얕은 복사라도 불변 타입인지 가변 타입인지에 따라서 결과가 달라진다

이제 다트 콜렉션의 얕은 복사는 인스턴스만 생성하고 내부 요소는 참조를 복사한다는 것을 알게 되었다.

 

그러면 다음 코드를 실행하면 어떤 결과가 출력될까?

class MyInt {
  int value = 0;
  MyInt(this.value);
  
  String toString() => 'MyInt($value)';
}

void main() {
  List<int> originIntList = [0];
  List<int> copiedIntList = List.from(originIntList);
  List<MyInt> originMyIntList = [MyInt(0)];
  List<MyInt> copiedMyIntList = List.from(originMyIntList);
  
  
  copiedIntList[0] = 10;
  copiedMyIntList[0].value = 10;
  
  print(originIntList);  // [0]
  print(copiedIntList);  // [10]
  print(originMyIntList);  // [MyInt(10)]
  print(copiedMyIntList);  // [MyInt(10)]
}

 

분면 다트 컬렉션의 얕은 복사는 요소의 참조를 복사한다고 했지만, originIntList는 값이 변하지 않았다! 그 이유는 불변 데이터은 값이 복사되기 때문이다. 

 

이 내용은 정말 중요하다고 생각한다. 다트의 불변 타입의 얕은 복사가 어떻게 작동하는지 정확히 모른다면, 잘못된 지식이 원치않은 부작용으로 나타나기 때문이다. 

 

 

 

깊은 복사 구현하기

이제 깊은 복사를 구현해보자. 다트는 깊은 복사를 지원하지 않으니 개발자가 직접 구현해야 한다. 깊은 복사를 하기 위해서는 깊은 복사에 사용되는 타입에서 복제 기능을 지원해야 한다. 

 

복제 기능은 객체가 가지는 속성과 동일한 속성을 가지는 새로운 객체를 만드는 책임을 가진다. 다트에서 복제 기능을 구현하는 방법은 생성자로 구현하거나 메서드로 구현할 수 있다.

 

다음 코드는 MyInt.clone 이라는 명명된 생성자로 구현한 복제 기능과 copy 메서드로  구현한 복제 기능을 가지는 클래스가 된다.

class MyInt {
  int value = 0;
  MyInt(this.value);
  MyInt.clone(MyInt myInt)
    : this(myInt.value);
  
  MyInt copy() {
    return MyInt(value);
  }
  
  String toString() => 'MyInt($value)';
}

 

다음 코드를 실행하면 복제 기능이 잘 작동하는 것을 확인할 수 있다. 반면 단순 할당은 같은 참조인 것을 확인할 수 있다.

void main() {
  MyInt origin = MyInt(0);
  MyInt copied1 = origin;
  MyInt copied2 = MyInt.clone(origin);
  MyInt copied3 = origin.copy();
  
  print(origin.hashCode);  // 1009964724
  print(copied1.hashCode); // 1009964724
  print(copied2.hashCode); // 893634546
  print(copied3.hashCode); // 359260343
}

 

 

 

콜렉션 인스턴스를 깊은 복사하는 방법

콜렉션 인스턴스의 요소가 깊은 복사를 지원하면 map를 이용해서 쉽게 깊은 복사를 할 수 있다. 각 요소별로 반복해서 복제 기능을 실행시키면 된다. 여기서는 map을 이용해서 콜렉션 인스턴스를 깊은 복사하는 방법을 보여주겠다.

void main() {
  List<MyInt> origin = [MyInt(0), MyInt(1)];
  List<MyInt> copied = origin.map((e) => e.copy()).toList();
  
  print(origin.hashCode);  // 1006928153
  print(copied.hashCode);  // 840395391
  print(origin[0].hashCode);  // 392805042
  print(copied[0].hashCode);  // 452799614
}

 

 

 

마무리 하며...

마지막으로 다트의 깊은 복사에 대해서 다시 요약해보자.

1. 다트의 타입은 불변 타입과 가변 타입이 있다.

2. 다트는 얕은 복사만 제공한다.

3. 얕은 복사라도 불변 타입인지 가변 타입인지에 따라 내부 동작이 달라진다.

반응형
댓글
공지사항