지금까지 변수, 함수, 컬렉션을 배웠습니다. 데이터를 담고, 반복하고, 변환했죠. 그런데 현실 세계를 코드로 표현할 때 데이터와 행동을 따로 관리하면 점점 복잡해집니다. "학생"이라는 개념을 표현하려면 이름, 학번, 학점 같은 데이터와 수강신청, 성적조회 같은 행동이 함께 있어야 자연스럽습니다.
클래스(class)는 이 데이터와 행동을 하나로 묶는 틀입니다. 설계도라고 생각해도 좋습니다. 이 설계도로 찍어낸 실체를 인스턴스(instance)라고 부릅니다.
클래스 선언
가장 단순한 클래스부터 시작합니다.
// 파일: main.dart
class Student {
String name = '';
int studentId = 0;
double gpa = 0.0;
void introduce() {
print('안녕하세요, $name입니다. 학번은 $studentId입니다.');
}
}
void main() {
var student = Student();
student.name = '김다트';
student.studentId = 20240001;
student.gpa = 4.2;
student.introduce();
}
class Student { } 블록 안에 변수(필드)와 함수(메서드)를 넣었습니다. Student() 로 인스턴스를 만들 때 new 키워드는 생략할 수 있습니다. Dart 2부터는 new가 선택사항이고, 현대적인 코드에서는 거의 쓰지 않습니다.
출력 결과입니다.
안녕하세요, 김다트입니다. 학번은 20240001입니다.
this 키워드
메서드 안에서 현재 인스턴스를 가리킬 때 this를 씁니다. 필드명과 매개변수명이 겹칠 때 특히 유용합니다.
// 파일: main.dart
class Student {
String name = '';
int studentId = 0;
void setName(String name) {
this.name = name; // this.name은 필드, name은 매개변수
}
void introduce() {
print('$this 입니다.');
}
@override
String toString() {
return 'Student($name, $studentId)';
}
}
void main() {
var s = Student();
s.setName('이플러터');
s.studentId = 20240002;
s.introduce();
}
toString()을 오버라이드하면 print(객체)나 문자열 보간에서 원하는 형태로 출력됩니다. $this를 쓰면 내부적으로 this.toString()이 호출됩니다.
접근 제어 — _private
Dart에는 private, public 키워드가 없습니다. 대신 이름 앞에 밑줄(_)을 붙이면 해당 파일 내에서만 접근 가능한 private 멤버가 됩니다.
// 파일: main.dart
class BankAccount {
String owner;
double _balance; // private 필드
BankAccount(this.owner, this._balance);
void deposit(double amount) {
if (amount <= 0) {
print('입금액은 0보다 커야 합니다.');
return;
}
_balance += amount;
print('$amount원 입금. 잔액: $_balance원');
}
void withdraw(double amount) {
if (amount > _balance) {
print('잔액이 부족합니다.');
return;
}
_balance -= amount;
print('$amount원 출금. 잔액: $_balance원');
}
double get balance => _balance; // getter로만 읽기 허용
}
void main() {
var account = BankAccount('홍길동', 100000);
account.deposit(50000);
account.withdraw(30000);
print('잔액: ${account.balance}원');
// account._balance = 999999; // 같은 파일이면 접근 가능, 다른 파일이면 오류
}
중요한 점이 있습니다. Dart의 _는 라이브러리(파일) 단위 private입니다. 자바나 코틀린처럼 클래스 단위가 아닙니다. 같은 파일 안이라면 _balance에 직접 접근할 수 있습니다. 실제 프로젝트에서는 파일을 분리해서 캡슐화를 달성합니다.
getter와 setter
필드를 외부에서 읽거나 쓸 때 로직을 끼워 넣고 싶다면 getter와 setter를 씁니다.
// 파일: main.dart
class Circle {
double _radius;
Circle(this._radius);
// getter: 반지름 읽기
double get radius => _radius;
// setter: 반지름 설정 (유효성 검사 포함)
set radius(double value) {
if (value < 0) throw ArgumentError('반지름은 음수일 수 없습니다.');
_radius = value;
}
// 계산 getter: 넓이
double get area => 3.14159 * _radius * _radius;
// 계산 getter: 지름
double get diameter => _radius * 2;
}
void main() {
var c = Circle(5.0);
print('반지름: ${c.radius}'); // getter 호출
print('넓이: ${c.area}'); // 계산 getter
print('지름: ${c.diameter}');
c.radius = 10.0; // setter 호출
print('변경된 반지름: ${c.radius}');
try {
c.radius = -1; // 예외 발생
} catch (e) {
print('오류: $e');
}
}
getter는 get 이름 => 표현식 또는 get 이름 { return ...; } 형태로 씁니다. setter는 반드시 매개변수 하나를 받습니다. 외부에서 보면 일반 필드처럼 c.radius, c.radius = 10.0 으로 사용하지만, 내부에서는 메서드가 실행됩니다.
클래스를 여러 개 만들어보기
이제 조금 더 실용적인 예제를 만들어봅니다. 온도를 표현하는 클래스입니다.
// 파일: main.dart
class Temperature {
double _celsius;
Temperature.celsius(this._celsius);
Temperature.fahrenheit(double fahrenheit)
: _celsius = (fahrenheit - 32) * 5 / 9;
Temperature.kelvin(double kelvin)
: _celsius = kelvin - 273.15;
double get celsius => _celsius;
double get fahrenheit => _celsius * 9 / 5 + 32;
double get kelvin => _celsius + 273.15;
bool get isBoiling => _celsius >= 100;
bool get isFreezing => _celsius <= 0;
@override
String toString() {
return '${_celsius.toStringAsFixed(1)}°C';
}
}
void main() {
var boiling = Temperature.celsius(100);
print(boiling); // 100.0°C
print('화씨: ${boiling.fahrenheit}'); // 212.0
print('끓는점?: ${boiling.isBoiling}'); // true
var bodyTemp = Temperature.fahrenheit(98.6);
print('체온: $bodyTemp'); // 37.0°C
var absolute = Temperature.kelvin(0);
print('절대영도: $absolute'); // -273.1°C
}
이 예제에서 이름 있는 생성자(Temperature.celsius, Temperature.fahrenheit)를 미리 맛봤습니다. 다음 챕터에서 생성자를 본격적으로 다룹니다.
정리
클래스는 데이터(필드)와 행동(메서드)을 하나로 묶는 설계도입니다. 인스턴스는 그 설계도로 만든 실체입니다. _로 시작하는 이름은 파일 내 private이고, getter/setter로 필드 접근에 로직을 추가할 수 있습니다.
다음 챕터에서는 인스턴스를 만드는 과정인 생성자(constructor)를 깊이 탐구합니다. Dart의 생성자는 다른 언어보다 훨씬 다양하고 강력합니다.