본문 바로가기

프로그래밍 언어 기초/Dart

[Dart] Dart 기본 문법3 [클래스, 생성자, 오버라이딩, static, 추상, 제네릭]

개요

오늘도 이어서 Flutter의 프로그래밍 언어인 Dart의 문법에 대해서 알아보겠습니다. 

오늘의 포스팅에서 소개할 문법은 아래와 같습니다. 

  • 클래스와 인스턴스
  • 클래스의 생성, 프로퍼티와 메서드
  • 생성자와 생성자, 그리고 오버로딩
  • 상속과 오버라이딩
  • private와 getter setter
  • 정적 변수 static
  • 추상과 인터페이스
  • 믹스인(Mixin)
  • 제네릭(Generic)
  • 케스케이드 연산자(Casecade Operator)

 

이중에서 특히 믹스인(Mixin)과 케스케이드 연산자는 dart 문법의 특징 중 하나이므로 잘 알아두시면 좋을 것 같습니다.


1. dart의 클래스와 인스턴스

객체지향 프로그래밍 언어에서 변수와 함수의 집합으로 구성된 데이터의 모음을 클래스 또는 객체 라고 부릅니다.

객체지향 프로그래밍에서 이러한 클래스는 모든 프로그램 설계의 기초가 되는 중요한 재료이며,

각각의 클래스는 유기적으로 상호 협력하여 개발자가 목적한 프로그램의 작업을 수행하게 됩니다.

 

그리고 이러한 클래스가 메인 함수와 같은 프로그램의 '진입점' 혹은 '시작 지점'에서,

변수를 매개체로 하여 사용 가능한 '도구'가 되었을 때, 그 변수에 담긴 클래스 유형으로 '인스턴스화(instantiate)' 되었다고 말하며,

이 변수에 담긴 데이터의 모음 즉 클래스의 어떤 '실체'를 인스턴스(instance)라고 부릅니다.

 

dart에서 클래스의 선언은 아래와 같이 할 수 있으며,

한층 더 구조적인 프로그래밍을 위해 본 포스팅에서는 같은 패키지 내,

서로 다른 위치에 저장된 클래스를 import 하여 사용하는 상황을 가정하고 예제 코드를 구성하였습니다.

 

 

// 패키지 정보 : ./class/Car.dart 

class Car{
    int speed;
    String name;

    Car([int speed = 0, String name = ''])
        : this.speed = speed,
          this.name = name;

    void drive(){
        print('Car($name) is driving...');
        print('Current speed is $speed');
    }
}

 

1.2. 프로퍼티와 메서드

위의 Car 클래스와 같이 dart의 클래스는 자신의 프로퍼티(=변수) 및 메서드를 자유롭게 선언할 수 있으며,

클래스의 인스턴스화를 시켜주는 '생성자' 메서드는 클래스 당 '하나'만 사용할 수 있습니다.

 

1.3. 생성자와 생성자 오버로딩

다른 프로그래밍 언어에서는 대부분 생성자와 메서드는 오버로딩(Overloading),

즉 같은 이름을 가진 함수가 여러개 존재할 수 있는데,

dart에서는 클래스당 같은 이름을 가진 생성자와 함수는 하나로 제한됩니다.

 

// 해당 패키지의 루트 경로에 위치한 메인 함수 = 프로그램의 시작점!

import './class/Car.dart';

void main(){
    // basic class
    Car car = Car(30, "Sonata");
    car.drive();
    // Car(Sonata) is driving...
    // Current speed is 30
}

 

 


2. 상속(Inheritance)과 오버라이딩(Overriding)

사람을 의미하는 People 클래스와 학생을 의미하는 Stutent라는 클래스가 있다고 가정하겠습니다.

이 두 클래스는 학생은 사람이다 라는 '관계(relationship)'을 형성하고 있으므로,  

People을 부모 클래스로(parent class), Student를 자식 클래스로 하여 아래와 같이 설계할 수 있습니다.

 

class People{
    String _name = '';
    int _age = 0;

    People({String name = '', int age = 0})
        : this._name = name,
          this._age = 0;
          
    void sayName(){
        print('My name is ${this._name}');
    }

    // getter, setter
    String get name {
        return this._name;
    }
    int get age {
        return this._age;
    }

