앞서 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 Classes | String, int, double, bool, const objects |
얕은 복사(Shallow Copy) / 깊은 복사(Deep Copy)
얕은 복사(Shallow Copy)와 깊은 복사(Deep Copy)는 객체를 복사하는 두 가지 기본적인 방법입니다.
각각의 차이점을 이해하기 위해 Dart 언어를 사용한 예제를 통해 설명하겠습니다.
얕은 복사 (Shallow Copy)
얕은 복사(Shallow Copy)란, 참조(메모리 주소)만 복사하는 것을 의미합니다.
이 경우, 복사된 객체와 원본 객체는 내부의 중첩된 객체나 배열 등에 대한 참조를 공유하게 됩니다.
Example)
이 예제에서 rect2
는 rect1
의 topLeft
와 bottomRight
객체에 대한 참조를 공유합니다.
따라서 rect1
의 topLeft
객체를 변경하면, rect2
의 topLeft
도 영향을 받습니다.
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)를 사용한다는 것입니다.
이는 굉장히 중요한데, 이에 따라서 가변 객체의 경우 값이 변경되더라도 주소값이 변경되지 않으므로 ==
를 통해 비교시 같다는 결과를 같게 됩니다.
이는 두 객체가 동일한 메모리 주소를 가리키는지를 확인하는 방식입니다. 그러나, 대부분의 경우 불변 객체의 상태(값) 자체를 비교하고 싶을 때가 많습니다.
문제 예제
이 예제에서 point1
과 point2
는 처음에 동일한 MutablePoint
객체를 참조합니다.
(앞서 말했듯이 Dart에서 일반 객체는 가변 객체입니다.)
이후에 point1
의 x
와 y
값을 변경해도 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는 다름 }