[Flutter] 객체 불변성이란?

hyenni·2025년 7월 6일

Flutter

목록 보기
4/5
post-thumbnail

1. 불변성의 정의

불변성(Immutability)이란 객체가 생성된 후 그 상태를 변경할 수 없는 특성을 말합니다.
즉, 불변 객체는 한 번 만들어지면 그 값을 바꿀 수 없는 객체입니다.

class Person {
  final String name;
  final int age;

  Person(this.name, this.age);
}

위 예시에서 Person 클래스는 final 필드를 가지고 있으므로, 생성된 이후에는 값을 바꿀 수 없습니다.
하지만 객체 자체는 여전히 변할 수 있습니다. 예를 들어, 리스트를 포함하고 있으면 내부는 바뀔 수 있습니다.

class Team {
  final List<String> members;

  Team(this.members);
}

void main() {
  final team = Team(['Alice', 'Bob']);
  team.members.add('Charlie'); // 가능함

}

final은 변수 자체의 레퍼런스만 고정할 뿐, 내부는 바뀔 수 있습니다.


2. 왜 불변 객체가 중요할까?

✅ 장점

항목설명
예측 가능한 코드객체 상태가 바뀌지 않기 때문에 버그가 줄어듭니다.
테스트 쉬움상태가 고정되므로 단위 테스트하기에 매우 좋습니다.
추론 쉬움값이 바뀌지 않으니 코드 흐름을 추론하기 쉽습니다.
멀티스레딩 안정성상태가 고정되어 있으므로 스레드 간 충돌이 없습니다.
Flutter UI 빌드 최적화변경되지 않으면 UI를 리빌드할 필요가 없어 효율적입니다.

3. Dart에서 객체를 불변으로 만드는 법

final 키워드 사용

각 필드를 final로 선언하면 해당 필드는 한 번만 값을 가질 수 있습니다.

class User {
  final String id;
  final String name;

  User(this.id, this.name);
}

하지만 이 방식으로는 모든 불변 객체를 만들기엔 한계가 있습니다.

  • ==, hashCode, copyWith, toJson 등을 수동으로 작성해야 함
  • 매번 생성자를 일일이 작성해야 함
  • 객체를 변경하려면 새로운 객체를 생성해야 함 → 귀찮음

Operator(==) & hashCode

Dart에서 객체를 비교할 때 기본적으로 주소를 비교합니다. 값이 같더라도 주소를 비교하기 때문에 ==으로 비교하게되면 false로 나옵니다.

final a = User(id: '1', name: 'Alice');
final b = User(id: '1', name: 'Alice');

print(a == b); // false (주소가 다름)

Set이나 Map의 key 비교는 hashCode를 보고 그 다음에 ==으로 비교합니다. 만약 ==은 true인데 hashCode가 다르면 같은 값인데도 다른 객체로 인식되기 때문에 항상 같이 오버라이드 해야합니다.

class User {
  final String id;
  final String name;

  User(this.id, this.name);

  
  bool operator ==(Object other) {
    return other is User && other.id == id && other.name == name;
  }

  // ❌ hashCode는 오버라이드하지 않음!
}

void main() {
  final a = User('1', 'Alice');
  final b = User('1', 'Alice');

  print(a == b); // true → 값은 같다고 판단

  final map = {a: 'hello'};

  print(map[b]); // ❌ null → hashCode가 달라서 찾지 못함
}


==와 hashCode는 항상 함께 정의해야 함

두 객체가 ==로 같다고 판단되면, 그들의 hashCode도 같아야 합니다.

class User {
  final String id;
  final String name;

  User(this.id, this.name);

  
  bool operator ==(Object other) {
    return other is User && other.id == id && other.name == name;
  }

  
  int get hashCode => id.hashCode ^ name.hashCode;
}

void main() {
  final a = User('1', 'Alice');
  final b = User('1', 'Alice');

  print(a == b); // ✅ true
  print(a.hashCode == b.hashCode); // ✅ true

  final map = {a: 'hello'};

  print(map[b]); // ✅ hello → 같은 key로 인식
}

이렇게 하면 값이 같으면 같은 객체로 인식됩니다. 하지만 매번 이런 코드를 쓰는 건 귀찮고 실수 위험도 있습니다.

