iBetter Books
수정

지금까지 변수, 함수, 컬렉션을 배웠습니다. 데이터를 담고, 반복하고, 변환했죠. 그런데 현실 세계를 코드로 표현할 때 데이터와 행동을 따로 관리하면 점점 복잡해집니다. "학생"이라는 개념을 표현하려면 이름, 학번, 학점 같은 데이터와 수강신청, 성적조회 같은 행동이 함께 있어야 자연스럽습니다.

클래스(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의 생성자는 다른 언어보다 훨씬 다양하고 강력합니다.