Dart 튜토리얼 8편: 상속·구성·확장(Mixins·Enums·Extension methods·Extension types)

요약#

핵심 요지#

  • 문제 정의: 기능을 재사용하려고 무조건 extends1만 쓰면 상속 구조가 복잡해진다.
  • 핵심 주장: Dart는 상황에 따라 mixin2, enum3, extension method4, extension type5로 확장 전략을 나눌 수 있다.
  • 주요 근거: 각 도구의 목적과 제약이 다르고, 타입과 멤버 해석 규칙도 다르다.
  • 실무 기준: “기능 추가”인지, “상태 모델링”인지, “기존 타입 API 보강”인지, “새 타입처럼 보이게”인지 먼저 결정한 뒤 도구를 고른다.

문서가 설명하는 범위#

  • mixinon6 제약, mixin class7 개념
  • enum 선언, 멤버 추가, switch8name9 사용
  • extension method로 기존 타입에 멤버 추가하는 방식
  • extension type의 “컴파일 타임 뷰”와 representation type10 개념

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


참고 자료#


문제 상황#

프로젝트가 커지면 “비슷한 기능”이 여러 곳에서 반복됩니다.
이때 가장 쉬운 선택은 상속입니다.
하지만 상속은 한 번 깊어지면 구조를 바꾸기 어렵습니다.
이런 이유로, “상속만으로 해결하지 말고 목적에 맞는 도구를 고르자”는 방향에서 여러 기능을 함께 봐야 합니다.

이 글은 네 가지 도구를 한 번에 묶어 정리합니다.
어떤 상황에서 무엇을 쓰는지 기준을 세우는 것이 목표입니다.


해결 방법#

단계 1: mixin으로 “기능 묶음”을 붙이기#

Why#

NOTE

상속은 공통 동작을 재사용하기 좋지만, 계층이 깊어지면 구조 변경이 어려워집니다.
그래서 “타입 계층은 유지하면서 기능만 덧붙이는 방식”이 필요합니다.

class A {
void log() => print('log');
}
class B {
void log() => print('log'); // 중복
}

What#

NOTE

mixin은 여러 클래스에서 재사용할 멤버 묶음을 정의하는 방법입니다.
with11를 사용해 클래스에 mixin을 적용합니다.
또한 on로 “어떤 타입에만 붙일 수 있는지”를 제한할 수 있습니다.

How#

TIP

mixin은 여러 클래스에서 재사용할 멤버 묶음을 정의하는 방법입니다.
with를 사용해 클래스에 mixin을 적용합니다.
또한 on로 “어떤 타입에만 붙일 수 있는지”를 제한할 수 있습니다.

mixin Musical {
bool canPlayPiano = false;
bool canCompose = false;
bool canConduct = false;
void entertainMe() {
if (canPlayPiano) {
print('Playing piano');
} else if (canConduct) {
print('Waving hands');
} else {
print('Humming to self');
}
}
}
class Musician with Musical {}

mixin은 상속이 아니기 때문에, “기능을 덧붙인다”는 느낌에 가깝습니다.
하지만 아무 클래스에나 붙이면 규칙이 흐려질 수 있습니다.
그래서 on로 제약을 걸어, 필요한 필드나 메서드가 있는 타입에만 섞도록 범위를 좁힙니다.

class Performer {
void perform() => print('Performing');
}
mixin PerformerDuty on Performer {
void callTime() => perform();
}
class Singer extends Performer with PerformerDuty {}

Watch out#

WARNING

mixin은 상속이 아니기 때문에, “기능을 덧붙인다”는 느낌에 가깝습니다.
하지만 아무 클래스에나 붙이면 규칙이 흐려질 수 있습니다.
그래서 on로 제약을 걸어, 필요한 필드나 메서드가 있는 타입에만 섞도록 범위를 좁힙니다.

