개체와 개체 지향 프로그래밍(Object Oriented Programming)
Dart는 개체 지향 프로그래밍으로 불리는 캡슐화, 상속, 다형성, 추상화 등의 개념을 제공합니다. 지금까지 배운 클래스와 클래스의 구성 요소들을 개체 지향 프로그래밍 관점으로 정리해보는 시간을 갖겠습니다.
// 개체 지향 프로그래밍: 캡슐화, 상속, 다형성, 추상화를 제공하는 프로그래밍 작성 기법
NOTE
이 문서의 내용은 박용준 강사가 집필한 C# 교과서의 내용을 기반으로 합니다. 다만, C 언어 또는 파이썬 기초 문법을 한 학기 이상 학습한 학생 개발자도 따라하기 어렵지 않게 작성했습니다.
개체 지향 프로그래밍 소개
여러분들이 이 강의를 듣고 있는 지금, 현업에서 매우 널리 사용되는 프로그램 작성 기법 중 하나가 바로 개체 지향 프로그래밍(Object Oriented Programming) 입니다. 보통 줄여서 OOP라고 부릅니다. 이번 강의를 통해 Dart 언어에서의 개체 지향 프로그래밍의 기본 개념을 가볍게 정리해 보겠습니다.
Flutter 역시 Dart 언어 위에서 동작하고, Flutter의 위젯 구조 또한 클래스를 기반으로 설계되어 있기 때문에 Dart의 개체 지향 개념을 이해하는 것은 매우 중요합니다.
개체 지향 프로그래밍의 목적
개체 지향 프로그래밍의 목적은 다음과 같습니다.
- 프로그램을 분석하기 쉬워집니다.
- 프로그램 유지보수가 쉬워집니다.
- 프로그램의 특정 기능을 재사용할 수 있습니다.
- 복잡한 현실 세계를 코드로 표현하기 쉬워집니다.
물론 이 말이 곧 개체 지향 프로그래밍이 모든 상황에서 무조건 정답이라는 뜻은 아닙니다. 다만, 큰 프로그램을 구조적으로 설계할 때 매우 강력한 도구가 됩니다.
클래스, 개체, 속성, 메서드
Dart에서도 클래스와 개체는 보통 명사로 표현합니다. 속성은 개체가 가진 데이터이고, 메서드는 개체가 수행하는 동작입니다.
예를 들면 다음과 같습니다.
- 클래스:
Car,Person,Dog - 속성:
name,age,color - 메서드:
run(),eat(),cry()
개체(Object)
클래스를 사용하여 새로운 형식을 정의하고, 그 클래스로부터 만들어진 실제 값을 개체(Object) 또는 인스턴스(instance) 라고 합니다.
개체는 보통 다음과 같은 3가지 관점에서 설명할 수 있습니다.
무엇인가이다(is something)
- 예) 고객(Customer), 자동차(Car), 학생(Student)
데이터를 가진다(has data)
- 예) 이름(name), 나이(age), 색상(color)
동작을 수행한다(performs action)
- 예) 이름 변경(changeName), 달리기(run), 울기(cry)
참고: 개체와 객체
많은 책과 강의에서는 Object Oriented Programming을 객체 지향 프로그래밍이라고도 부릅니다. 이 글에서는 원문의 흐름을 살려 개체 지향 프로그래밍이라고 표현하겠습니다. 실제로는 같은 의미로 이해하면 됩니다.
참고: Dart의 간단한 데이터 클래스 느낌
C#에서 POCO 클래스가 있듯이, Dart에서도 속성 위주로 이루어진 간단한 클래스를 만들 수 있습니다.
class PetFish {
String name;
int age;
String kind;
PetFish(this.name, this.age, this.kind);
}
void main() {
var fish = PetFish('Nemo', 1, 'Clownfish');
print(fish.name);
}
이런 클래스는 데이터를 묶어서 다루기에 좋습니다.
현실 세계의 자동차 설계도 및 자동차 개체 흉내내기
현실 세계의 자동차 설계도와 설계도로부터 만들어진 자동차를 프로그램 세계에서는 Car 클래스와 car 개체로 표현할 수 있습니다. 이를 통해 개체 지향 프로그래밍의 핵심 용어를 정리해 보겠습니다.
이번 절은 지금까지 배운 클래스, 개체, 속성, 메서드 등을 OOP 관점으로 다시 묶어 보는 시간이므로 가볍게 읽고 넘어가면 됩니다.
개체 지향 프로그래밍
개체 지향 프로그래밍을 설명할 때 자주 쓰는 비유 중 하나는 현실 세계를 프로그래밍 세계로 옮겨 놓는 것입니다. 이를 모델링(modeling)이라고도 합니다.
예를 들어 현실 세계의 자동차를 프로그램 속 Car 클래스로 표현하면, 자동차가 가지는 여러 특징과 동작을 코드로 관리할 수 있게 됩니다.
클래스
클래스는 흔히 설계도에 비유합니다.
- 클래스 = 설계도
Dart에서 클래스는 다음처럼 정의할 수 있습니다.
class Car {
String name = '좋은차';
void run() {
print('$name 자동차가 달립니다.');
}
}
개체
클래스는 설계도이고, 실제로 만들어진 하나의 값이 바로 개체입니다.
- 자동차 설계도(클래스) → 생성 → 자동차(개체)
Dart에서는 클래스로부터 개체를 다음처럼 만듭니다.
void main() {
var car = Car();
car.run();
}
일반적으로 클래스 이름은 Car처럼 대문자로 시작하고, 개체 이름은 car처럼 소문자로 시작하는 경우가 많습니다.
필드(Field)
필드는 클래스 내부에 저장되는 데이터입니다. 자동차로 비유하면 자동차의 내부 부품이나 상태 값과 비슷합니다.
Dart에서는 클래스 안의 변수를 필드처럼 사용할 수 있습니다.
class Car {
String name = '기본차';
}
생성자(Constructor)
생성자는 개체가 생성될 때 가장 먼저 실행되는 특별한 메서드입니다. Dart에서는 클래스 이름과 같은 이름으로 생성자를 정의합니다.
class Car {
String name;
Car(this.name);
}
사용 예:
void main() {
var car = Car('스포츠카');
print(car.name);
}
소멸자(Destructor)
C#에는 소멸자 문법이 있지만, Dart는 C#의 소멸자처럼 직접 정의하는 방식을 일반적으로 사용하지 않습니다. 메모리 정리는 런타임의 가비지 컬렉션(GC) 이 담당합니다.
즉, Dart에서는 “개체가 더 이상 사용되지 않을 때 메모리가 정리된다” 정도로 이해하면 충분합니다. 초급 단계에서는 소멸자를 직접 다룰 일이 거의 없습니다.
메서드(Method)
메서드는 클래스가 수행하는 동작입니다.
자동차의 예를 들면 전진, 후진, 정지 같은 동작이 메서드가 될 수 있습니다.
class Car {
String name;
Car(this.name);
void run() {
print('$name 자동차가 달립니다.');
}
}
속성(Property)
C#의 get, set 속성과 비슷한 개념을 Dart에서도 사용할 수 있습니다.
Dart에서는 필드를 직접 공개할 수도 있고, 필요하면 getter/setter를 통해 제어할 수도 있습니다.
class Person {
String _name = '';
String get name => _name;
set name(String value) => _name = value;
}
Dart에서는 앞에 밑줄 _ 이 붙은 이름은 라이브러리 내부 전용(private 비슷한 개념) 으로 사용됩니다.
인덱서(Indexer)
C#처럼 this[index] 형태의 인덱서를 Dart에서 직접 만드는 문법은 없습니다.
대신 List를 필드로 두고 메서드나 getter/setter로 비슷한 동작을 만들 수 있습니다.
예를 들어:
class CarCatalog {
List<String> names = ['1번 자동차', '2번 자동차'];
String getByIndex(int index) => names[index];
}
대리자(Delegate)
C#의 대리자(delegate)는 메서드를 값처럼 다룰 수 있는 기능입니다. Dart에서도 함수는 일급 개체(first-class object) 이므로 변수에 저장하거나 매개변수로 전달할 수 있습니다. 즉, Dart에서는 대리자라는 키워드 대신 함수 타입이나 콜백(callback) 으로 이해하면 됩니다.
void hello() {
print('Hello');
}
void execute(void Function() action) {
action();
}
void main() {
execute(hello);
}
이벤트(Event)
Dart에는 C#의 event 키워드와 완전히 같은 문법은 없지만, 콜백이나 Stream 등을 사용해 비슷한 흐름을 만들 수 있습니다.
초급 단계에서는 “어떤 일이 발생했을 때 실행할 함수를 연결하는 방식” 정도로 이해하면 됩니다.
네임스페이스(Namespace)
C#에는 namespace가 있지만, Dart는 대신 라이브러리와 import 구조를 사용합니다.
파일 단위 분리와 import 문을 통해 이름 충돌을 줄입니다.
인터페이스(Interface)
Dart에는 interface 키워드가 따로 없지만, 모든 클래스는 암묵적으로 인터페이스 역할을 할 수 있습니다.
보통 implements 키워드를 사용하여 인터페이스처럼 활용합니다.
class Animal {
void cry() {}
}
class Dog implements Animal {
@override
void cry() {
print('멍멍');
}
}
메타데이터(Annotation)
C#의 특성과 비슷하게, Dart에도 클래스나 메서드에 설명을 덧붙이는 애너테이션(annotation) 개념이 있습니다.
예를 들어 @override가 대표적입니다.
개체 지향 프로그래밍의 4가지 큰 개념
개체 지향 프로그래밍(OOP)의 핵심 개념은 다음 4가지입니다.
- 추상화(Abstraction)
- 캡슐화(Encapsulation)
- 상속(Inheritance)
- 다형성(Polymorphism)
이번 장에서는 흔히 말하는 캡, 상, 추, 다를 Dart 기준으로 맛보기 정리해 보겠습니다.
캡슐화(Encapsulation)
캡슐화는 데이터와 기능을 하나로 묶고, 내부 구현을 외부에서 함부로 건드리지 못하게 숨기는 개념입니다.
Dart에서는 보통 _name처럼 밑줄을 붙여 내부 데이터를 숨기고, getter/setter나 메서드를 통해 접근하게 합니다.
즉,
- 필드는 숨기고
- 필요한 통로만 공개하는 것
이것이 캡슐화입니다.
상속(Inheritance)
상속은 부모 클래스의 특징을 자식 클래스가 물려받는 것입니다.
예를 들어 Animal 클래스를 만들고, Dog, Cat이 이를 상속받으면 공통 기능을 재사용할 수 있습니다.
Dart에서는 extends 키워드를 사용합니다.
추상화(Abstraction)
추상화는 복잡한 것에서 핵심만 뽑아 공통된 틀을 만드는 것입니다.
예를 들어 동물마다 우는 방식은 다르지만 “동물은 운다”는 공통 규칙을 정의할 수 있습니다. 이런 공통 규칙을 추상 클래스로 만들면 자식 클래스들이 이를 구현하게 할 수 있습니다.
Dart에서는 abstract class를 사용합니다.
다형성(Polymorphism)
다형성은 같은 이름의 기능이 상황에 따라 다르게 동작하는 성질입니다.
예를 들어 Animal 타입으로 Dog, Cat을 받아도 실제 실행되는 cry()는 서로 다를 수 있습니다.
이것이 바로 다형성입니다.
캡슐화를 사용하여 좀 더 세련된 프로그램 만들기
캡슐화를 사용하면 프로그램을 더 세련되고 안전하게 만들 수 있습니다. 이번에는 필드를 숨기고, 외부에서는 메서드나 속성을 통해서만 접근하는 예제를 DartPad에서 실행해 보겠습니다.
다음 코드를 입력한 뒤 실행해 보세요.
코드: EncapsulationNote.dart
// 캡슐화(Encapsulation): 필드는 숨기고, 필요한 통로만 공개하자.
class Person {
// [1] 필드: 라이브러리 내부 전용(private 비슷한 역할)
String _name = '';
// [2] 메서드: public 메서드 역할
void setName(String n) {
_name = n;
}
String getName() {
return _name;
}
}
void main() {
// [A] person 개체 생성
var person = Person();
// [B] Set 메서드로 필드 설정
person.setName('Dart');
// [C] Get 메서드로 필드 값 확인
print(person.getName());
}
Dart
위 코드에서 _name은 외부에서 직접 건드리지 않고, setName()과 getName()을 통해서만 접근합니다.
이렇게 하면 내부 데이터를 더 안전하게 보호할 수 있습니다.
조금 더 Dart답게 getter/setter 문법을 쓰면 다음처럼 작성할 수도 있습니다.
class Person {
String _name = '';
String get name => _name;
set name(String value) => _name = value;
}
void main() {
var person = Person();
person.name = 'Flutter';
print(person.name);
}
이 방식은 Flutter 코드에서도 자주 보게 됩니다.
다형성 기법을 통한 프로그램의 융통성 높이기
개체 지향 프로그래밍의 다형성을 사용하면 프로그램의 융통성이 높아집니다. 즉, 같은 방식으로 코드를 작성하되, 실제로는 다양한 형태의 개체를 받아 서로 다른 동작을 하게 만들 수 있습니다.
이번에는 다형성을 Dart로 구현해 보겠습니다.
다음 내용을 입력한 뒤 실행해 보세요.
코드: PolymorphismDemo.dart
// 다형성: 같은 메서드 호출이지만 실제 동작은 전달된 개체에 따라 달라짐
// [1] Animal 클래스: 추상 클래스 및 기본 클래스
abstract class Animal {
String cry();
}
// [2] Dog 클래스
class Dog extends Animal {
@override
String cry() => '멍멍멍';
}
// [3] Cat 클래스
class Cat extends Animal {
@override
String cry() => '야옹';
}
// [4] Trainer 클래스
class Trainer {
void doCry(Animal animal) {
print(animal.cry());
}
}
void main() {
// [A] 기본 개체 생성 방법
print(Dog().cry());
print(Cat().cry());
// [B] 부모 클래스 변수로 개체 생성
Animal dog = Dog();
print(dog.cry());
Animal cat = Cat();
print(cat.cry());
// [C] 다형성 테스트
var trainer = Trainer();
trainer.doCry(Dog());
trainer.doCry(Cat());
}
멍멍멍
야옹
멍멍멍
야옹
멍멍멍
야옹
코드 설명
Animal 클래스는 추상 클래스입니다.
이 클래스는 “동물이라면 cry() 기능이 있어야 한다”는 틀만 제공합니다.
Dog, Cat 클래스는 Animal을 상속받아 각각 자신만의 cry()를 구현합니다.
Trainer 클래스의 doCry() 메서드는 Animal 형식 하나만 받습니다.
하지만 실제로 전달되는 값이 Dog()인지 Cat()인지에 따라 다른 결과가 출력됩니다.
즉,
- 코드는 하나
- 실제 동작은 여러 형태
이것이 다형성입니다.
추상화를 사용하여 공통 규칙 만들기
앞의 다형성 예제는 사실 추상화도 함께 보여주고 있습니다. 이번에는 추상화만 따로 더 짧게 정리해 보겠습니다.
코드: AbstractionDemo.dart
abstract class RemoteControl {
void turnOn();
void turnOff();
}
class TVRemote extends RemoteControl {
@override
void turnOn() {
print('TV 전원을 켭니다.');
}
@override
void turnOff() {
print('TV 전원을 끕니다.');
}
}
void main() {
RemoteControl remote = TVRemote();
remote.turnOn();
remote.turnOff();
}
TV 전원을 켭니다.
TV 전원을 끕니다.
추상화는 이런 식으로 “무엇을 해야 하는가”를 먼저 정하고, “어떻게 할 것인가”는 자식 클래스에서 구현하게 만드는 개념입니다.
Flutter에서도 이런 설계 감각이 중요합니다. 예를 들어 공통 동작을 추상화해 두면 다양한 위젯이나 기능 클래스들이 같은 규칙을 따르도록 만들 수 있습니다.
상속을 사용하여 코드 재사용하기
상속은 공통된 속성과 기능을 부모 클래스에 두고, 자식 클래스에서 재사용하는 방법입니다.
코드: InheritanceDemo.dart
class Animal {
String name;
Animal(this.name);
void eat() {
print('$name 이(가) 먹습니다.');
}
}
class Dog extends Animal {
Dog(String name) : super(name);
void bark() {
print('$name 이(가) 멍멍 짖습니다.');
}
}
void main() {
var dog = Dog('초코');
dog.eat();
dog.bark();
}
초코 이(가) 먹습니다.
초코 이(가) 멍멍 짖습니다.
위 예제에서 Dog는 Animal을 상속받아 eat() 기능을 그대로 재사용합니다.
그리고 자신만의 bark()도 추가합니다.
이것이 상속의 기본적인 모습입니다.
Dart에서의 오버라이드와 오버로드
C#을 배우던 흐름을 이어서 정리하면, 다형성과 관련된 핵심 개념으로 오버라이드가 있습니다.
오버라이드(Override)
부모 클래스의 메서드를 자식 클래스에서 다시 정의하는 것입니다.
Dart에서는 @override를 붙여서 명확하게 표현합니다.
class Animal {
void sound() {
print('소리');
}
}
class Dog extends Animal {
@override
void sound() {
print('멍멍');
}
}
void main() {
Animal animal = Dog();
animal.sound();
}
오버로드(Overload)
C#에서는 같은 이름의 메서드를 매개변수만 다르게 여러 개 만들 수 있는 메서드 오버로드가 있습니다. 하지만 Dart는 C#처럼 전통적인 메서드 오버로드를 지원하지 않습니다.
대신 다음과 같은 방식으로 비슷한 효과를 냅니다.
- 선택적 매개변수
- 이름 있는 매개변수
- 기본값
예를 들면 다음과 같습니다.
void greet([String name = '손님']) {
print('안녕하세요, $name');
}
void main() {
greet();
greet('Dart');
}
안녕하세요, 손님
안녕하세요, Dart
즉, Dart에서는 C#식 오버로드 대신 매개변수 설계 방식으로 융통성을 만듭니다.
클래스의 멤버 종합 연습: 자동차 클래스 구현하기
이번에는 자동차 클래스를 만들고 지금까지 배운 여러 요소를 한 번에 정리해 보겠습니다. C# 원문의 긴 종합 예제를 Dart 스타일로 바꾼 버전입니다.
Dart는 C#과 문법이 다르므로 완전히 동일한 기능을 1:1로 옮기기보다는, Dart OOP 학습에 맞게 핵심 요소를 정리하는 방식으로 구성했습니다.
다음 내용을 DartPad에 입력한 뒤 실행해 보세요.
코드: CarWorld.dart
// [1] 추상 클래스: 공통 규칙
abstract class Vehicle {
void run();
}
// [2] 클래스: 설계도
class Car extends Vehicle {
// [3] 필드
String _name;
final List<String> _names;
// [4] 생성자
Car() : _name = '좋은차', _names = [];
Car.withName(String name)
: _name = name,
_names = [];
Car.withLength(int length)
: _name = '좋은차',
_names = List.filled(length, '');
// [5] 속성(getter/setter)
String get name => _name;
set name(String value) => _name = value;
int get length => _names.length;
// [6] 메서드
@override
void run() {
print('$_name 자동차가 달립니다.');
}
// [7] 인덱서 비슷한 기능을 메서드로 흉내내기
String getByIndex(int index) => _names[index];
void setByIndex(int index, String value) {
_names[index] = value;
}
// [8] 반복용 getter
List<String> get items => _names;
// [9] 콜백(이벤트 비슷한 구조)
void Function()? click;
// [10] 이벤트 처리기 비슷한 메서드
void onClick() {
if (click != null) {
click!();
}
}
}
void main() {
// [A] 클래스, 생성자, 메서드 테스트
var campingCar = Car.withName('캠핑카');
campingCar.run();
// [B] 속성 테스트
var sportsCar = Car();
sportsCar.name = '스포츠카';
sportsCar.run();
// [C] 인덱스 방식 데이터 저장 테스트
var cars = Car.withLength(2);
cars.setByIndex(0, '1번 자동차');
cars.setByIndex(1, '2번 자동차');
for (int i = 0; i < cars.length; i++) {
print(cars.getByIndex(i));
}
// [D] 반복 테스트
for (final name in cars.items) {
print(name);
}
// [E] 콜백 테스트
var btn = Car.withName('전기자동차');
btn.click = btn.run;
btn.onClick();
}
캠핑카 자동차가 달립니다.
스포츠카 자동차가 달립니다.
1번 자동차
2번 자동차
1번 자동차
2번 자동차
전기자동차 자동차가 달립니다.
종합 예제 설명
이 예제에는 다음 내용이 들어 있습니다.
1. 추상 클래스
Vehicle은 공통 규칙을 제시합니다.
“탈것이라면 run() 기능이 있어야 한다”는 틀만 제공합니다.
2. 클래스
Car는 실제 자동차 설계도 역할을 합니다.
3. 필드
_name, _names는 내부 데이터입니다.
특히 _name은 외부에서 직접 건드리지 않도록 숨긴 값입니다.
4. 생성자
Dart는 이름 있는 생성자(named constructor)를 자주 사용합니다.
Car()Car.withName(...)Car.withLength(...)
이 방식은 Dart 코드에서 매우 흔합니다.
5. 속성
getter/setter를 통해 _name에 안전하게 접근합니다.
6. 메서드
run()은 자동차의 동작을 나타냅니다.
7. 인덱서 비슷한 처리
Dart에는 C#식 인덱서가 없으므로 메서드로 대체했습니다.
8. 반복
items를 꺼내 for-in 문으로 반복할 수 있습니다.
9. 대리자/이벤트 비슷한 처리
C#의 delegate/event 대신, Dart에서는 함수 타입 필드나 콜백을 사용합니다.
DartPad에서 단계별로 실습하는 추천 순서
Flutter 입문 전에 Dart OOP를 맛보기로 익히려면 다음 순서대로 실습하면 좋습니다.
1단계: 클래스와 개체
먼저 Car 같은 클래스를 만들고 개체를 생성해 봅니다.
class Car {
String name;
Car(this.name);
void run() {
print('$name 자동차가 달립니다.');
}
}
void main() {
var car = Car('연습차');
car.run();
}
2단계: 캡슐화
필드를 숨기고 getter/setter 또는 메서드로만 접근합니다.
3단계: 상속
Animal → Dog처럼 공통 기능을 부모에 두고 자식이 재사용하게 합니다.
4단계: 추상화
abstract class로 공통 규칙을 정리합니다.
5단계: 다형성
부모 타입 하나로 여러 자식 개체를 받아 서로 다른 동작을 확인합니다.
Flutter 학습과 연결해서 보기
Dart의 개체 지향 프로그래밍은 Flutter와 매우 밀접합니다.
예를 들어 Flutter에서는
- 위젯도 클래스이고
- 화면도 클래스이고
- 상태 관리도 클래스나 개체를 많이 사용하며
- 공통 기능을 추상화하거나 상속/구성을 통해 재사용합니다
따라서 Dart OOP를 가볍게라도 이해하고 Flutter로 넘어가면 훨씬 수월합니다.
특히 다음 감각이 중요합니다.
- 클래스는 설계도
- 개체는 실제 사용 값
- 필드는 상태
- 메서드는 동작
- 추상화는 공통 규칙
- 상속은 재사용
- 캡슐화는 보호
- 다형성은 유연성
장 요약
개체 지향 프로그래밍의 목적은 프로그램을 좀 더 구조적으로 만들고, 재사용성과 유연성을 높이는 데 있습니다. Dart 역시 이러한 개체 지향 기능을 충실히 제공하며, Flutter 개발의 기초가 되는 언어답게 클래스 중심의 설계가 매우 중요합니다.
이번 장에서는 Dart 기준으로 다음 내용을 가볍게 정리했습니다.
- 캡슐화: 필드를 숨기고 필요한 통로만 공개
- 상속: 부모 클래스의 기능 재사용
- 추상화: 공통 규칙 정의
- 다형성: 같은 형식으로 다양한 개체 처리
앞으로 Flutter를 학습하면서도 이 네 가지 개념은 계속 등장하게 됩니다. 지금은 완벽히 외우기보다는, 예제를 직접 실행해 보면서 “아, 이런 느낌이구나” 정도로 감각을 잡는 것이 가장 중요합니다.