이번에는 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, 연산자 오버로딩)에 대해서 알아보았다. 추가 적인 내용이 필요한 경우에는 댓글을 요청드리고, 틀린 부분이 있다면 이것 또한 댓글로 알려주시면 수정하도록 하겠습니다!
'IT > Dart' 카테고리의 다른 글
Dart_10일차: 파일 입출력 (File I/O) & 스트림 (Stream) 활용 (0) | 2025.03.09 |
---|---|
Dart_9일차 : 예외 처리 (Exception Handling) & Future Error Handling (0) | 2025.03.07 |
Dart_7일차 : 파일 입출력 & JSON 데이터 처리 (0) | 2025.03.05 |
Dart_6일차 : 고급 Stream 활용 (listen(), StreamController, Broadcast Stream) (2) | 2025.03.04 |
Dart_5일차 : 비동기 프로그래밍 (Future, async/await, Stream) (0) | 2025.02.21 |