(Flutter-기초 강의) 7. Dart – 객체(Object) 불변성 및 비교 알기 (Immutable Object, Shallow Copy / Deep Copy – feat. Equatable Package)

앞서 Dart에 대해 3. Dart 소개 – 개요, 특징, 키워드4. Dart 필수 문법 및 동작의 이해 에서 다루긴 했지만 flutter 객체의 특성 중 불변 객체(Immutable Obejct), 얕은 복사(Shallow Copy) / 깊은 복사(Deep Copy)에 대해 알지 못하면 이해하는데 혼란이 올 수 있으므로 이 설명하고 가도록 하겠습니다.

불변 객체 (immutable object)란 ?

개요

불변 객체(Immutable Object)는 한 번 생성된 후에는 그 상태가 변경될 수 없는 객체를 말합니다.

이러한 특성 덕분에 불변 객체는 여러 가지 이점을 제공합니다:

가변 객체 vs 불변 객체

구분가변 객체 (Mutable Object)불변 객체 (Immutable Object)
정의객체가 생성된 후에도 상태(속성 값)를 변경할 수 있음.한 번 생성되면 그 상태가 변경될 수 없는 객체.
상태 변경가능하며, 객체의 속성을 자유롭게 변경할 수 있음.불가능. 객체의 속성은 생성 시에만 설정됨.
사용 사례상태가 자주 변경되거나 동적인 데이터 관리에 적합.공유 자원, 구성 설정, 멀티스레드 환경에서 안전.
쓰레드 안전성쓰레드 안전을 직접 관리해야 함. 동시성 문제가 발생할 수 있음.자연스럽게 쓰레드에 안전. 동시성 관리가 용이함.
부수 효과변경 가능성 때문에 예기치 않은 부수 효과 발생 가능.부수 효과가 없어 프로그램의 예측 가능성이 높음.
메모리 효율성새로운 상태를 반영하기 위해 추가 메모리 할당이 필요할 수 있음.같은 값의 객체를 재사용할 수 있어 메모리 효율적임.
종류List, Set, Map, Custom ClassesString, int, double, bool, const objects

얕은 복사(Shallow Copy) / 깊은 복사(Deep Copy)

얕은 복사(Shallow Copy)와 깊은 복사(Deep Copy)는 객체를 복사하는 두 가지 기본적인 방법입니다.

각각의 차이점을 이해하기 위해 Dart 언어를 사용한 예제를 통해 설명하겠습니다.

Shallow_Copy_Deep_Copy

얕은 복사 (Shallow Copy)

얕은 복사(Shallow Copy)란, 참조(메모리 주소)만 복사하는 것을 의미합니다.

이 경우, 복사된 객체와 원본 객체는 내부의 중첩된 객체나 배열 등에 대한 참조를 공유하게 됩니다.

Example)

이 예제에서 rect2rect1topLeftbottomRight 객체에 대한 참조를 공유합니다.

따라서 rect1topLeft 객체를 변경하면, rect2topLeft도 영향을 받습니다.

class Point {
  int x;
  int y;

  Point(this.x, this.y);
}

class Rectangle {
  Point topLeft;
  Point bottomRight;

  Rectangle(this.topLeft, this.bottomRight);
}

void main() {
  var rect1 = Rectangle(Point(0, 0), Point(10, 10));
  var rect2 = Rectangle(rect1.topLeft, rect1.bottomRight); // Shallow Copy

  rect1.topLeft.x = 5; // Change rect1's topLeft

  print(rect2.topLeft.x); // rect2's topLeft is changed also
}

깊은 복사 (Deep Copy)

깊은 복사(Deep Copy)란, 값이 동일한 객체를 새롭게 생성하는 것을 의미합니다.

복사된 객체와 원본 객체는 서로 독립적이며, 내부 객체들의 참조도 별도로 복사됩니다.

Example)