    set name(String name){
        this._name= name;
    }
    set age(int age){
        this._age = age;
    }
}

 

 

import "People.dart";

class Student extends People {
    int grade;
    String className;

    Student({
        String name = '',
        int age = 0,
        int grade = 1,
        String className = ''
    }):this.grade = grade,
       this.className = className,
       super(name:name, age:age);

    // 오버라이딩
    @override
    void sayName(){
        // super.sayName();
        print('Hi My name is ${super.name}');
    }

    void sayClass(){
        print('My Class is ${className}');
    }

}

 

 

부모 클래스인 People를 상속 받는 Student 클래스는

이름(name)과 나이(age)그리고 학년(grade)과 반 이름(className)이라는 프로퍼티를 가진다고 했을 때,

위와 같이 생성자에서 이름과 나이에 대한 입력 값은 부모 클래스를 호출하여 초기화할 수 있고,

자신에게만 즉, Student 클래스의 고유한 프로퍼티인 grade는 this를 사용하여 자신의 생성자에서 초기화할 수 있습니다.

 

이때, 자식 클래스인 Student가 부모 클래스의 '생성자(constructor)'를 호출할 때에는 'super'라는 키워드를 통해 호출할 수 있으며, 이 때에는 부모 클래스에 선언된 부모 클래스의 생성자(constructor)가 호출됩니다.

 

그리고, 자식클래스에서 이처럼 자신의 프로퍼티를 초기화하고 부모의 생성자를 통해 프로퍼티를 초기화 하려면,

자식 클래스의 생성자에서 필요로하는 초기화 작업을 먼저 수행하고 쉼표(,)를 통해 구분한 다음

가장 마지막에 super 키워드를 통해서 부모 클래스의 생성자를 호출해야 합니다.

 

만일 이 순서가 올바르지 않게 작성된다면, dart의 클래스는 컴파일 시에 오류를 발생시킵니다.

 

 

2.1. private와 getter setter

 

객체지향 프로그래밍 언어에서 어떤 클래스에 있는 변수 즉,

프로퍼티는 해당 클래스가 인스턴스화된 인스턴스에서 사용할 수 있습니다.

 

다시 말하자면, 어떤 변수 V1에 클래스 A의 인스턴스가 담겼다면

V1 변수를 통해 A 인스턴스의 프로퍼티를 다룰 수 있다는 것을 의미합니다.

 

dart 클래스에서도 객체지향 프로그래밍의 중요한 개념 중 하나인 캡슐화(encapsulation)를 지원하는데,

프로퍼티 이름 앞에 '_' 를 추가하여 접근을 제한하고, getter/setter를 통해 데이터 은닉을 할 수 있습니다.

 

class People{
    String _name = '';
    int _age = 0;

    People({String name = '', int age = 0})
        : this._name = name,
          this._age = 0;
          
    void sayName(){
        print('My name is ${this._name}');
    }

    // getter, setter
    String get name {
        return this._name;
    }
    int get age {
        return this._age;
    }

    set name(String name){
        this._name= name;
    }
    set age(int age){
        this._age = age;
    }
}

 

일반적인 클래스에서 프로퍼티 앞에 _가 붙은 것은 해당 클래스를 인스턴스로 만들더라도,

변수 등에서 사용할 수 없으며, 위의 샘플 코드 중에서는 People 클래스의 _name 프로퍼티가 이에 해당합니다.

 

private 된 프로퍼티는 getter setter를 통해서 값을 읽고 쓸 수 있습니다.

 

 

2.2 정적 변수 static

 

이처럼 클래스 내의 변수는 그 목적에 따라 다양하게 접근을 제한할 수 있습니다.

private처럼 외부에서는 변수를 사용하지 못하게 하는 방법도 있지만,

어느 곳에서나 함께 사용할 수 있도록 개방된 프로퍼티도 만들 수 있습니다.

 

이처럼 클래스의 인스턴스에 귀속되지 아니하고, 클래스 자체에 귀속된 변수를 static 변수라고 하며

이 변수는 클래스의 프로퍼티 앞에 static 키워드를 붙여 만들 수 있습니다.

 

또한, 이러한 static 변수는 어디서나 접근할 수 있다는 특징을 가지므로,

