Dart 튜토리얼 7편: 클래스 설계 기본(Classes·Constructors·Methods·Extend)

요약#

핵심 요지#

  • 문제 정의: 클래스는 “데이터”와 “동작”이 함께 움직이는데, 규칙 없이 섞으면 사용자가 API를 이해하기 어렵다.
  • 핵심 주장: Dart는 class1를 중심으로 constructor2로 초기화하고, method3/getter4/setter5로 동작을 설계한다.
  • 주요 근거: extends6super7로 공통 동작을 상속하고, 필요한 부분만 override8로 바꿀 수 있다.
  • 실무 기준: 상태는 필드로, 초기화 규칙은 생성자로, 외부 API는 메서드·getter/setter로, 확장은 상속으로 단계적으로 설계한다.

문서가 설명하는 범위#

  • class와 인스턴스 변수, 클래스 멤버의 기본 규칙
  • constructor로 필드를 초기화하는 방법과 형태
  • method/getter/setteroperator9의 사용법
  • extends로 상속하고 superoverride로 확장하는 방식

읽는 시간: 17분 | 난이도: 초급


참고 자료#

  • Classes - classes, instance variables, class members
  • Constructors - constructors, initializing fields, named constructors
  • Methods - instance methods, getters/setters, operators, abstract methods
  • Extend a class - extends, overriding, super, noSuchMethod

문제 상황#

클래스는 데이터를 담는 그릇이면서, 동시에 그 데이터로 무엇을 할지 정의하는 설계 단위입니다.
입문자 입장에서는 “어디까지를 필드10로 두고, 어디부터를 메서드로 만들지”가 가장 헷갈립니다.
인스턴스11를 만들 때 생성자를 어디에 두고 어떤 값을 받을지도 함께 고민해야 합니다.

이 혼란을 줄이려면, 역할을 단계별로 나눠 생각하는 것이 좋습니다.
먼저 class로 형태를 만들고, constructor로 초기화 규칙을 고정합니다.
그 다음 method/getter/setter로 외부에서 쓰기 좋은 API를 만듭니다.
마지막으로 extendssuper로 공통 동작을 재사용합니다.


해결 방법#

클래스 설계를 4단계로 정리합니다.
각 단계는 “코드를 어떻게 쓰는지”뿐 아니라 “왜 분리하는지”도 함께 설명합니다.

단계 1: class로 데이터와 책임을 묶기#

Why#

NOTE

관련된 값이 흩어져 있으면, 전달할 때 순서를 틀리거나 누락하기 쉽습니다.
그래서 “의미가 있는 데이터 묶음”을 하나의 타입으로 고정하는 것이 필요합니다.

// (x, y)를 따로 전달하면 순서를 바꿔 넣는 실수가 생길 수 있다.
void move(double x, double y) {}

What#

NOTE

class는 데이터(필드)와 동작을 묶어, 한 덩어리로 다루게 하는 타입입니다.

How#

TIP

class는 관련된 데이터와 동작을 묶는 단위입니다.
기본 예시는 좌표를 나타내는 타입처럼, 의미가 있는 데이터 묶음을 클래스 하나로 만드는 흐름입니다.
필드인스턴스가 가진 상태를 표현하고, 생성자는 그 상태를 만들어 줍니다.

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

Watch out#

WARNING

final 필드는 생성 시점에 반드시 값이 정해져야 합니다.
따라서 생성자에서 값을 받거나, 초기값/초기화 목록으로 값을 넣어야 합니다.

class Point {
final double x;
final double y;
// Point(); // 컴파일 오류: final 필드가 초기화되지 않았다.
Point(this.x, this.y);
}

결론: 의미 있는 데이터 묶음이 한 타입으로 고정되어, 전달과 재사용이 쉬워집니다.


단계 2: constructor로 초기화 규칙을 고정하기#

Why#

NOTE

인스턴스를 만든 뒤에 필드를 하나씩 채우기 시작하면, “중간 상태”가 생깁니다.
그래서 생성 단계에서 초기화 규칙을 한 번에 고정하는 편이 안전합니다.

class User {
String name;
int age;
User(this.name, this.age);
}

What#

NOTE

constructor는 인스턴스를 만들면서 필드를 초기화하는 규칙입니다.

How#

TIP

constructor는 인스턴스를 만들 때 “필드를 어떤 규칙으로 채울지”를 정합니다.
기본 생성자, 이름 있는 생성자, 그리고 초기화 목록을 사용한 패턴을 살펴보면 흐름이 잡힙니다.
입문자 관점에서는 “생성자에서 필드를 다 채운다”는 원칙만 먼저 잡아도 사고가 줄어듭니다.

class Person {
final String name;
final int age;
Person(this.name, this.age);
Person.teen(this.name) : age = 10;
}

Watch out#

WARNING

필드를 “나중에 채우겠다”는 방식은 실수를 만들기 쉽습니다.
특히 final 필드는 생성자에서 초기화가 끝나야 하므로, 생성자/초기화 목록으로 규칙을 고정하는 편이 안전합니다.

