소소한 일상과 잡다한 정보

IT/Dart

Dart_8일차 : 클래스 심화 (생성자, Getter/Setter, 연산자 오버로딩)

pandada 2025. 3. 6. 16:58
반응형

 

이번에는 Dart의 클래스 개념을 더 깊이 있게 이해하는 시간을 가져보자.

  • 생성자(Constructor) 활용법
  • Getter/Setter를 통한 캡슐화
  • 연산자 오버로딩 (Operator Overloading)

위 사항을 좀 더 공부를 해보면 객체 지향 프로그래밍을 더 강력하게 활용할 수 있을 것이다.

 

 


 

1. 생성자 (Constructor)

 클래스의 생성자는 객체가 생성될 때 자동으로 실행되는 함수다.
 Dart에서는 기본 생성자 외에도 여러 가지 생성자를 활용할 수 있다.

 ✔️ 기본 생성자

class Car {
  String brand;
  int year;

  // 기본 생성자
  Car(this.brand, this.year);

  void display() {
    print("🚗 브랜드: $brand, 출시 연도: $year");
  }
}

void main() {
  Car myCar = Car("KGM", 2021);
  myCar.display();
}
  • this.brand, this.year를 사용하면 생성자에서 직접 변수 초기화 가능
  • Car myCar = Car("KGM", 2021); 실행 시 자동으로 Car 객체 생성

 

 ✔️ 이름이 있는 생성자 (Named Constructor)

class Car {
  String brand;
  int year;

  // 기본 생성자
  Car(this.brand, this.year);

  // 이름이 있는 생성자 (디폴트 값 설정 가능)
  Car.defaultCar() : brand = "Unknown", year = 2000;

  void display() {
    print("🚗 브랜드: $brand, 출시 연도: $year");
  }
}

void main() {
  Car car1 = Car("Kia", 2014);
  Car car2 = Car.defaultCar(); // 기본값으로 객체 생성

  car1.display();
  car2.display();
}
  • Car.defaultCar()를 호출하면 기본값("Unknown", 2000)으로 초기화
  • 여러 개의 생성자를 만들 수 있어서 코드의 유연성이 증가

 

2.  Getter & Setter (캡슐화)

Dart에서 getter와 setter를 사용하면 클래스 내부의 변수에 대한 접근을 제한하고, 원하는 방식으로 데이터 조작이 가능.

 ✔️ Getter & Setter 예제

class Person {
  String _name = ""; // private 변수 (언더스코어 `_` 사용)
  int _age = 0;

  // Getter (값을 읽을 때 사용)
  String get name => _name;
  int get age => _age;

  // Setter (값을 설정할 때 사용, 유효성 검사 가능)
  set name(String newName) {
    if (newName.isNotEmpty) {
      _name = newName;
    } else {
      print("⚠️ 이름은 비워둘 수 없습니다.");
    }
  }

  set age(int newAge) {
    if (newAge > 0) {
      _age = newAge;
    } else {
      print("⚠️ 나이는 0보다 커야 합니다.");
    }
  }
}

void main() {
  Person p = Person();
  p.name = "Kim"; // Setter 호출
  p.age = 25;      // Setter 호출

  print("👤 이름: ${p.name}, 🎂 나이: ${p.age}"); // Getter 호출
}
  • Getter는 변수명 => 값 형태로 정의 ( get name => _name; )
  • Setter는 set 변수명(값) 형태로 정의 ( set name(String newName) {...} )
  • Setter 내부에서 유효성 검사를 추가할 수도 있음

3. 연산자 오버로딩 (Operator Overloading)

Dart에서는 +, -, *, == 같은 연산자를 직접 정의할 수 있다. 이를 **연산자 오버로딩 (Operator Overloading)**이라고 한다.

 ✔️ + 연산자 오버로딩 예제

class Point {
  int x, y;

  Point(this.x, this.y);

  // + 연산자 오버로딩
  Point operator +(Point other) {
    return Point(x + other.x, y + other.y);
  }

  void display() {
    print("📍 좌표: ($x, $y)");
  }
}

