Dart 객체지향 프로그래밍
이 문서 주변 탐색
주제 태그, 링크, 시리즈 흐름을 중심으로 옆으로 이동할 수 있습니다.
시리즈 흐름
이 문서는 아직 읽기 시리즈에 연결되지 않았습니다.
관련 문서
같이 읽을 만한 관련 문서가 아직 없습니다.
이 문서를 참조하는 문서
이 문서를 참조하는 다른 문서가 아직 없습니다.
1. 클래스와 인스턴스
클래스란?
클래스는 설계도고, 인스턴스는 그 설계도로 만든 실제 객체임
void main() {
// Person 클래스로 morymory라는 인스턴스 생성
Person morymory = Person('mory', ['친구1', '친구2']);
morymory.sayHello(); // Hello~
morymory.introduce(); // 헬로우 mory
}
class Person {
String name;
List<String> friends;
// 생성자
Person(String name, List<String> friends)
: this.name = name,
this.friends = friends;
void sayHello() {
print('Hello~');
}
void introduce() {
print('헬로우 ${this.name}');
}
}클래스 내부의 함수는 "메서드"라고 부름 - 함수랑 메서드는 기능적으로 같은데, 메서드는 특정 객체에 속해있어서
this로 자기 자신 데이터에 접근할 수 있음this 키워드는 "이 인스턴스 자기 자신"을 가리킴. 메서드 안에서
this.name은 "이 객체의 name 속성"이라는 뜻
왜 클래스를 쓸까?
// ❌ 클래스 없이 여러 사람 관리
String person1Name = '모리';
int person1Age = 25;
String person2Name = '철수';
int person2Age = 30;
// 사람이 늘어날수록 변수가 기하급수적으로 증가...
// ✅ 클래스로 깔끔하게 관리
class Person {
String name;
int age;
Person(this.name, this.age);
}
List<Person> people = [
Person('모리', 25),
Person('철수', 30),
Person('영희', 28),
];2. 생성자 완전 정복
Dart의 생성자는 다른 언어보다 훨씬 유연하고 간결함
기본 생성자
class Person {
String name;
int age;
// 가장 기본적인 형태
Person(String name, int age)
: this.name = name,
this.age = age;
}간소화 문법 (Syntactic Sugar) ⭐
class Person {
String name;
int age;
// 위와 완전히 동일! Dart의 강력한 단축 문법
Person(this.name, this.age);
}this.name을 파라미터로 쓰면 자동으로 해당 필드에 할당됨이건 Dart만의 편의 기능이고, Java나 JavaScript에서는 안 됨
코드량이 절반 이하로 줄어들어서 실무에서 거의 항상 이 방식 씀
Named Parameters (명명된 매개변수)
class Person {
String name;
int age;
String? nickname; // 선택적
Person({
required this.name,
required this.age,
this.nickname,
});
}
void main() {
// 순서 상관없이 이름으로 전달 - 가독성 향상!
var person = Person(
age: 25,
name: '모리',
nickname: 'mory',
);
}Named Constructor (명명된 생성자)
신기하게 생성자를 여러 개 만들 수 있음. 오버로드 같은 느낌
class Person {
String name;
List<String> friends;
// 기본 생성자
Person(this.name, this.friends);
// Named Constructor - 리스트에서 생성
Person.fromList(List values)
: name = values[0],
friends = values[1];
// Named Constructor - 기본값으로 생성
Person.anonymous()
: name = '익명',
friends = [];
// Named Constructor - Map에서 생성 (JSON 파싱에 유용!)
Person.fromJson(Map<String, dynamic> json)
: name = json['name'],
friends = List<String>.from(json['friends'] ?? []);
}
void main() {
// 다양한 방법으로 같은 클래스의 인스턴스 생성
var p1 = Person('모리', ['친구1']);
var p2 = Person.fromList(['모링', ['친구A', '친구B']]);
var p3 = Person.anonymous();
var p4 = Person.fromJson({'name': '철수', 'friends': ['영희']});
}Named Constructor는 함수 오버로딩의 대안임. Dart는 같은 이름의 함수를 여러 개 못 만들어서 이 방식 씀
실무에서
fromJson,fromMap,empty,copy같은 이름 자주 씀Flutter에서 API 응답을 모델로 변환할 때
fromJson생성자가 필수적으로 쓰임
Initializer List (초기화 리스트)
class Rectangle {
final double width;
final double height;
final double area; // 계산된 값
Rectangle(this.width, this.height)
: area = width * height; // 생성자 본문 실행 전에 초기화
}콜론(:) 뒤에 오는 부분이 초기화 리스트로, 생성자 본문 실행 전에 실행됨
3. 불변성 (Immutability)
final - 런타임 불변
class Person {
final String name; // 한 번 설정되면 변경 불가
final List<String> friends;
Person(this.name, this.friends);
}
void main() {
var person = Person('모리', ['철수', '영희']);
// person.name = '다른이름'; // ❌ 컴파일 에러!
// ⚠️ 주의: 리스트 자체는 못 바꾸지만, 내용은 수정 가능!
person.friends.add('민수'); // ✅ 가능
print(person.friends); // [철수, 영희, 민수]
// person.friends = []; // ❌ 리스트 자체 교체는 불가
}JavaScript의 const와 비슷한 동작임. 참조(reference)는 고정되지만 내부 값은 변경 가능
진정한 불변 리스트가 필요하면
List.unmodifiable()쓰면 됨클래스 필드에
final붙이는 건 좋은 습관임. 의도치 않은 변경 방지
const 생성자 - 컴파일 타임 상수
class Point {
final int x;
final int y;
// const 생성자 - 모든 필드가 final이어야 함
const Point(this.x, this.y);
}
void main() {
// const로 생성하면 컴파일 타임에 결정됨
const p1 = Point(1, 2);
const p2 = Point(1, 2);
// 🎯 같은 값이면 메모리 공유!
print(identical(p1, p2)); // true - 같은 객체!
// const 없이 생성하면 매번 새 인스턴스
var p3 = Point(1, 2);
var p4 = Point(1, 2);
print(identical(p3, p4)); // false - 다른 객체
}
const 객체는 단 하나만 존재함 (Flyweight 패턴이랑 비슷)
Flutter에서 위젯에
const붙이면 리빌드 최적화에 큰 도움 됨const는 빌드 타임에 값을 알 수 있어야 함.
DateTime.now()나 함수 호출 결과는 사용 불가
final vs const 정리
특성 |
|
|
|---|---|---|
값 결정 시점 | 런타임 | 컴파일 타임 |
| ✅ 가능 | ❌ 불가 |
인스턴스 필드 | ✅ 가능 | ❌ 불가 (static만) |
메모리 공유 | ❌ 불가 | ✅ 동일 값은 공유 |
4. Getter와 Setter
Getter/Setter는 계산된 속성이나 접근 제어가 필요할 때 씀
Getter
class Person {
final String firstName;
final String lastName;
final List<String> friends;
Person(this.firstName, this.lastName, this.friends);
// Getter - 계산된 속성
String get fullName => '$firstName $lastName';
// Getter - 리스트의 첫 번째 요소
String get firstFriend {
if (friends.isEmpty) return '친구 없음';
return friends[0];
}
// Getter - 친구 수
int get friendCount => friends.length;
}
void main() {
var person = Person('김', '모리', ['철수', '영희']);
// 메서드가 아닌 속성처럼 접근!
print(person.fullName); // 김 모리
print(person.firstFriend); // 철수
print(person.friendCount); // 2
}Setter
class Temperature {
double _celsius = 0; // private 변수
// Getter
double get celsius => _celsius;
double get fahrenheit => _celsius * 9 / 5 + 32;
// Setter - 값 하나만 받을 수 있음!
set celsius(double value) {
if (value < -273.15) {
throw ArgumentError('절대영도 이하는 불가능');
}
_celsius = value;
}
// Setter - 화씨로 설정하면 자동 변환
set fahrenheit(double value) {
_celsius = (value - 32) * 5 / 9;
}
}
void main() {
var temp = Temperature();
temp.celsius = 25;
print(temp.fahrenheit); // 77.0
temp.fahrenheit = 100;
print(temp.celsius); // 37.777...
}Getter/Setter는 필드처럼 보이지만 실제로는 메서드임
Setter는 무조건 파라미터 하나만 받음 (= 연산자 우측 값)
실무에서 Setter보다 Getter를 훨씬 많이 씀. 값 설정은 명시적인 메서드가 더 명확할 때가 많음
5. 캡슐화와 Private
언더스코어(_)로 Private 선언
오 private라는 개념이 있네. 클래스 이름에 언더스코어 붙이면 됨. 함수도 마찬가지
// person.dart 파일
class Person {
String name; // public - 어디서든 접근 가능
String _secret; // private - 같은 파일에서만 접근 가능
Person(this.name, this._secret);
void _privateMethod() { // private 메서드
print('비밀: $_secret');
}
void revealSecret() { // public 메서드
_privateMethod(); // 내부에서는 호출 가능
}
}
// ⚠️ 같은 파일에서는 private도 접근 가능!
void sameFileFunction() {
var p = Person('모리', '비밀정보');
print(p._secret); // ✅ 같은 파일이라 가능
}// main.dart 파일 (다른 파일)
import 'person.dart';
void main() {
var person = Person('모리', '비밀');
print(person.name); // ✅ public
// print(person._secret); // ❌ 다른 파일에서 private 접근 불가
person.revealSecret(); // ✅ public 메서드 통해 간접 접근
}Dart의 private는 파일 단위임. 같은 파일이면 다른 클래스에서도 접근 가능
Java/C#의
private키워드랑 다르게 네이밍 컨벤션으로 구현됨클래스 자체도 private로 만들 수 있음:
class _InternalHelper { ... }
6. 상속 (Inheritance)
기본 상속
class Idol {
String name;
int membersCount;
Idol({required this.name, required this.membersCount});
void sayName() {
print('저는 ${this.name}입니다.');
}
void sayMembersCount() {
print('${this.name}은 ${this.membersCount}명의 멤버가 있습니다.');
}
}
// extends로 상속
class BoyGroup extends Idol {
// 자식 클래스의 생성자
BoyGroup(String name, int membersCount)
: super(name: name, membersCount: membersCount);
// ↑ super()로 부모 생성자 호출
// 자식만의 메서드 추가
void sayBoyGroup() {
print('저희는 남성 아이돌입니다.');
}
}
class GirlGroup extends Idol {
// 추가 속성
String company;
GirlGroup({
required String name,
required int membersCount,
required this.company,
}) : super(name: name, membersCount: membersCount);
void sayCompany() {
print('소속사는 $company입니다.');
}
}
void main() {
var bts = BoyGroup('BTS', 7);
bts.sayName(); // 저는 BTS입니다. (부모 메서드)
bts.sayBoyGroup(); // 저희는 남성 아이돌입니다. (자식 메서드)
var blackpink = GirlGroup(
name: 'BLACKPINK',
membersCount: 4,
company: 'YG',
);
blackpink.sayMembersCount(); // BLACKPINK은 4명의 멤버가 있습니다.
blackpink.sayCompany(); // 소속사는 YG입니다.
}extends는 "확장한다"는 의미로, 부모의 모든 것을 물려받음super는 "부모"를 가리킴. 생성자에서super()로 부모 생성자 호출해야 함Dart는 단일 상속만 지원함. 여러 클래스 상속은 불가능하고, 이걸 보완하기 위해 Mixin이 있음
7. 메서드 오버라이딩
@override 어노테이션
상속하고 있는 메서드를 덮어씌울 수 있음. 완전 똑같은 시그니처여야 함
class Idol {
String name;
int membersCount;
Idol({required this.name, required this.membersCount});
void introduce() {
print('안녕하세요, $name입니다.');
}
}
class BoyGroup extends Idol {
BoyGroup({required super.name, required super.membersCount});
// 부모 메서드를 덮어씀
@override
void introduce() {
// super로 부모 메서드 호출 가능
super.introduce(); // 안녕하세요, BTS입니다.
print('저희는 남성 아이돌입니다!');
}
}
void main() {
var bts = BoyGroup(name: 'BTS', membersCount: 7);
bts.introduce();
// 출력:
// 안녕하세요, BTS입니다.
// 저희는 남성 아이돌입니다!
}
오버라이딩 규칙
class Parent {
int calculate(int x) {
return x * 2;
}
}
class Child extends Parent {
// ✅ 시그니처가 완전히 동일해야 함
@override
int calculate(int x) {
return super.calculate(x) + 10; // 부모 결과에 10 추가
}
// ❌ 시그니처가 다르면 오버라이딩이 아닌 새 메서드
// int calculate(int x, int y) { } // 이건 다른 메서드
}
@override는 선택적이지만 항상 붙이는 게 좋음. 오타로 새 메서드 만드는 실수 방지super.method()로 부모 구현 호출하면서 기능 확장 가능this써도 부모 필드에 접근 가능하지만, 명확성 위해super권장. 근데 this 써도 문제없긴 함
8. Static 멤버
static 변수와 메서드
static 키워드도 있음
class Employee {
static String company = '네이버'; // 모든 인스턴스가 공유
static int employeeCount = 0; // 직원 수 카운터
String name;
Employee(this.name) {
employeeCount++; // 새 직원 생성될 때마다 증가
}
// static 메서드
static void printCompanyInfo() {
print('회사: $company, 총 직원 수: $employeeCount');
}
void introduce() {
// 인스턴스 메서드에서 static 접근 가능
print('저는 $company의 $name입니다.');
}
}
void main() {
// static은 클래스 이름으로 접근
print(Employee.company); // 네이버
var emp1 = Employee('모리');
var emp2 = Employee('철수');
Employee.printCompanyInfo(); // 회사: 네이버, 총 직원 수: 2
// 모든 직원의 회사 변경
Employee.company = '카카오';
emp1.introduce(); // 저는 카카오의 모리입니다.
emp2.introduce(); // 저는 카카오의 철수입니다.
}static은 클래스에 속하는 것이지, 인스턴스에 속하지 않음
인스턴스 안 만들어도
ClassName.staticMember로 접근 가능유틸리티 함수, 상수, 팩토리 메서드 등에 주로 씀
static 메서드 안에서는
this사용 못 함 (인스턴스가 없으니까)
9. 추상 클래스와 인터페이스
추상 클래스 (Abstract Class)
abstract를 넣으면 추상적이라는 의미. 그래서 함수 바디 안 넣어도 됨
// abstract 키워드로 추상 클래스 선언
abstract class Shape {
// 추상 메서드 - 본문 없음, 자식이 반드시 구현해야 함
double getArea();
double getPerimeter();
// 일반 메서드 - 공통 구현 제공
void describe() {
print('넓이: ${getArea()}, 둘레: ${getPerimeter()}');
}
}
class Rectangle extends Shape {
final double width;
final double height;
Rectangle(this.width, this.height);
@override
double getArea() => width * height;
@override
double getPerimeter() => 2 * (width + height);
}
class Circle extends Shape {
final double radius;
static const double pi = 3.14159;
Circle(this.radius);
@override
double getArea() => pi * radius * radius;
@override
double getPerimeter() => 2 * pi * radius;
}
void main() {
// Shape shape = Shape(); // ❌ 추상 클래스는 인스턴스화 불가
List<Shape> shapes = [
Rectangle(10, 5),
Circle(7),
];
for (var shape in shapes) {
shape.describe(); // 다형성!
}
}인터페이스 (implements)
Dart에는 별도의 interface 키워드가 없음. 모든 클래스가 인터페이스로 사용 가능
implements를 붙여서 사용. 시그니처를 강제화하는 거라고 보면 됨
// 이 클래스는 인터페이스로도 사용 가능
class Flyable {
void fly() {
print('날고 있습니다.');
}
}
class Swimmable {
void swim() {
print('수영하고 있습니다.');
}
}
// implements로 인터페이스 구현
// ⚠️ implements 쓰면 모든 메서드를 직접 구현해야 함!
class Duck implements Flyable, Swimmable {
@override
void fly() {
print('오리가 날고 있습니다.');
}
@override
void swim() {
print('오리가 수영하고 있습니다.');
}
}
// extends vs implements 비교
class Bird extends Flyable {
// fly() 구현 안 해도 됨 - 부모 것 상속받음
}
class Airplane implements Flyable {
@override
void fly() {
// 반드시 직접 구현해야 함!
print('비행기가 날고 있습니다.');
}
}extends= "~이다" 관계 (is-a), 부모 구현을 상속받음implements= "~처럼 행동한다" 관계 (can-do), 시그니처만 강제하고 구현은 직접추상 클래스는 일부 구현 + 일부 강제 (템플릿 메서드 패턴)
implements는 여러 개 가능, extends는 하나만 가능
실전 예시: Repository 패턴
// 인터페이스 역할의 추상 클래스
abstract class UserRepository {
Future<User?> findById(String id);
Future<List<User>> findAll();
Future<void> save(User user);
Future<void> delete(String id);
}
// 실제 구현 - 데이터베이스 버전
class DatabaseUserRepository implements UserRepository {
@override
Future<User?> findById(String id) async {
// DB 조회 로직
return User(id: id, name: 'from DB');
}
// ... 나머지 구현
}
// 테스트용 구현 - 메모리 버전
class InMemoryUserRepository implements UserRepository {
final Map<String, User> _storage = {};
@override
Future<User?> findById(String id) async {
return _storage[id];
}
// ... 나머지 구현
}10. 제네릭 (Generics)
타입을 외부에서 받을 때 사용함
기본 제네릭
// T는 타입 파라미터 - 외부에서 타입을 주입받음
class Box<T> {
T content;
Box(this.content);
T getContent() => content;
void setContent(T newContent) {
content = newContent;
}
}
void main() {
// String 타입의 Box
var stringBox = Box<String>('안녕하세요');
print(stringBox.getContent()); // 안녕하세요
// int 타입의 Box
var intBox = Box<int>(42);
print(intBox.getContent()); // 42
// 타입 추론도 가능
var autoBox = Box('자동 추론'); // Box<String>
}여러 타입 파라미터
class Pair<K, V> {
K key;
V value;
Pair(this.key, this.value);
@override
String toString() => '($key: $value)';
}
void main() {
var entry = Pair<String, int>('age', 25);
print(entry); // (age: 25)
var coord = Pair<double, double>(37.5, 127.0);
print(coord); // (37.5: 127.0)
}제네릭 제약 (Bounded Generics)
// T는 num을 상속한 타입만 가능 (int, double)
class NumberBox<T extends num> {
T value;
NumberBox(this.value);
// num의 메서드 사용 가능
bool isPositive() => value > 0;
T doubled() => (value * 2) as T;
}
void main() {
var intBox = NumberBox<int>(10);
print(intBox.doubled()); // 20
var doubleBox = NumberBox<double>(3.14);
print(doubleBox.isPositive()); // true
// var stringBox = NumberBox<String>('hello'); // ❌ 컴파일 에러!
}제네릭은 타입 안전성 유지하면서 코드 재사용 가능하게 해줌
List<T>,Map<K, V>,Future<T>등 Dart 코어 라이브러리가 제네릭으로 가득함Flutter에서
StatefulWidget<T>,Provider<T>등 제네릭 많이 씀관례적으로
T(Type),E(Element),K(Key),V(Value) 사용함
11. Mixin
Mixin이란?
Mixin은 코드를 재사용하면서 다중 상속의 문제를 피하는 방법임
// mixin 정의
mixin Singing {
void sing() {
print('노래를 부릅니다 🎵');
}
}
mixin Dancing {
void dance() {
print('춤을 춥니다 💃');
}
}
mixin Acting {
void act() {
print('연기를 합니다 🎭');
}
}
// 기본 클래스
class Performer {
String name;
Performer(this.name);
void introduce() {
print('안녕하세요, $name입니다.');
}
}
// with 키워드로 mixin 사용
class Idol extends Performer with Singing, Dancing {
Idol(super.name);
}
class Actor extends Performer with Acting {
Actor(super.name);
}
// 세 가지 모두!
class MultiTalent extends Performer with Singing, Dancing, Acting {
MultiTalent(super.name);
}
void main() {
var idol = Idol('BTS');
idol.introduce(); // 안녕하세요, BTS입니다.
idol.sing(); // 노래를 부릅니다 🎵
idol.dance(); // 춤을 춥니다 💃
// idol.act(); // ❌ Acting mixin 없음
var talent = MultiTalent('아이유');
talent.sing();
talent.dance();
talent.act();
}Mixin 제약조건
// 특정 클래스에서만 사용 가능한 mixin
mixin IdolSkills on Performer {
void fanService() {
print('$name이(가) 팬서비스를 합니다!');
}
}
class Idol extends Performer with IdolSkills {
Idol(super.name);
}
// ❌ Performer를 상속하지 않으면 사용 불가
// class Animal with IdolSkills { }extends는 하나, with는 여러 개 가능함
Mixin은 생성자를 가질 수 없음 (인스턴스화 불가)
on키워드로 특정 클래스에서만 사용하도록 제한 가능Flutter에서
SingleTickerProviderStateMixin,WidgetsBindingObserver등 자주 쓰임
12. OOP 설계 원칙
SOLID 원칙 요약
원칙 | 설명 | Dart 적용 |
|---|---|---|
Single Responsibility | 클래스는 하나의 책임만 | 작은 클래스로 분리 |
Open/Closed | 확장에 열림, 수정에 닫힘 | 상속, Mixin 활용 |
Liskov Substitution | 자식은 부모 대체 가능 | 올바른 override |
Interface Segregation | 인터페이스 분리 | 작은 mixin들 |
Dependency Inversion | 추상화에 의존 | 추상 클래스, implements |
실전 팁
// ❌ 나쁜 예: 너무 많은 책임
class UserManager {
void createUser() { }
void deleteUser() { }
void sendEmail() { } // 이메일은 별개의 책임
void generateReport() { } // 리포트도 별개의 책임
}
// ✅ 좋은 예: 책임 분리
class UserRepository {
void create() { }
void delete() { }
}
class EmailService {
void send() { }
}
class ReportGenerator {
void generate() { }
}
본 글은 AI의 도움을 받아 작성되었습니다.
읽기 시리즈
현재 위치를 확인하고, 흐름을 따라 바로 다음 읽기로 이어갈 수 있습니다.