클래스 class

클래스와 객체란 무엇인가

클래스는 사물을 분류하고 추상화하는 개념입니다. 주변 현실의 대상을 하나의 타입으로 묶어 정의하는데, 전 세계 모든 사람들을 ‘사람’이라는 클래스로 추상화한다고 생각하면 쉽게 이해할 수 있습니다. Bob, Tom처럼 구체적인 개개인이 바로 객체가 됩니다.

클래스는 ‘설계도’ 혹은 ‘템플릿’과 동일한 역할로, 객체가 갖춰야 할 구조와 행동 규칙을 미리 정의해줍니다.

앞서 예시로 든 사람 클래스를 기준으로 설명하겠습니다. 사람 클래스 자체가 템플릿으로, 사람은 이름, 손발 등 신체 기관을 갖고 말하고 사고하는 행동을 할 수 있습니다. 이름, 나이 같은 정보는 클래스의 속성 특징이고, 말하기, 사고하기 같은 동작은 클래스의 행동에 해당합니다. 즉 클래스는 현실 사물에서 공통적인 틀만 추출한 것이며, 이 클래스를 기반으로 Jack, Jim 같은 독립적인 객체를 여러 개 만들 수 있습니다.

위에서 설명한 클래스의 속성(이름, 나이)과 행동(말하기, 사고하기)은 코드 상에서 변수와 함수로 표현합니다.

클래스 안에는 객체가 반드시 보유해야 할 속성(변수)메서드(함수)를 명시합니다.

클래스는 빵을 만들 때 쓰는 틀에 비유할 수 있습니다. 틀 자체는 실제 빵이 아니며, 하나의 틀로 여러 개의 실물 빵을 찍어내듯 클래스로 다수의 객체를 생성할 수 있습니다.

프로그래밍 발전 초기에는 클래스 개념 자체가 없었습니다. C 언어가 대표적인 예로 클래스 없이 함수, 변수, 각종 구문만 사용하는 절차지향 방식이었죠. 소프트웨어 기능이 점점 풍부해지면서 절차지향 코드는 중복 로직이 과도하게 많고 관리가 어려우며 계층 구조가 흐릿한 단점이 두드러졌습니다. 이후 개발자들은 현실 세상과 동일하게 로직을 개별 객체로 추상화하는 객체지향 패러다임을 고안했습니다. 회사를 예로 들면 초기에는 사장이 모든 업무를 직접 처리하지만 직원을 채용한 뒤 회계 업무는 재무팀, 인력 채용은 인사팀에 분담하면 업무 흐름이 훨씬 체계적이고 깔끔해지는 원리와 같습니다.

객체는 클래스의 인스턴스(Instance)입니다.

new 키워드나 생성자를 호출하면 해당 클래스로부터 하나의 객체가 생성됩니다.

객체는 프로그램 실행 시 실제로 존재하는 실체로 데이터를 저장하고 메서드를 호출할 수 있습니다.

클래스와 객체의 관계

  • 클래스는 설계도이고, 객체는 설계도로 완공된 실제 건물입니다.
  • 하나의 클래스로 여러 객체를 생성할 수 있으며, 각 객체는 서로 독립적인 데이터를 보관합니다.
  • 동일 클래스로 만들어진 모든 객체는 클래스에 정의된 행동(메서드)을 공유해서 사용합니다.

이제 다트(Dart) 언어로 넘어가 알아보겠습니다.

아래 내용들을 순서대로 학습할 예정입니다.

다트는 완전 객체지향 언어로 모든 값은 클래스의 인스턴스입니다. Null을 제외한 모든 클래스는 기본 상위 클래스인 Object를 상속받습니다.

다트는 클래스와 믹스인(Mixin)을 결합하는 방식으로 상속 기능을 구현합니다.

단일 상속 규칙: 모든 클래스는 부모 클래스를 단 하나만 가질 수 있지만, 믹스인을 통해 여러 클래스의 로직을 재사용할 수 있습니다.

확장 메서드: 기존 클래스 코드를 수정하거나 하위 클래스를 새로 만들지 않아도 클래스에 새로운 기능을 추가할 수 있습니다.

