Dart에서 클래스는 객체 지향 프로그래밍의 기본 단위로, 객체의 속성과 동작을 정의하는 역할을 한다. Dart에서 클래스는 class 키워드를 사용하여 정의된다. 클래스를 정의할 때는 주로 객체의 특성에 해당하는 필드(fields)와 객체가 수행할 수 있는 메소드(methods)를 포함시킨다. 또한, 생성자를 통해 객체의 초기 상태를 정의할 수 있다.

클래스 정의의 기본 구조

Dart에서 클래스의 기본 구조는 다음과 같다:

class ClassName {
  // 필드
  var fieldName;

  // 생성자
  ClassName(this.fieldName);

  // 메소드
  void methodName() {
    // 메소드의 내용
  }
}

필드(Field)

필드는 클래스 내에서 객체가 가질 수 있는 데이터를 저장하는 변수이다. 필드는 여러 데이터 타입을 가질 수 있으며, 일반적으로 클래스 내부에서 선언된다. 예를 들어, 자동차 클래스라면 필드로 color, model, speed와 같은 변수가 있을 수 있다. 필드는 객체의 상태를 나타낸다.

class Car {
  String color;  // 필드
  String model;  // 필드
  int speed;     // 필드
}

생성자(Constructor)

생성자는 클래스의 인스턴스가 생성될 때 호출되며, 주로 객체의 필드를 초기화하는 역할을 한다. Dart에서 생성자는 클래스 이름과 동일한 이름을 가지며, 클래스가 생성될 때 호출된다. Dart에서는 축약 생성자 문법을 제공하여 필드에 값을 쉽게 할당할 수 있다.

class Car {
  String color;
  String model;
  int speed;

  // 생성자
  Car(this.color, this.model, this.speed);
}

위 예시에서 Car 클래스는 세 개의 필드를 가지고 있으며, Car 클래스의 인스턴스를 생성할 때 필드 값을 초기화할 수 있다.

void main() {
  var myCar = Car('red', 'sedan', 100);
  print(myCar.color);  // 출력: red
}

기본 생성자

만약 클래스에 생성자를 명시적으로 정의하지 않으면, Dart는 기본 생성자를 제공한다. 기본 생성자는 인수 없이 호출되며, 객체를 생성할 때 필드를 초기화하지 않는다.

class Car {
  String color;
  String model;
  int speed;
}

void main() {
  var myCar = Car();
  print(myCar.color);  // null 출력
}

기본 생성자는 객체 생성 시 필드를 초기화하지 않기 때문에, 필드의 값은 기본적으로 null이 된다.

Named Constructor (명명된 생성자)

Dart에서는 클래스에서 여러 생성자를 정의할 수 있도록 명명된 생성자 기능을 제공한다. 명명된 생성자를 사용하면 생성자의 이름을 붙여서 다른 방식으로 객체를 생성할 수 있다.

class Car {
  String color;
  String model;
  int speed;

  // 기본 생성자
  Car(this.color, this.model, this.speed);

  // 명명된 생성자
  Car.empty() {
    color = 'white';
    model = 'sedan';
    speed = 0;
  }
}

명명된 생성자를 사용하면 특정한 조건에 맞게 객체를 생성할 수 있다.

void main() {
  var emptyCar = Car.empty();
  print(emptyCar.color);  // 출력: white
}

상수 생성자

만약 클래스의 인스턴스를 상수로 만들고 싶다면 상수 생성자를 사용할 수 있다. 상수 생성자는 const 키워드를 사용하며, 객체가 불변 상태일 때 유용하다.

class Point {
  final int x;
  final int y;

  const Point(this.x, this.y);
}

void main() {
  var p1 = const Point(1, 2);
  var p2 = const Point(1, 2);

  print(identical(p1, p2));  // true
}

상수 생성자를 통해 생성된 객체는 동일한 값의 객체끼리 메모리에서 같은 인스턴스를 공유한다. 이는 메모리 절약과 성능 향상에 기여할 수 있다.

초기화 리스트 (Initializer List)

Dart에서 생성자는 클래스의 필드를 초기화하는 방법을 제공하는데, 때로는 생성자의 본문을 실행하기 전에 필드를 초기화해야 할 때가 있다. 이럴 때 초기화 리스트(Initializer List)를 사용할 수 있다. 초기화 리스트는 생성자의 본문이 실행되기 전에 실행된다. 이를 통해 final 필드를 초기화하거나, 상위 클래스의 생성자를 호출하는 데 유용하다.