결론: 인스턴스가 만들어진 직후의 상태가 예측 가능해지고, 필드 초기화가 흩어지지 않습니다.


단계 3: method/getter/setter로 외부 API를 설계하기#

Why#

NOTE

외부에 그대로 필드를 노출하면, 어디서 어떤 값이 들어오는지 통제하기 어렵습니다.
그래서 “읽기/쓰기/동작”을 각각의 멤버로 분리해 의도를 드러내는 편이 좋습니다.

class Temperature {
double celsius;
Temperature(this.celsius);
}

What#

NOTE

getter/setter는 필드처럼 보이지만, 내부 규칙(계산/검증/변환)을 넣을 수 있는 멤버입니다.

How#

TIP

동작은 세 가지로 나눠 생각하면 정리됩니다.
method는 “호출해서 실행하는 동작”이고, getter는 “값처럼 읽히는 계산”입니다.
setter는 “대입을 받는 동작”이라서, 내부 규칙(검증/변환)을 한 곳에 모을 수 있습니다.

class Rectangle {
double left;
double top;
double width;
double height;
Rectangle(this.left, this.top, this.width, this.height);
double get right => left + width;
set right(double value) => left = value - width;
}

같은 클래스에서 method를 추가해 “행동”도 표현할 수 있습니다.

class Rectangle {
double left;
double top;
double width;
double height;
Rectangle(this.left, this.top, this.width, this.height);
double get right => left + width;
set right(double value) => left = value - width;
void moveBy(double dx, double dy) {
left = left + dx;
top = top + dy;
}
}

Watch out#

WARNING

setter에서 자기 자신을 다시 대입하면 무한 재귀가 될 수 있습니다.
즉, right = value; 같은 대입을 set right(...) 안에서 다시 호출하지 않도록 주의해야 합니다.

class Bad {
double _right = 0;
double get right => _right;
// set right(double value) => right = value; // 위험: setter가 자기 자신을 다시 호출한다.
set right(double value) => _right = value;
}

결론: 외부에서는 값처럼, 내부에서는 규칙처럼 동작을 정리해 API12를 읽기 쉽게 만듭니다.


단계 4: extends로 공통 동작을 재사용하기#

Why#

NOTE

공통 동작이 여러 곳에 복사되면, 수정할 때 누락이 생기기 쉽습니다.
그래서 “공통 부분은 한 곳에 모으고”, 다른 부분만 덮어쓰는 방식이 필요합니다.

class Animal {
void speak() {}
}

What#

NOTE

extends로 상속 관계를 만들고, 필요한 동작만 override로 바꿉니다.

How#

TIP

extends는 기존 클래스를 기반으로 새 클래스를 만드는 문법입니다.
상속은 “공통 동작을 재사용하고, 필요한 부분만 확장”하는 방식으로 이해하면 됩니다.
이때 부모 생성자 호출과 super의 역할이 함께 등장합니다.

class Animal {
final String name;
Animal(this.name);
void speak() {
print('$name makes a noise.');
}
}
class Dog extends Animal {
Dog(super.name);
@override
void speak() {
print('$name barks.');
}
}

Watch out#

WARNING

override는 동작을 바꾸는 것이므로, 기본 동작까지 유지해야 한다면 super 호출을 포함해야 합니다.

class Dog extends Animal {
Dog(super.name);
@override
void speak() {
super.speak();
print('$name barks.');
}
}

결론: 공통 로직은 부모에 두고, 차이점만 override로 바꿔 유지보수를 단순하게 만듭니다.

Footnotes#

  1. class(클래스): 데이터와 동작을 한 타입으로 묶는 정의 단위다.

  2. constructor(생성자): 인스턴스를 만들 때 필드를 초기화하는 특별한 멤버다.

  3. method(메서드): 객체에 속한 동작을 정의하는 함수다.

  4. getter(게터): 필드처럼 읽히지만 실제로는 값을 반환하는 메서드다.

  5. setter(세터): 필드처럼 대입되지만 실제로는 값을 받아 상태를 바꾸는 메서드다.

  6. extends(익스텐즈): 다른 클래스를 상속받아 새 클래스를 만드는 키워드다.

  7. super(수퍼): 상위 클래스의 생성자나 멤버를 가리키는 키워드다.

  8. override(오버라이드): 상위 클래스의 멤버를 하위 클래스에서 재정의하는 표시다.

  9. operator(연산자): + 같은 연산 기호로 동작을 정의하는 문법 요소다.

  10. field(필드): 클래스가 가지는 데이터 항목으로, 인스턴스의 상태를 나타낸다.

  11. instance(인스턴스): class로부터 만들어진 실제 객체다.

  12. API(API): 외부에서 사용할 수 있도록 공개한 사용 방법의 모음이다.

공유

이 글이 도움이 되었다면 다른 사람과 공유해주세요!

Dart 튜토리얼 7편: 클래스 설계 기본(Classes·Constructors·Methods·Extend)
https://moodturnpost.net/posts/dart/dart-class-design-basics/
작성자
Moodturn
게시일
2026-01-04
Moodturn

목차