void main() {
  Point p1 = Point(3, 4);
  Point p2 = Point(1, 2);

  Point result = p1 + p2; // 연산자 오버로딩 사용

  p1.display();
  p2.display();
  result.display(); // (3+1, 4+2) => (4,6)
}
  • operator +를 오버로딩하여 Point + Point 연산을 가능하게 만듦
  • 객체 간 연산이 가능해져서 코드가 더 직관적이 됨

 

 ✔️ == 연산자 오버로딩 예제

class Person {
  String name;
  int age;

  Person(this.name, this.age);

  // == 연산자 오버로딩 (두 객체가 같은지 비교)
  @override
  bool operator ==(Object other) {
    if (other is Person) {
      return name == other.name && age == other.age;
    }
    return false;
  }

  @override
  int get hashCode => name.hashCode ^ age.hashCode;
}

void main() {
  Person p1 = Person("Kim", 25);
  Person p2 = Person("Kim", 25);
  Person p3 = Person("Lee", 30);

  print(p1 == p2); // true (이름과 나이가 같음)
  print(p1 == p3); // false (이름이 다름)
}
  • == 연산자를 오버로딩하여 객체 비교 가능
  • hashCode를 재정의하면 Set에서도 중복 제거 가능

 

📌 hashCode를 재정의하는 이유

Dart에서 == 연산자를 오버로딩할 때 hashCode를 반드시 함께 재정의해야 하는 이유객체의 동등성(equality) 비교를 올바르게 처리하기 위해서다.

 

✅ 1. hashCode란?

  • 객체의 고유한 정수 값(해시 코드)을 반환하는 역할
  • Set, Map 같은 컬렉션에서 객체의 중복 여부를 판별할 때 사용
  • 같은 값을 가진 객체는 같은 hashCode를 가져야 함

 

✅ 2. == 연산자를 재정의하면 hashCode도 재정의해야 하는 이유

✔️ hashCode를 재정의하지 않은 경우

class Person {
  String name;
  int age;

  Person(this.name, this.age);

  @override
  bool operator ==(Object other) {
    if (other is Person) {
      return name == other.name && age == other.age;
    }
    return false;
  }
}

void main() {
  Person p1 = Person("Kim", 25);
  Person p2 = Person("Kim", 25);

  print(p1 == p2); // true (이름과 나이가 같음)

  Set<Person> people = {p1, p2};
  print(people.length); // 2 (중복이 제거되지 않음 ❌)
}
  • p1과 p2는 == 연산자로 비교하면 같지만, Set에서는 다른 객체로 인식
  • 왜냐하면 hashCode를 재정의하지 않았기 때문이다.

 

✔️ hashCode를 재정의한 경우

class Person {
  String name;
  int age;

  Person(this.name, this.age);

  @override
  bool operator ==(Object other) {
    if (other is Person) {
      return name == other.name && age == other.age;
    }
    return false;
  }

  @override
  int get hashCode => name.hashCode ^ age.hashCode;
}

void main() {
  Person p1 = Person("Kim", 25);
  Person p2 = Person("Kim", 25);

  print(p1 == p2); // true (이름과 나이가 같음)

  Set<Person> people = {p1, p2};
  print(people.length); // 1 (중복 제거됨 ✅)
}
  • 이제 p1과 p2가 같은 hashCode를 가지므로 Set에서 중복이 제거됨
  • 이제 Set, Map 같은 컬렉션에서 객체를 올바르게 비교 가능


✔️ hashCode를 정의할 때 ^( XOR ) 연산자를 사용하는 이유

  @override
  int get hashCode => name.hashCode ^ age.hashCode;
  • XOR(^) 연산자는 서로 다른 값을 합쳐 새로운 해시 값을 만들기 위해 사용됨
  • 이렇게 하면 두 속성(name, age)의 값을 조합하여 고유한 해시 코드를 생성할 수 있음
  • 즉, 같은 name과 age를 가진 객체라면 같은 hashCode를 갖도록 보장함

 

📌 결론

== 연산자를 재정의할 때 hashCode도 함께 재정의해야 컬렉션(Set, Map)에서 올바르게 동작함
같은 값의 객체는 같은 hashCode를 가져야 함
^ 연산자를 사용하여 여러 속성을 조합한 고유한 해시 값을 생성