클래스 수식자: 다른 라이브러리에서 해당 클래스를 상속하거나 구현할 수 있는지 권한을 제어합니다.

지금부터 다트의 클래스 문법을 단계별로 공부해보겠습니다.

클래스 멤버

클래스 멤버는 메서드와 변수를 통칭하는 용어로, 클래스 내부를 구성하는 모든 요소를 의미합니다.

. 연산자를 사용해 인스턴스 멤버에 접근하고, ?.는 널 안전 접근 연산자로 널 포인터 예외 발생을 막아줍니다.

아래 완성된 예시는 클래스 정의, 클래스로 객체 인스턴스화, 객체 변수명으로 멤버를 호출하는 전 과정을 보여줍니다.

// 클래스를 선언하는 것은 틀을 제작하는 것과 같음 
class Point {
  double x, y;
  Point(this.x, this.y);

  // 인스턴스 메서드: 두 점 사이 거리 제곱 계산
  double distanceTo(Point other) {
    var dx = x - other.x;
    var dy = y - other.y;
    return dx * dx + dy * dy;
  }
}

void main() {
  // Point 클래스로 객체 p를 생성, 객체를 통해 각종 연산 진행
  var p = Point(2, 2);

  // 인스턴스 변수 읽기 (암시적 getter)
  print(p.y); //2.0

  // 인스턴스 메서드 호출
  double dist = p.distanceTo(Point(4, 4));
  print(dist); // 8.0

  // ?. 널 안전 접근
  Point? nullablePoint;
  var a = nullablePoint?.y; // 객체가 null일 경우 a에 null이 할당되고 예외가 발생하지 않음
  print(a); // null
}Code language: JavaScript (javascript)

생성자(Constructors)

일반 생성자

다트는 다른 C계열 언어와 차이점이 존재하는데 명명 생성자를 지원합니다. 다른 C계열 언어는 클래스명과 동일한 이름만 생성자로 사용할 수 있지만 다트는 클래스명과 별개의 식별자를 생성자로 정의할 수 있습니다.

문법 형식: 클래스명() / 클래스명.식별자();

new 키워드는 생략 가능 (다트 2 버전부터 지원)

class Point {
  double x, y; // 두 변수는 클래스 멤버로 클래스 속성 저장용 

  // 기본 생성자, 위 두 변수 x y 초기화
  Point(this.x, this.y);

  // 명명 생성자 Point.fromJson: json 데이터로 x y 초기화하는 생성자
  Point.fromJson(Map<String, double> json)
      : x = json['x']!,
        y = json['y']!;

}

void main() {
  var p1 = Point(2, 2);//기본 생성자 호출, 2,2를 전달해 x=2, y=2로 초기화

  var p2 = Point.fromJson({'x': 1, 'y': 2}); //fromJson 명명 생성자에 json 전달

  // new를 적거나 생략해도 동작 결과가 완전히 동일함
  var p3 = new Point(3, 3); //기본 생성자로 객체 생성

  print("p1:(${p1.x},${p1.y}), p2:(${p2.x},${p2.y}), p3:(${p3.x},${p3.y})");
  //출력: p1:(2.0,2.0), p2:(1.0,2.0), p3:(3.0,3.0)
}Code language: JavaScript (javascript)

상수 생성자 const

생성자 앞에 const를 붙이면 컴파일 타임 상수 객체가 생성되며, 매개변수가 완전히 동일한 상수는 동일한 인스턴스를 재사용합니다.

class ImmutablePoint {
  final double x, y;
  // 상수 생성자 규칙: 모든 멤버 변수에 final을 붙여야 함, const 수식자 사용 시 x,y는 필수로 final
  const ImmutablePoint(this.x, this.y);
}