결론: 공통 기능을 분리해 재사용하면서도, 적용 대상은 on로 제한해 설계를 흔들리지 않게 할 수 있습니다.


단계 2: enum으로 “상태”를 안전하게 모델링하기#

Why#

NOTE

상태를 문자열로 다루면 오타/누락이 생기기 쉽습니다.
그래서 가능한 값의 집합을 타입으로 고정하는 편이 안전합니다.

const status = 'approved';
// if (status == 'aproved') {} // 오타가 나도 컴파일 단계에서 잡히지 않는다.

What#

NOTE

enum은 가능한 값의 집합을 선언하는 방식입니다.
switch로 분기할 때 enum을 쓰면 가독성이 좋아집니다.
또한 각 열거 값에는 name 속성이 있어 문자열 이름을 얻을 수 있습니다.

How#

TIP
enum Color { red, green, blue }
void main() {
final aColor = Color.blue;
switch (aColor) {
case Color.red:
print('Red as roses!');
case Color.green:
print('Green as grass!');
default:
print(aColor);
}
print(Color.blue.name);
}

Watch out#

WARNING

enum은 단순한 값 목록만이 아니라, 멤버를 가질 수 있는 타입이기도 합니다.
즉, 값의 집합을 선언하면서 해당 값에 대한 계산도 한 곳에 둘 수 있습니다.

결론: 문자열 비교 대신 타입으로 상태를 다루고, switch로 분기를 정리해 실수를 줄일 수 있습니다.


단계 3: extension method로 기존 타입에 “새 API”를 붙이기#

Why#

NOTE

기존 타입을 직접 수정할 수 없거나, 수정하고 싶지 않은 상황이 많습니다.
그래서 “타입은 그대로 두고, 필요한 멤버만 추가하는 방식”이 필요합니다.

// String에 parseInt 같은 메서드가 있으면 편하지만, String을 직접 수정할 수는 없다.

What#

NOTE

extension method는 기존 타입을 수정하지 않고 새 멤버를 추가하는 방법입니다.
확장 선언에서 on12으로 대상 타입을 지정합니다.
그리고 확장 안에서 getter13, operator14, 일반 메서드를 모두 정의할 수 있다고 보여줍니다.

How#

TIP
extension NumberParsing on String {
int parseInt() => int.parse(this);
}
void main() {
print('42'.parseInt());
}

제네릭을 함께 쓰면, 호출한 쪽의 정적 타입에 따라 타입 매개변수가 묶입니다.

extension MyFancyList<T> on List<T> {
int get doubleLength => length * 2;
List<T> operator -() => reversed.toList();
List<List<T>> split(int at) => [sublist(0, at), sublist(at)];
}

Watch out#

WARNING

extension method는 “해당 이름의 멤버가 없을 때” 주로 효과가 드러납니다.
기존 타입에 같은 이름의 멤버가 이미 있다면, 어느 쪽이 선택되는지(우선순위)가 코드 맥락에 따라 달라질 수 있습니다.

결론: 기존 타입을 감싸는 래퍼 클래스를 만들지 않고도, 프로젝트에 필요한 편의 API를 추가할 수 있습니다.


단계 4: extension type로 “새 타입처럼 보이게” 만들기#

Why#

NOTE

값의 “의미”는 다르지만, 내부 표현은 같은 경우가 있습니다.
예를 들어 int로 관리하되 “이 값은 그냥 숫자가 아니라 특정 규칙을 가진 값”이라고 표현하고 싶을 수 있습니다.

// 같은 int라도 의미가 다른 값이 섞이면 실수가 생길 수 있다.
int userId = 1;
int productId = 2;

What#

NOTE