hashCode객체의 동등성(equality)을 비교할 때만 사용됨.

 

연산자 hashCode 재정의 필요 여부 설명
== (동등 비교) ✅ 필수 객체가 같은지 판단하는 기준이므로 hashCode가 필요
!= (다름 비교) ❌ 불필요 == 연산자의 반대값을 자동으로 사용
+, -, *, / (연산자 오버로딩) ❌ 불필요 객체를 조작할 뿐, 동등성을 비교하지 않음
Set, Map에 객체를 저장할 때 ✅ 필수 같은 객체를 중복 없이 저장하려면 hashCode 필요
  • ==을 재정의할 때는 hashCode도 재정의해야 하고, +, -, *, / 같은 연산자 오버로딩에는 필요하지 않음.
  • 우리는 hashCode를 직접 호출하지 않지만, 컬렉션(Set, Map)에서는 자동으로 사용됨
  • ==을 오버라이딩하면 hashCode도 자동으로 활용되므로, 정의 순서는 상관없음
  • 가독성을 위해 ==을 먼저 정의하는 것이 일반적
반응형

✔️ 테스트 예제 코드 1

 위에서 진행했던 내용을 토대로 충분한 실습을 진행한 후, 테스트 예제를 진행해보도록 하자.

 1. Rectangle 클래스를 만들어 + 연산자를 오버로딩하여 두 사각형의 면적을 합산하는 기능 구현

 2. Rectangle 클래스 생성

 3. 가로(width), 세로(height) 속성 추가

 4. 면적(area)을 계산하는 메서드 추가

 5. + 연산자를 오버로딩하여 두 사각형의 면적을 합산하는 기능 구현

class Rectangle {
  double width;
  double height;

  Rectangle(this.width, this.height);

  // 면적 계산 메서드
  double get area => width * height;

  // + 연산자 오버로딩 (두 사각형의 면적을 더하는 기능)
  Rectangle operator +(Rectangle other) {
    return Rectangle(width + other.width, height + other.height);
  }

  void display() {
    print("📏 가로: $width, 세로: $height, 면적: ${area}");
  }
}

void main() {
  Rectangle rect1 = Rectangle(4, 5); // 4x5 = 20
  Rectangle rect2 = Rectangle(3, 2); // 3x2 = 6

  Rectangle result = rect1 + rect2; // 연산자 오버로딩 사용

  rect1.display();
  rect2.display();
  result.display(); // (4+3, 5+2) = (7x7) = 49
}

 

 

✔️ 테스트 예제 코드 2

 1. Student 클래스를 만들고, == 연산자를 오버로딩하여 학번(studentID)이 같으면 동일한 학생으로 판별하는 기능 구현

 2. Student 클래스 생성

 3. 이름(name), 학번(studentID), 학년(grade) 속성 추가

 4. == 연산자 오버로딩하여 학번이 같으면 동일한 학생으로 판별

 5. hashCode 재정의하여 Set<Student>에서 중복 제거 가능하도록 처리

class Student {
  String name;
  int studentID;
  int grade;

  Student(this.name, this.studentID, this.grade);

  // == 연산자 오버로딩 (학번이 같으면 같은 학생으로 간주)
  @override
  bool operator ==(Object other) {
    if (other is Student) {
      return studentID == other.studentID;
    }
    return false;
  }

  // hashCode 재정의 (같은 학번이면 같은 해시값 반환)
  @override
  int get hashCode => studentID.hashCode;

  void display() {
    print("🎓 이름: $name, 학번: $studentID, 학년: $grade학년");
  }
}

void main() {
  Student s1 = Student("Kim", 1001, 2);
  Student s2 = Student("Lee", 1002, 3);
  Student s3 = Student("Park", 1001, 4); // 학번이 s1과 동일함

  print(s1 == s2); // false (학번 다름)
  print(s1 == s3); // true (학번 같음)

  // Set을 사용하여 중복 학생 제거
  Set<Student> students = {s1, s2, s3};
  print("\n📌 등록된 학생 목록:");
  for (var student in students) {
    student.display();
  }
}
  • operator ==를 오버로딩하여 학번( studentID )이 같으면 같은 객체로 판별
  • hashCode를 학번(studentID)의 해시값으로 설정하여 Set에서 중복 제거 가능

 