class Point {
  final int x;
  final int y;

  // 초기화 리스트를 사용하여 final 필드를 초기화
  Point(int x, int y) : x = x, y = y;
}

초기화 리스트를 사용하면 생성자의 본문에서 별도로 필드를 초기화하지 않아도 된다. 이는 final 필드와 같이 생성 후 변경할 수 없는 필드를 초기화할 때 유용하다.

class Rectangle {
  final int width;
  final int height;
  final int area;

  // 초기화 리스트에서 넓이를 계산
  Rectangle(int width, int height)
      : width = width,
        height = height,
        area = width * height;
}

상속에서 초기화 리스트 사용

초기화 리스트는 상속 관계에서도 유용하게 사용된다. 상위 클래스의 생성자를 호출하기 전에 자식 클래스의 필드를 초기화할 수 있다. 예를 들어, 다음과 같이 상위 클래스와 하위 클래스가 있을 때, 하위 클래스의 생성자가 호출되면 초기화 리스트를 통해 상위 클래스의 생성자를 호출할 수 있다.

class Shape {
  final String color;

  Shape(this.color);
}

class Circle extends Shape {
  final double radius;

  // 상위 클래스 생성자를 호출하는 초기화 리스트
  Circle(this.radius) : super('red');
}

위의 예에서 Circle 클래스는 Shape 클래스의 생성자를 초기화 리스트에서 호출하고 있다. 이는 상위 클래스가 먼저 초기화된 후 자식 클래스가 초기화됨을 보장한다.

팩토리 생성자 (Factory Constructor)

팩토리 생성자는 객체를 생성할 때 동일한 타입의 새로운 인스턴스를 생성하지 않고, 이미 존재하는 인스턴스를 반환하거나 다른 논리를 통해 객체를 생성하고자 할 때 사용된다. Dart에서 팩토리 생성자는 factory 키워드를 사용하여 정의된다. 팩토리 생성자는 주로 객체의 캐싱 또는 인스턴스의 유일성을 보장할 때 사용된다.

class Logger {
  static final Map<String, Logger> _cache = <String, Logger>{};
  final String name;

  // 팩토리 생성자
  factory Logger(String name) {
    if (_cache.containsKey(name)) {
      return _cache[name]!;
    } else {
      final logger = Logger._internal(name);
      _cache[name] = logger;
      return logger;
    }
  }

  Logger._internal(this.name);
}

위의 예에서 Logger 클래스는 팩토리 생성자를 사용하여 동일한 이름의 인스턴스가 존재할 경우 그 인스턴스를 반환하고, 존재하지 않으면 새로운 인스턴스를 생성하여 반환한다.

void main() {
  var logger1 = Logger('UI');
  var logger2 = Logger('UI');

  print(identical(logger1, logger2));  // true
}

팩토리 생성자를 통해 객체의 캐싱이 가능하며, 동일한 이름의 로거 인스턴스가 재사용된다.

클래스 정의 시 고려할 사항

클래스를 정의할 때, 몇 가지 고려해야 할 사항들이 있다.

final 필드

final 필드는 한 번 값이 할당되면 변경할 수 없는 필드이다. Dart에서는 이러한 필드를 초기화할 수 있는 기회를 생성자에서 제공한다. final 필드를 사용함으로써 불변성을 보장하고, 의도치 않은 값 변경을 방지할 수 있다.

class Employee {
  final String name;
  final int id;

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

정적 필드와 메소드

클래스의 정적 필드정적 메소드는 인스턴스에 종속되지 않고, 클래스 자체에 속하는 속성 및 메소드이다. static 키워드를 사용하여 정의되며, 이는 모든 인스턴스가 동일한 값을 공유하거나, 특정한 공통 동작을 구현할 때 사용된다.

class Employee {
  static int totalEmployees = 0;

  Employee() {
    totalEmployees++;
  }
}

void main() {
  var emp1 = Employee();
  var emp2 = Employee();

  print(Employee.totalEmployees);  // 출력: 2
}

정적 메소드도 비슷한 방식으로 동작하며, 인스턴스 없이 호출될 수 있다.

class MathUtils {
  static int square(int x) => x * x;
}

void main() {
  print(MathUtils.square(4));  // 출력: 16
}

정적 필드와 메소드는 클래스의 범용적인 동작을 정의할 때 유용하다.