→ 이를 해결하기 위해 나온 게 Freezed 패키지입니다.


class User with _$User {
  const factory User({
    required String id,
    required String name,
  }) = _User;
}
void main() {
  final a = User(id: '1', name: 'Alice');
  final b = User(id: '1', name: 'Alice');

  print(a == b); // ✅ true
  print(a.hashCode == b.hashCode); // ✅ true

  final map = {a: 'hello'};
  print(map[b]); // ✅ hello
}

→ 내부적으로 operator와 hashcode가 자동 생성됩니다.
→ 값 비교 기반 객체를 안전하고 간결하게 구현 가능합니다.


4. Freezed 패키지로 불변 객체 쉽게 만들기

Freezed란?

Dart에서 불변 객체데이터 클래스를 쉽게 만들 수 있도록 도와주는 코드 생성기입니다.

주요 특징

기능설명
자동 생성==, hashCode, copyWith, toString 등 자동 생성
copyWith 지원일부 필드만 변경한 새 객체 생성 가능
sealed class 지원복잡한 상태 표현에 유리 (예: union type)
진정한 불변성모든 필드는 final, 리스트는 UnmodifiableListView 등으로 감쌈

사용 예시

import 'package:freezed_annotation/freezed_annotation.dart';
part 'user.freezed.dart';
part 'user.g.dart';


class User with _$User {
  const factory User({
    required String id,
    required String name,
  }) = _User;

  factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
}

코드 생성

flutter pub run build_runner build

이 명령어는 user.freezed.dartuser.g.dart 파일을 생성합니다.

자동 생성된 기능들

final user1 = User(id: '1', name: 'Alice');
final user2 = user1.copyWith(name: 'Bob'); // 변경은 이렇게만 가능

print(user1 == user2);

5. JsonSerializable: JSON <-> 객체 변환 자동화

json_serializable 패키지란?

Dart에서 객체와 JSON 간 변환을 자동으로 해주는 코드 생성 도구입니다.
freezed와 함께 사용하면 강력한 시너지를 발휘합니다.

사용하는 이유

  • API 통신할 때 JSON을 주고받음
  • 직접 fromJson, toJson 만들면 귀찮고 실수 가능성 큼

사용 예시


class Product with _$Product {
  const factory Product({
    required int id,
    required String name,
  }) = _Product;

  factory Product.fromJson(Map<String, dynamic> json) =>
      _$ProductFromJson(json);
}

코드 생성

flutter pub run build_runner build

이 명령어는 product.g.dart 파일을 생성합니다. 해당 파일이 생성되고 자동으로 JSON 변환이 가능해집니다.


6. Freezed & JsonSerializable 설치 방법

pubspec.yaml 파일에 의존성 추가

dependencies:
  flutter:
    sdk: flutter
  freezed_annotation: ^3.0.0

dev_dependencies:
  build_runner: ^2.4.15
  freezed: ^3.0.6
  json_serializable: ^6.9.5

의존성 설치

flutter pub get

7. 활용 방식

상태관리 (Bloc, Riverpod 등)

  • 상태 객체를 불변으로 만들면 변경 감지/비교가 쉬움
  • copyWith를 통해 일부 상태만 변경 가능 → 재사용 용이

Flutter UI 최적화

  • Widget.build()가 자주 호출되는 구조에서 객체가 바뀌지 않으면 성능 유지됨
  • 값이 바뀌었는지 쉽게 비교할 수 있어 변경된 UI만 다시 그릴 수 있음

8. 장단점 요약

장점단점
상태 관리, 테스트에 매우 유리초기 작성 시 다소 복잡할 수 있음
불변 객체로 인해 버그 감소코드 생성 툴 의존 (build_runner)
Freezed로 자동화 가능빌드 속도가 느려질 수 있음 (대규모일 때)

마무리 요약

  • 객체 불변성이란? → 생성 후 상태가 변하지 않는 객체

  • 왜 중요? → 예측 가능성, 안정성, 테스트 용이성

  • 어떻게 구현?

    • 기본 Dart → final
    • 활용 → freezed + json_serializable로 자동화

📚 참고 링크

profile
안녕하세요

0개의 댓글