✔️ 테스트 예제 코드 3

 1. 테스트 예제 코드 2를 바탕으로 7일차에서 진행했던 json 까지 활용해보기.

 2. 이름과 학번이 같으면 동일한 학생으로 인식하도록 == 및 hashCode 수정

 3. 학생 데이터를 JSON 파일로 저장 및 불러오는 기능 추가

 4. Set<Student>을 사용하여 중복 제거 자동화

 5. 파일이 존재하지 않으면 자동으로 빈 리스트 반환 (예외 처리 추가)

 6. 난이도가 있는 부분으로 테스트 예제 코드 3은 스킵해도 될 것 같다... 힘들다면...

import 'dart:io';
import 'dart:convert';

class Student {
  String name;
  int studentID;
  int grade;

  Student(this.name, this.studentID, this.grade);

  // == 연산자 오버로딩 (학번과 이름이 같으면 같은 학생으로 간주)
  @override
  bool operator ==(Object other) {
    if (other is Student) {
      return studentID == other.studentID && name == other.name;
    }
    return false;
  }

  // hashCode 재정의 (학번과 이름을 기준으로 해시값 생성)
  @override
  int get hashCode => studentID.hashCode ^ name.hashCode;

  // 학생 정보를 JSON 형태로 변환
  Map<String, dynamic> toJson() {
    return {
      'name': name,
      'studentID': studentID,
      'grade': grade,
    };
  }

  // JSON 데이터를 Student 객체로 변환하는 factory 생성자
  factory Student.fromJson(Map<String, dynamic> json) {
    return Student(json['name'], json['studentID'], json['grade']);
  }

  void display() {
    print("🎓 이름: $name, 학번: $studentID, 학년: $grade학년");
  }
}

// JSON 파일 저장 함수
Future<void> saveStudentsToFile(Set<Student> students, String filePath) async {
  List<Map<String, dynamic>> studentList =
      students.map((student) => student.toJson()).toList();
  String jsonString = jsonEncode(studentList);
  await File(filePath).writeAsString(jsonString);
  print("✅ 학생 정보가 JSON 파일로 저장되었습니다! ($filePath)");
}

// JSON 파일에서 학생 정보 불러오기 함수
Future<Set<Student>> loadStudentsFromFile(String filePath) async {
  File file = File(filePath);
  if (!await file.exists()) {
    print("⚠️ 저장된 학생 정보가 없습니다.");
    return {};
  }

  String jsonString = await file.readAsString();
  List<dynamic> studentList = jsonDecode(jsonString);
  return studentList.map((json) => Student.fromJson(json)).toSet();
}

void main() async {
  String filePath = 'students.json';

  // 기존 학생 데이터 불러오기
  Set<Student> students = await loadStudentsFromFile(filePath);

  // 새로운 학생 정보 추가
  students.add(Student("Kim", 1001, 2));
  students.add(Student("Kim", 1001, 3)); // 같은 학번, 다른 학년 (중복 아님)
  students.add(Student("Lee", 1002, 4));
  students.add(Student("Kim", 1001, 2)); // 같은 학생 (중복 제거됨)

  // JSON 파일로 저장
  await saveStudentsToFile(students, filePath);

  // 저장된 학생 정보 다시 불러오기
  Set<Student> loadedStudents = await loadStudentsFromFile(filePath);

  // 출력
  print("\n📌 JSON 파일에서 불러온 학생 목록:");
  for (var student in loadedStudents) {
    student.display();
  }
}
  • json 파일이 없음으로 중복 제거한 json 파일을 저장하는 프로그램

 


 이렇게 클래스 심화 (생성자, Getter/Setter, 연산자 오버로딩)에 대해서 알아보았다. 추가 적인 내용이 필요한 경우에는 댓글을 요청드리고, 틀린 부분이 있다면 이것 또한 댓글로 알려주시면 수정하도록 하겠습니다!


 

반응형