민감한 데이터에 대해서는 static으로 다루는 것이 권장되지 않습니다.

 

class People{
    String _name = '';
    int _age = 0;

    static int total = 0; // static 정적 변수

    People({String name = '', int age = 0})
        : this._name = name,
          this._age = 0 {
         total++;
  }
    void sayName(){
        print('My name is ${this._name}');
    }

    // getter, setter
    String get name {
        return this._name;
    }
    int get age {
        return this._age;
    }

    set name(String name){
        this._name= name;
    }
    set age(int age){
        this._age = age;
    }

}

 

import "People.dart";

class Student extends People {
    int grade;
    String className;

    Student({
        String name = '',
        int age = 0,
        int grade = 1,
        String className = ''
    }):this.grade = grade,
       this.className = className,
       super(name:name, age:age);

    // 오버라이딩
    @override
    void sayName(){
        // super.sayName();
        print('Hi My name is ${super.name}');
    }

    // 생략 가능!
    void sayClass(){
        print('My Class is ${className}');
    }

}

 

import './class/People.dart';
import './class/Student.dart';

void main(){

    Student student1 = Student(className:'Star');
    student1.sayName();
    student1.sayClass();

    Student student2 = Student(className:'Moon');
    student2.sayName();
    student2.sayClass();

    print('total student num is ${People.total}'); // total student num is 2

}

 

 

위 코드의 메인함수에서 Student 클래스는 People 클래스를 상속 받고 있는데,

자기 자신의 생성자를 호출하면서 부모 클래스의 생성자를 호출하고 있으므로

메인 함수에서 전체 total 변수의 값은 2가 됩니다.

 


3. 추상 클래스와 인터페이스

dart에서 함수의 몸체 부분을 미리 정하지 아니하고,

상속과 동시에 구현을 강제할 수 있는 방법은 추상 클래스(Abtract Class)와 인터페이스(Interface)가 있습니다.

그런데, dart에서는 Java 등과 같이 인터페이스를 interface라는 키워드로 별도로 구분하지 않습니다.

 

따라서, dart에서 추상 클래스를 의미하는 abstract 키워드로 추상 클래스와 인터페이스를 모두 선언할 수 있으며,

추상 클래스를 '상속(Inheritance)'하기 위해서는 extends를 사용하고,

인터페이스로서 '구현(Implement)'할 때에는 implements를 사용합니다.

 

동일하게 abstract 키워드를 통해 선언한 클래스를 사용하더라도,

상속 또는 구현하는 클래스에서 그 클래스가 '추상 클래스(Abstract Class)' 인지 '인터페이스(Interface)'인지 결정됩니다.

 

dart에서 추상클래스의 상속(Inheritance)은 아래와 같이 할 수 있습니다.

 

abstract class BaseCar{
    int speed = 0;
    void drive();
}

 

import 'BaseCar.dart';

class SuperCar extends BaseCar {
    @override
    int speed;

    SuperCar([int speed = 0]): this.speed = speed;

    @override
    void drive(){
        print('brrrrrrrroooommm');
    }

}

 

import './class/SuperCar.dart';

void main(){
    SuperCar superCar = SuperCar(10);
    superCar.drive(); // brrrrrrrroooommm

}

 

 

dart에서 인터페이스의 구현(Implement)는 아래와 같이 할 수 있습니다.

 

import 'People.dart';
import 'BaseCar.dart';

class Employee extends People implements BaseCar {// BaseCar를 Interface로써 사용!

    @override
    int speed;

    Employee({
        String name = '',
        int age = 0,
        int speed = 0
    }): this.speed = speed, super(name:name, age:age);

    @override
    void drive(){
        print('broom');
    }

    /*
    Error: 'drive' is already declared in this scope.
    void drive(int speed){
        print('broom speed : $speed');
    }
    */
}

 

import './class/Employee.dart';

void main(){
    Employee employee = Employee();
    employee.drive();
    // employee.drive(2); // Error: 'drive' is already declared in this scope;
}

 

 

dart에서는 메서드의 오버로딩(overloading)을 지원하지 않습니다.

따라서, 파라미터의 타입과 개수에 따라 함수가 서로 구분이 되더라도,

같은 이름을 갖는 함수를 하나만 사용할 수 있습니다.

 


4. 믹스인(Mixin)