extension type는 기존 값을 기반으로 “새 타입처럼” 보이게 만드는 선언입니다.
핵심 메시지는 두 가지입니다.
첫째, extension typecompile time15에 의미가 있지만, 컴파일 과정에서 erasure16될 수 있습니다.
둘째, extension typerun time17에 별도의 wrapper object18를 만들지 않을 “선택지”를 제공한다고 설명합니다.
일반 wrapper class19가 더 안전할 수 있지만, extension typewrapper object를 피할 수 있어 일부 상황에서 성능에 이점이 있을 수 있습니다.

입문자 관점에서는 이렇게 이해하면 됩니다.
extension type은 “새 객체를 하나 더 만드는 것”이 아니라, “이 값은 이런 규칙으로만 쓰겠다”를 타입으로 표현하는 도구입니다.
그래서 representation type과 “어떤 멤버가 보이는지” 규칙을 먼저 잡아야 합니다.

How#

TIP

아래는 “가장 작은 형태 → 멤버 노출 규칙 → 생성자/검증 오해 포인트” 순서로 따라가는 구성입니다.

4-1) 가장 작은 형태: representation type부터 정하기#

extension type E(int i)처럼, 괄호 안에 representation type을 둡니다.

extension type E(int i) {
// 허용할 연산(멤버) 집합을 정의한다.
}

이때 중요한 점은, extension type에서 “보이게 할 멤버”를 내가 선택한다는 것입니다.
즉, representation type이 가진 멤버가 자동으로 전부 노출되는 방식이 아닙니다.

4-2) “정의한 멤버만” 쓸 수 있다: NumberE 예시로 감 잡기#

int를 기반으로 NumberE를 만들고, operator·getter·메서드를 직접 정의할 수 있습니다.

extension type NumberE(int value) {
// 연산자:
NumberE operator +(NumberE other) => NumberE(value + other.value);
// getter:
NumberE get next => NumberE(value + 1);
// 메서드:
bool isValid() => !value.isNegative;
}

아래처럼, “extension type의 정적 타입(static type20)“에서 무엇이 가능한지/불가능한지를 확인할 수 있습니다.

void testE() {
var num1 = NumberE(1);
int num2 = NumberE(2); // 오류: 'NumberE'를 'int'에 할당할 수 없다.
num1.isValid(); // 가능: extension type이 정의한 멤버 호출.
num1.isNegative(); // 오류: 'NumberE'는 'int'의 'isNegative' 멤버를 정의하지 않는다.
var sum1 = num1 + num1; // 가능: 'NumberE'가 '+'를 정의한다.
var diff1 = num1 - num1; // 오류: 'NumberE'는 'int'의 '-' 멤버를 정의하지 않는다.
var diff2 = num1.value - 2; // 가능: representation type의 값(value)에 참조로 접근할 수 있다.
var sum2 = num1 + 2; // 오류: 매개변수 타입 'NumberE'에 'int'를 넣을 수 없다.
List<NumberE> numbers = [
NumberE(1),
num1.next, // 가능: 'next' getter는 'NumberE'를 반환한다.
1, // 오류: 'int' 요소를 'NumberE' 리스트에 넣을 수 없다.
];
}

이 예시는 Step 4의 목표(“새 타입처럼 보이게”)를 가장 직관적으로 보여줍니다.
int를 그대로 쓰면 가능한 연산이 너무 많습니다.
반면 NumberE처럼 만들면, 타입이 허용하지 않는 사용은 컴파일 단계에서 막힙니다.

4-3) 타입만 바뀌었다고 생성자가 자동 실행되지는 않는다#

static type의 변화가 곧 생성자 호출을 의미하는 것은 아닙니다.
검증 같은 작업을 “생성자에서 반드시” 수행하고 싶다면, NumberE(i)처럼 명시적으로 생성자를 호출해야 한다고 안내합니다.

아래는 intrepresentation type으로 삼아 “ID처럼 써야 하는 값”에 허용할 연산만 남기는 패턴입니다.
operator를 필요한 만큼만 노출해, 잘못된 연산을 컴파일 단계에서 막을 수 있습니다.