void main() {
  // 상수 인스턴스 생성
  var a = const ImmutablePoint(1, 1); // 상수 객체 생성 시 const 키워드 필수 
  var b = const ImmutablePoint(1, 1);
  print(identical(a, b)); // true, 동일한 객체 참조

  var m = const ImmutablePoint(1, 1); 
  var n = const ImmutablePoint(2, 1);
  print(identical(m, n)); // false, x값이 다르면 별개의 객체

  // const가 없으면 일반 객체로 인스턴스를 공유하지 않음
  var c = ImmutablePoint(1, 1);
  print(identical(a, c)); //false

  // const 컨텍스트 내부에서는 안쪽 const를 생략 가능
  const pointMap = {
    'p1': [ImmutablePoint(0, 0)],
    'p2': [ImmutablePoint(1, 10), ImmutablePoint(-2, 11)]
  };

  print(pointMap); //{p1: [Instance of 'ImmutablePoint'], p2: [Instance of 'ImmutablePoint', Instance of 'ImmutablePoint']}

  print(identical(p1, p2)); // false
  print(identical(p2, p3)); // false
  print(identical(p1, p3)); // false
}Code language: PHP (php)
상수 생성자는 전달하는 매개변수가 전부 일치할 때만 같은 인스턴스를 재사용하며, 어느 한 매개변수라도 다르면 독립적인 객체가 생성됩니다.

객체 런타임 타입 가져오기 runtimeType

객체.runtimeType으로 실행 시 타입 정보를 얻을 수 있지만, runtimeType으로 비교하기보다 타입 판단 연산자 is 사용을 권장합니다.

class Point {
  double x, y;
  Point(this.x, this.y);

  // 인스턴스 메서드: 두 점 거리 제곱 계산
  double distanceTo(Point other) {
    var dx = x - other.x;
    var dy = y - other.y;
    return dx * dx + dy * dy;
  }
}

void main() {
  var p = Point(2, 3);
  print("p의 타입: ${p.runtimeType}"); // Point

  // 권장하는 안정적인 타입 검사 방식
  if (p is Point) {
    print("p는 Point 타입입니다");
  }

  // 실서비스 코드에서 사용하지 않는 것을 권장
  if (p.runtimeType == Point) {
    print("p는 Point 타입입니다 ");
  }
}Code language: JavaScript (javascript)

인스턴스 변수 Instance Variables

  1. 널 가능 타입을 초기화하지 않으면 기본값 null이고, 널 불가 타입은 반드시 초기화해야 함
  2. 모든 인스턴스 변수는 암시적 getter가 자동 생성되고, final이 아니거나 초기화되지 않은 late final 변수는 자동으로 setter가 생성됨
  3. 일반 인스턴스 변수 초기화 구문에서는 this에 접근할 수 없으며, late 변수 초기화 시에는 this 사용이 허용됨
  4. final 인스턴스 변수는 단 한 번만 값을 할당할 수 있으며, 선언 시·생성자 매개변수·초기화 리스트 중 한 곳에서 설정해야 함

암시적 get/set

class Point {
  double? x; // 초기값 null, getter + setter 자동 생성
  double? y;
  double z = 0; // 기본 초기값 0
}

void main() {
  var point = Point();
  point.x = 4; // 암시적 setter 호출
  assert(point.x == 4); // 암시적 getter 호출
  assert(point.y == null);
  print(point.z); // 0
}
Code language: JavaScript (javascript)

초기화 구문의 this 접근 제한

double globalX = 1.5;
class Point {
  // 유효: 외부 전역 변수 참조, this에 의존하지 않음
  double? x = globalX;

  // 컴파일 오류: 일반 초기화 구문에서 this 사용 불가
  // double? y = this.x;

  // 유효: late 변수 초기화에서 this 참조 가능
  late double? z = this.x;

  // 유효: 생성자 매개변수 문법 sugar인 this.x는 초기화 구문이 아님
  Point(this.x, this.y);
  double? y;
}

void main() {
  var p = Point(null, 5);
  print(p.z); // null
}
Code language: JavaScript (javascript)

final 인스턴스 변수 세 가지 초기화 방식

class ProfileMark {
  // 방식1: 변수 선언과 동시에 초기화
  final DateTime start = DateTime.now();
  final String name;

  // 방식2: 생성자 매개변수로 값 할당
  ProfileMark(this.name);

  // 방식3: 생성자 초기화 리스트로 할당, unnamed 명명 생성자는 name에 빈 문자열 기본 할당
  ProfileMark.unnamed() : name = '';
}