Point Class와 Rectangle Class에 copyWith()를 통해 새로운 Point 객체와 Rectangle 객체를 만들어 반환함으로써 깊은 복사를 수행하였습니다.

class Point {
  int x;
  int y;

  Point(this.x, this.y);

  // Point's Deep Copy
  Point copyWith({int? x, int? y}) {
    return Point(x ?? this.x, y ?? this.y);
  }
}

class Rectangle {
  Point topLeft;
  Point bottomRight;

  Rectangle(this.topLeft, this.bottomRight);

  // Rectangle's Deep Copy
  Rectangle copyWith({Point? topLeft, Point? bottomRight}) {
    return Rectangle(
      topLeft?.copyWith() ?? this.topLeft.copyWith(),
      bottomRight?.copyWith() ?? this.bottomRight.copyWith(),
    );
  }
}

얕은 복사 vs 깊은 복사 특징 비교

구분얕은 복사 (Shallow Copy)깊은 복사 (Deep Copy)
정의객체의 최상위 레벨만 복사하고, 내부 구조(참조)는 원본 객체와 공유.객체의 모든 레벨을 복사하여 완전히 독립된 복사본을 생성.
참조 공유내부 객체나 배열 등의 참조를 원본 객체와 공유.내부 객체나 배열 등의 참조도 복사되어, 원본과 복사본이 서로 독립적.
변경 영향원본 객체의 변경이 복사본에 영향을 미침.원본 객체의 변경이 복사본에 영향을 미치지 않음.
사용 사례빠른 복사가 필요하고, 내부 객체의 변경이 없거나 공유되어도 괜찮은 경우.원본 객체와 완전히 독립된 복사본이 필요한 경우(예: 데이터 수정, 멀티스레드 환경).
성능복사 과정이 빠름(단순 참조 복사).복사 과정이 느림(객체의 모든 내용을 복사해야 함).
메모리 사용적은 메모리 사용(복사본이 원본의 일부 데이터를 공유하기 때문에).더 많은 메모리 사용(복사본이 원본의 모든 데이터를 독립적으로 가짐).

비교

Dart에서의 비교

Dart의 기본 객체 비교 방식은 얕은 비교(Shallow Comparison)를 사용한다는 것입니다.

이는 굉장히 중요한데, 이에 따라서 가변 객체의 경우 값이 변경되더라도 주소값이 변경되지 않으므로 ==를 통해 비교시 같다는 결과를 같게 됩니다.

이는 두 객체가 동일한 메모리 주소를 가리키는지를 확인하는 방식입니다. 그러나, 대부분의 경우 불변 객체의 상태(값) 자체를 비교하고 싶을 때가 많습니다.

문제 예제

이 예제에서 point1point2는 처음에 동일한 MutablePoint 객체를 참조합니다.
(앞서 말했듯이 Dart에서 일반 객체는 가변 객체입니다.)

이후에 point1xy 값을 변경해도 point2는 여전히 point1과 같은 객체를 참조하고 있습니다.

즉, 메모리 주소가 같기 때문에 point1 == point2는 여전히 true를 반환합니다.

이러한 얕은 비교 방식은 객체의 내부 상태가 변했음에도 불구하고 동일한 참조를 가지고 있다면 객체를 ‘동일하다’고 판단합니다.

class MutablePoint {
  int x;
  int y;

  MutablePoint(this.x, this.y);
}

void main() {
  var point1 = MutablePoint(2, 3);
  var point2 = point1; // point2 references the same object as point1.

  print(point1 == point2); // true, both reference the same memory address

  point1.x = 5; // changing the x value of point1
  point1.y = 6; // changing the y value of point1

  // Even after changing the values in point1, point2 still references the same object as point1.
  print(point1 == point2); // still true, as they reference the same object
}

개선

operator == ()hashCode() 재정의를 통한 값 비교 수행하기

operator == method를 override 함으로써 값을 비교하도록 개선하였습니다.