extension type IdNumber(int id) {
// 'int' 타입의 '<' 연산자를 감싼다.
operator <(IdNumber other) => id < other.id;
// 예를 들어 '+' 연산자는 선언하지 않는다.
// ID에는 더하기가 의미 없기 때문이다.
}
void main() {
// extension type로 규칙을 강제하지 않으면,
// 'int'는 ID를 위험한 연산에 노출한다.
int myUnsafeId = 42424242;
myUnsafeId = myUnsafeId + 10; // 동작하지만, ID에는 허용되면 안 되는 연산이다.
var safeId = IdNumber(42424242);
safeId + 10; // 컴파일 오류: '+' 연산자가 없다.
myUnsafeId = safeId; // 컴파일 오류: 타입이 맞지 않는다.
myUnsafeId = safeId as int; // 가능: representation type으로 런타임 캐스팅.
safeId < IdNumber(42424241); // 가능: 감싼 '<' 연산자를 사용한다.
}

Watch out#

WARNING

extension typecompile time에 의미가 있지만, erasure될 수 있습니다.
즉, 실행 중에는 “별도 객체로 감싼 타입”처럼 동작한다고 기대하면 오해가 생길 수 있습니다.

또한 static type이 바뀐다고 해서 생성자가 자동으로 실행되는 것이 아닙니다.
검증/정규화가 필요하다면, 생성자 호출을 코드에서 명시해야 합니다.

extension type IdNumber(int id) {}
void main() {
final id = IdNumber(1);
// 여기에서 별도 검증 로직이 자동으로 실행되지는 않는다.
}

결론: 코드에서는 새 타입처럼 다루면서도, wrapper object를 만들지 않는 선택지로 일부 상황에서는 비용을 줄일 수 있습니다.

Footnotes#

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

  2. mixin(믹스인): 여러 클래스에 재사용할 멤버 묶음을 섞어 넣는 기능이다.

  3. enum(열거형): 가능한 값의 집합을 선언하는 타입이다.

  4. extension method(확장 메서드): 기존 타입에 새 멤버를 추가하는 기능이다.

  5. extension type(확장 타입): 기존 값을 기반으로 새 타입처럼 보이게 하는 타입 선언이다.

  6. on(제약 절): mixin을 적용할 수 있는 타입을 제한하는 문법이다.

  7. mixin class(믹스인 클래스): 클래스와 mixin 양쪽으로 사용할 수 있는 선언 형태다.

  8. switch(스위치): 값에 따라 실행 경로를 나누는 분기 문법이다.

  9. name(이름 속성): enum 값의 이름 문자열을 제공하는 속성이다.

  10. representation type(표현 타입): extension type이 내부적으로 표현되는 실제 타입이다.

  11. with(위드): 클래스에 mixin을 적용하는 키워드다.

  12. on(대상 타입): extension method가 적용될 대상 타입을 지정하는 문법이다.

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

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

  15. compile time(컴파일 시점): 소스 코드를 분석하고 프로그램을 만들 때(컴파일러가 판단하는 단계)를 말한다.

  16. erasure(소거): 컴파일 과정에서 특정 타입 정보가 런타임에는 남지 않는 특성이다.

  17. run time(실행 시점): 프로그램이 실제로 실행 중일 때를 말한다.

  18. wrapper object(래퍼 객체): 다른 값을 감싸서 새로운 동작이나 제약을 제공하는 객체다.

  19. wrapper class(래퍼 클래스): 다른 값을 필드로 감싸 별도 객체로 보관하는 일반 클래스다.

  20. static type(정적 타입): 컴파일러가 변수/표현식에 대해 알고 있는 타입이다.

공유

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

Dart 튜토리얼 8편: 상속·구성·확장(Mixins·Enums·Extension methods·Extension types)
https://moodturnpost.net/posts/dart/dart-inheritance-composition-extension/
작성자
Moodturn
게시일
2026-01-04
Moodturn

목차