void main() {
  var m1 = ProfileMark("user1");
  var m2 = ProfileMark.unnamed();
  print(m1.name);
  print(m2.name);
}
Code language: PHP (php)

암시적 인터페이스 Implicit interfaces

다트의 모든 클래스는 자체적인 암시적 인터페이스를 갖고 있으며, 클래스 내 모든 공개 인스턴스 멤버를 포함합니다.

  • implements: 인터페이스를 구현하며, 인터페이스의 모든 메서드를 전부 재정의해야 함
  • extends: 상속으로 부모 클래스의 구현 로직을 재사용 가능
  • 밑줄로 시작하는 비공개 변수 _xxx는 인터페이스에 노출되지 않음

1 단일 인터페이스 구현

class Person {
  final String _name;
  Person(this._name);
  // 인터페이스 공개 메서드
  String greet(String who) =&gt; "Hello $who, I'm $_name";
}

// Person 인터페이스를 구현하므로 모든 인터페이스 멤버 재정의 필수
class Impostor implements Person {
  String get _name =&gt; "";
  @override
  String greet(String who) =&gt; "Hi $who, guess who?";
}

String greetBob(Person p) =&gt; p.greet("Bob");

void main() {
  print(greetBob(Person("Kathy")));
  print(greetBob(Impostor()));
}
Code language: PHP (php)

하위 클래스에서 상위 인터페이스의 메서드를 재정의할 때 @override 어노테이션을 붙이며, 자바와 문법이 유사하고 implements 키워드로 인터페이스를 구현합니다.

2 여러 개의 인터페이스 구현

//추상 클래스
abstract class Comparable {
  int compareTo(Object other);
}

//추상 클래스
abstract class Location {
  double get x;
  double get y;
}

//두 개의 인터페이스를 동시 구현
class Point implements Comparable, Location {

  @override
  double x, y;

  Point(this.x, this.y);

  @override
  int compareTo(Object other) {
    if (other is Point) {
      return (x + y).compareTo(other.x + other.y);
    }
    return -1;
  }
}

void main() {
  var p = Point(2, 3);
  print(p.x);
  print(p.compareTo(Point(1, 1)));
}
Code language: PHP (php)

@override
double x, y;

이 어노테이션은 두 속성이 상위 인터페이스 Location에 정의된 추상 getter를 구현한다는 의미입니다.

클래스 정적 변수 & 정적 메서드 static

1. 정적 변수 static (클래스 변수, 전역 단일 인스턴스)

  • 객체 인스턴스가 아닌 클래스 자체에 속하며, 지연 초기화로 최초 접근 시 메모리에 생성됨
  • 상수 정적 변수는 소문자 카멜케이스로 명명하는 것을 권장
class Queue {
  static const initialCapacity = 16; //정적 상수 변수
  static int count = 0; //정적 변수
  Queue() {
    count++;
  }
}

void main() {
  assert(Queue.initialCapacity == 16); //접근하는 순간 초기화, 기본값 16
  Queue(); //count 값 1 증가
  Queue();
  print(Queue.count); // 2
}
Code language: JavaScript (javascript)

2. 정적 메서드 static (클래스 메서드)

  • this를 사용할 수 없고 인스턴스 멤버에 접근 불가, 오직 정적 변수만 참조 가능
  • 클래스명.메서드() 형식으로 직접 호출
  • 컴파일 타임 상수로 취급되어 상수 생성자의 매개변수로 전달 가능
import 'dart:math';

class Point {
  double x, y;
  Point(this.x, this.y);

  // 정적 메서드: 두 점 사이 직선 거리 계산
  static double distanceBetween(Point a, Point b) {
    var dx = a.x - b.x;
    var dy = a.y - b.y;
    return sqrt(dx * dx + dy * dy);
  }
}

void main() {
  var a = Point(2, 2);
  var b = Point(4, 4);
  var dist = Point.distanceBetween(a, b);
  print(dist); // 약 2.828
}Code language: JavaScript (javascript)

정적 메서드는 클래스에 소속되므로 클래스명으로 호출해야 하며, 매개변수로 두 개의 Point 인스턴스를 전달합니다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다