hashCode를 함께 수정해주지 않으면 Map, Set 등의 해시 기반 자료형에서 의도와 다르게 동작할 수 있습니다.

class MutablePoint {
  int x;
  int y;

  MutablePoint(this.x, this.y);

  @override
  bool operator ==(Object other) {
    if (identical(this, other)) return true;
    return other is MutablePoint && other.x == x && other.y == y;
  }

  @override
  int get hashCode => x.hashCode ^ y.hashCode;
}

void main() {
  var point1 = MutablePoint(2, 3);
  var point2 = MutablePoint(2, 3); 

  print(point1 == point2); // true, 

  point1.x = 5; // Change point1's x
  point1.y = 6; // Change point1's y

  print(point1 == point2); // false, point 1 and point 2 are different
}

copyWith() 메소드를 구현하여 깊은 복사를 수행하기

깊은 복사를 통해 메모리만 복사하는게 아니고, 새로운 객체로 깊은 복사를 수행했으므로

원본의 값을 바꾸더라도 복사한 객체의 값이 변경되지 않습니다.

class Point {
  int x;
  int y;

  Point(this.x, this.y);

  // deep copy of Point
  Point copyWith({int? x, int? y}) {
    return Point(x ?? this.x, y ?? this.y);
  }
}

class Rectangle {
  Point topLeft;
  Point bottomRight;

  Rectangle(this.topLeft, this.bottomRight);

  // deep copy of Rectangle
  Rectangle copyWith({Point? topLeft, Point? bottomRight}) {
    return Rectangle(
      topLeft?.copyWith() ?? this.topLeft.copyWith(),
      bottomRight?.copyWith() ?? this.bottomRight.copyWith(),
    );
  }
}

void main() {
  var rect1 = Rectangle(Point(0, 0), Point(10, 10));
  var rect2 = rect1.copyWith(); // 깊은 복사

  rect1.topLeft.x = 5; // rect1의 topLeft 변경

  print(rect2.topLeft.x); // 0, rect2's topLeft is not changed
}

Equatable 패키지

Reference Link: https://pub.dev/packages/equatable

Equatable 패키지를 사용하면 객체 비교를 간편하게 수행할 수 있습니다.

Equatable은 객체의 동등성 비교를 위해 == 연산자와 hashCode를 쉽게 오버라이드할 수 있도록 도와주는 유틸리티 패키지입니다.

설치

$ dart pub add equatable

사용법

만약 Equatable을 사용하지 않을 경우 다음과 같이 operator == 와 hashCode를 override를 통해 구현해주어 동등성을 판단해야합니다.

class Point {
  final int x;
  final int y;

  Point(this.x, this.y);

  @override
  bool operator ==(Object other) {
    if (identical(this, other)) return true;
    if (other is! Point) return false;

    return other.x == x && other.y == y;
  }

  @override
  int get hashCode => x.hashCode ^ y.hashCode;
}

void main() {
  var point1 = Point(2, 3);
  var point2 = Point(2, 3);
  var point3 = Point(4, 5);

  print(point1 == point2); // true, point1 is point2 same in deep comparison
  print(point1 == point3); // false, point1 is point3 different in deep comparison
}

그러나, Equatable을 extends를 통해 상속하고

List<Object?> get props 에 동등성을 판단할 변수를 넣어주어서 값 비교를 통해 보다 간편하게 동등성을 판단할수 있게 해줍니다.

import 'package:equatable/equatable.dart';

class Point extends Equatable {
  final int x;
  final int y;

  Point(this.x, this.y);

  @override
  List<Object?> get props => [x, y];
}

void main() {
  var point1 = Point(2, 3);
  var point2 = Point(2, 3);
  var point3 = Point(4, 5);

  print(point1 == point2); // true, point1과 point2는 동등함
  print(point1 == point3); // false, point1과 point3는 다름
}

참조 링크

Official Document (Dart)

내 Dart Github 저장소 (10_immutable)

Leave a Comment