믹스인(mixin)은 특정 클래스에 있는 기능 중 원하는 기능을 골라 넣을 수 있는 기능입니다.

특정 클래스를 지정해서 속성들을 정의할 수 있으며, 지정한 클래스를 상속하는 클래스에서도 사용할 수 있습니다.

 

믹스인의 가장 간단한 용례는 아래와 같습니다.

 

// 믹스인에서 기능을 추출할 추상 클래스

abstract class BaseCar{
    int speed = 0;
    void drive();
}

 

import "BaseCar.dart";

// 추상 클래스로부터 drive라는 기능을 오버라이딩한 믹스인
mixin Engine on BaseCar {
    void drive(){
        print('super engine brrrroooom');
    }
}

 

import "Engine.dart";
import "BaseCar.dart";

// 믹스인을 사용하는 클래스,
// 믹스인에서 기능을 추출하는데 사용한 클래스 BaseCar가 추상클래스이므로
// 믹스인을 사용하기 위한 클래스에서도 이를 상속 받아야 해서 아래와 같이 작성
class ClassicCar extends BaseCar with Engine {


}

 

import './class/ClassicCar.dart';

void main(){
    // 믹스인
    ClassicCar sCar = ClassicCar();
    sCar.drive(); // super engine brrrroooom
}

 


5. 제네릭(Generic)

dart에서는 클래서 생성시 내부의 프로퍼티나 함수의 리턴타입을 미리 정하지 아니하고,

인스턴스 생성시에 결정하는 제네릭(Generic)을 지원합니다.

제네릭은 <> 사이에 알파벳 대문자로 표기하며, 꼭 지켜야할 문법이 있는 것은 아니지만,

보통 아래와 같은 기준으로 제네릭을 구분하여 사용합니다.

 

제네릭 설명
T 변수 타입을 표현할 때,
E 리스트와 같은 컬레션 변수의 요소 등의 타입을 정할 때,
K 주로 Map 자료형에서 Key 값
V 주로 Map 자료형에서 Value 값

 

제네릭(Generic)은 아래와 같이 사용할 수 있습니다.

 

class Artist<T> {
    final T genre;
    Artist({
        required this.genre,
    });
}

 

import './class/Artist.dart';


void main(){
    // 제네릭
    Artist artist = Artist<int>(genre:1); // 인스턴스 생성시 타입을 정함!
    print('genre is ${artist.genre}'); // genre is 1

}

 

 


6. 케스케이드 연산자(Cascade Operator)

dart에서는 클래스에 속한 여러 메서드를 인스턴스 생성 후 연속해서 사용할 수 있습니다.

클래스 생성후에 클래스의 메서드를 연속적으로 사용하기 위한

연산자를 케스케이드 연산자(Casecade Operator)라고 하며 아래와 같이 사용합니다.

 

class People{
    String _name = '';
    int _age = 0;

    static int total = 0;

    People({String name = '', int age = 0})
        : this._name = name,
          this._age = 0 {
         total++;
  }
    void sayName(){
        print('My name is ${this._name}');
    }

    // getter, setter
    String get name {
        return this._name;
    }
    int get age {
        return this._age;
    }

    set name(String name){
        this._name= name;
    }
    set age(int age){
        this._age = age;
    }

}

 

import "People.dart";

class Student extends People {
    int grade;
    String className;

    Student({
        String name = '',
        int age = 0,
        int grade = 1,
        String className = ''
    }):this.grade = grade,
       this.className = className,
       super(name:name, age:age);

    // 오버라이딩
    @override
    void sayName(){
        // super.sayName();
        print('Hi My name is ${super.name}');
    }

    // 생략 가능!
    void sayClass(){
        print('My Class is ${className}');
    }

}

 

 

먼저 위와 같이 People 클래스를 상속받은 Student 클래스는

총 메서드가 2개 구현되어 있습니다.

 

이러한 경우 메인 함수에서 Student 클래스의 인스턴스는

아래와 같이 케스케이드 연산자(Casecade Operator)를 사용할 수 있습니다.

 

import './class/Student.dart';

void main(){
    // Casecade Operator
    Student student3 = Student(name:'Jhon', className:'Sun')
        ..sayName() // Hi My name is Jhon
        ..sayClass(); // My Class is Sun
}