[Flutter] 다트 언어 마스터하기 – 다트 객체지향 프로그래밍 ❷

이 글은 [Must Have] 코드팩토리의 플러터 프로그래밍 2판에서 발췌했습니다.
골든래빗 출판사
코드팩토리(최지호) 지음

플러터(Flutter)는 다트(Dart) 언어를 사용합니다. 대부분의 플러터 입문자는 별도의 책으로 다트를 공부하지 않고 플러터 서적에서 1개 장 분량으로 얕게 배웁니다. 플러터로 앱을 원활히 개발하려면 다트를 탄탄하게 아는 것이 중요합니다. 그래서 이 책은 또 다른 자료를 찾아보지 않아도 될 정도로 깊이 있게 다트를 다룹니다.

1장에서 다트 입문하기, 2장에서 객체지향 프로그래밍, 3장에서 비동기 프로그래밍, 4장은 다트 3.0 신규 문법을 학습합니다.

 

다트 객체지향 프로그래밍 ❷

다트 언어는 높은 완성도로 객체지향 프로그래밍을 지원합니다. 플러터 역시 객체지향 프로그래밍(Object-oriented programming, OOP) 중심으로 설계된 프레임워크입니다. 따라서 객체지향 프로그래밍을 알면 좋은 코드를 작성하는 데 유리합니다. 2장에서는 객체지향 프로그래밍의 기초부터 강력하고 유용한 기능까지 알아보겠습니다.

2편은 인터페이스, 믹스인, 제네릭, 스태틱, 캐스케이드 연산자를 정리했습니다.

 

5. 인터페이스

상속은 공유되는 기능을 이어받는 개념이지만 인터페이스(Interface)는 공통으로 필요한 기능을 정의만 해두는 역할을 합니다. ‘3. 상속’에서 사용한 Idol 클래스를 인터페이스로 사용하겠습니다. 다트에는 인터페이스를 지정하는 키워드가 따로 없습니다. 상속은 단 하나의 클래스만 할 수 있지만 인터페이스는 적용 개수에 제한이 없습니다. 여러 인터페이스를 적용하고 싶으면 , 기호를 사용하여 인터페이스를 나열해 입력해주면 됩니다.

class Idol {
  final String name;
  final int membersCount;

  Idol(this.name, this.membersCount);

  void sayName() {
    print('저는 ${this.name}입니다.');
  }

  void sayMembersCount() {
    print('${this.name} 멤버는 ${this.membersCount}명입니다.');
  }
}

// ❶ implements 키워드를 사용하면 원하는 클래스를 인터페이스로 사용할 수 있습니다.
class GirlGroup implements Idol {
  final String name;
  final int membersCount;

  GirlGroup(
      this.name,
      this.membersCount,
      );

  void sayName() {
    print('저는 여자 아이돌 ${this.name}입니다.');
  }

  void sayMembersCount() {
    print('${this.name} 멤버는 ${this.membersCount}명입니다.');
  }
}

 

❶ GirlGroup 클래스는 Idol 클래스가 정의한 모든 기능을 다시 정의했습니다. ‘상속과 인터페이스가 뭐가 다르지’라는 생각이 들 겁니다. 상속받을 때는 부모 클래스의 모든 기능이 상속되므로 재정의할 필요가 없습니다. 반면 인터페이스는 반드시 모든 기능을 다시 정의해줘야 합니다. 귀찮아 보이지만 애초에 반드시 재정의할 필요가 있는 기능을 정의하는 용도가 인터페이스이기 때문입니다. 그렇게 하면 실수로 빼먹는 일을 방지할 수 있습니다.

인터페이스 사용법은 클래스와 같습니다.

void main() {
  GirlGroup redVelvet = GirlGroup('블랙핑크', 4);

  redVelvet.sayName();
  redVelvet.sayMembersCount();
}

 

▼ 실행 결과

 

6. 믹스인

믹스인(Mixin)은 특정 클래스에 원하는 기능들만 골라 넣을 수 있는 기능입니다. 특정 클래스를 지정해서 속성들을 정의할 수 있으며 지정한 클래스를 상속하는 클래스에서도 사용할 수 있습니다. 그리고 인터페이스처럼 한 개의 클래스에 여러 개의 믹스인을 적용할 수도 있습니다. 인터페이스와 마찬가지로 여러 믹스인을 적용하고 싶으면 , 기호로 열거하면 됩니다. 3. ‘상속’에서 사용한 Idol 클래스를 사용해 믹스인하는 방법을 알아보겠습니다.

 

class Idol {
  final String name;
  final int membersCount;

  Idol(this.name, this.membersCount);

  void sayName() {
    print('저는 ${this.name}입니다.');
  }

  void sayMembersCount() {
    print('${this.name} 멤버는 ${this.membersCount}명입니다.');
  }
}

mixin IdolSingMixin on Idol{
  void sing(){
    print('${this.name}이 노래를 부릅니다.');
  }
}

// 믹스인을 적용할 때는 with 키워드 사용
class BoyGroup extends Idol with IdolSingMixin{
  BoyGroup(
      super.name,
      super.membersCount,
      );

  void sayMale() {
    print('저는 남자 아이돌입니다.');
  }
}

void main(){
  BoyGroup bts = BoyGroup('BTS', 7);

  // 믹스인에 정의된 dance() 함수와 sing() 함수 사용 가능
  bts.sing();
}

 

▼ 실행 결과

 

7. 추상

추상(Abstract)은 상속이나 인터페이스로 사용하는 데 필요한 속성만 정의하고 인스턴스화할 수 없도록 하는 기능입니다. 인터페이스 예제와 같이 Idol 클래스를 인터페이스로 사용하고 Idol 클래스를 따로 인스턴스화할 일이 없다면, Idol 클래스를 추상 클래스로 선언해서 Idol 클래스의 인스턴스화를 방지하고 메서드 정의를 자식 클래스에 위임할 수 있습니다. 또한 추상 클래스는 추상 메서드를 선언할 수 있으며 추상 메서드는 함수의 반환 타입, 이름, 매개변수만 정의하고 함수 바디의 선언을 자식 클래스에서 필수로 정의하도록 강제합니다. Idol 추상 클래스를 작성하겠습니다.

 

// ❶ abstract 키워드를 사용해 추상 클래스 지정
abstract class Idol {
  final String name;
  final int membersCount;

  Idol(this.name, this.membersCount); // ❷ 생성자 선언

  void sayName();          // ❸ 추상 메서드 선언
  void sayMembersCount();  // ➍ 추상 메서드 선언
}

 

❶ abstract 키워드로 추상 클래스를 지정했습니다. ❷ 생성자를 비롯해 ❸과 ❹ 메서드도 선언만 하며 어떻게 동작하는지 정의가 없습니다. 이처럼 추상 클래스는 선언까지만 해주면 됩니다.

추상 클래스를 구현(Implements)하는 클래스를 만들겠습니다.

// implements 키워드를 사용해 추상 클래스를 구현하는 클래스
class GirlGroup implements Idol {
  final String name;
  final int membersCount;

  GirlGroup(
      this.name,
      this.membersCount,
      );

  void sayName() {
    print('저는 여자 아이돌 ${this.name}입니다.');
  }

  void sayMembersCount() {
    print('${this.name} 멤버는 ${this.membersCount}명입니다.');
  }
}

 

이제 생성자를 비롯해 모든 메서드를 정의해줬습니다. 하나라도 정의하지 않으면 에러가 납니다.

사용 방법은 클래스와 같습니다.

void main() {
  GirlGroup redVelvet = GirlGroup('블랙핑크', 4);

  redVelvet.sayName();
  redVelvet.sayMembersCount();
}

 

▼ 실행 결과

 

추상 메서드는 부모 클래스를 인스턴스화할 일이 없고, 자식 클래스들에 필수적 또는 공통적으로 정의돼야 하는 메서드가 존재할 때 사용됩니다. 프로그래밍 입문자라면 추상 클래스와 일반 클래스를 사용하는 적절한 상황을 쉽게 구분하기 어려울 수 있습니다. 지금은 ‘추상 클래스는 인스턴스화가 필요 없는 공통 부모 클래스를 만들 때 사용한다’ 정도로 이해하고 넘어가주세요.

 

8. 제네릭

개인적으로 제네릭(Generic)은 객체지향 프로그래밍에서 가장 아름다운 기능이라고 생각합니다. 제네릭은 클래스나 함수의 정의를 선언할 때가 아니라 인스턴스화하거나 실행할 때로 미룹니다. 특정 변수의 타입을 하나의 타입으로 제한하고 싶지 않을 때 자주 사용합니다. 예를 들어 정수를 받는 함수, 문자열을 받는 함수를 각각 setInt( ), setString( )처럼 따로 만들지 않아도, 제네릭을 사용해 set( ) 함수 하나로 여러 자료형을 입력받게 처리할 수 있습니다.

사실 제네릭에 대해 배우지도 않은 채 이미 제네릭을 많이 사용했습니다. Map, List, Set 등에서 사용한 < > 사이에 입력되는 값이 제네릭 문자입니다. 예를 들어 List<String>이라고 입력하면 String값들로 구성된 리스트를 생성하겠다는 뜻인데, List 클래스는 제네릭이므로 인스턴스화를 하기 전 어떤 타입으로 List가 생성될지 알지 못합니다. Map과 Set 또한 마찬가지입니다.

직접 코드로 구현하며 알아보겠습니다.

 

// 인스턴스화할 때 입력받을 타입을 T로 지정합니다.
class Cache<T> {
  // data의 타입을 추후 입력될 T 타입으로 지정합니다.
  final T data;

  Cache({
    required this.data,
  });
}

void main() {
  // T의 타입을 List<int>로 입력합니다.
  final cache = Cache<List<int>>(
    data: [1,2,3],
  );

  // 제네릭에 입력된 값을통해 data 변수의 타입이 자동으로 유추됩니다.
  // reduce() 함수가 기억나지 않는다면 1.3.1절 List타입의 reduce() 함수를 복습해보세요.
  print(cache.data.reduce((value, element) => value + element));
}

 

▼ 흔히 사용되는 제네릭 문자들

 

▼ 실행 결과

 

예제에서는 ‘T’를 사용해서 제네릭 타입을 표현했습니다. 어떤 문자를 사용해서 어떤 값을 표현해도 프로그램적으로 상관없지만 일반적으로 개발자들이 많이 사용하는 문자들이 존재합니다. 해당 문자들을 상단의 코드 옆에 표로 정리해두었습니다.

 

9. 스태틱

지금까지 작성한 변수와 메서드 등 모든 속성은 각 ‘클래스의 인스턴스’에 귀속되었습니다. 하지만 static 키워드를 사용하면 클래스 자체에 귀속됩니다.

 

class Counter{
  // ❶ static 키워드를 사용해서 static 변수 선언
  static int i= 0;


  // ❷ static 키워드를 사용해서 static 변수 선언
  Counter(){
    i++;
    print(i++);
  }
}

void main() {
  Counter count1 = Counter();
  Counter count2 = Counter();
  Counter count3 = Counter();
}

 

▼ 실행 결과

 

❶ 변수 i를 스태틱으로 지정했습니다(스태틱 변수 또는 정적 변수라고 부릅니다). Counter 클래스에 귀속되기 때문에 인스턴스를 호출할 때마다 1씩 증가합니다. ❷ 생성자에 this.i가 아니고 i로 명시했습니다. static 변수는 클래스에 직접 귀속되기 때문에 생성자에서 static값을 지정하지 못합니다. 결과적으로 static 키워드는 인스턴스끼리 공유해야 하는 정보에 지정하면 되겠습니다.

 

10. 캐스케이드 연산자

캐스케이드 연산자(Cascade Operator)는 인스턴스에서 해당 인스턴스의 속성이나 멤버 함수를 연속해서 사용하는 기능입니다. 캐스케이드 연산자는 .. 기호를 사용합니다. 자세한 방법은 다음 코드를 보면서 확인하겠습니다.

 

void main() {
 // cascade operator (..)을 사용하면
 // 선언한 변수의 메서드를 연속으로 실행할 수 있습니다.
 Idol blackpink= Idol('블랙핑크', 4)
   ..sayName()
   ..sayMembersCount();
}

 

▼ 실행 결과

 

이처럼 캐스케이드 연산자를 사용하면 더 간결한 코드를 작성할 수 있습니다.

 

다트 객체지향 프로그래밍 마무리

다트 언어가 제공하는 객체지향 프로그래밍 기법을 알아보았습니다. 기본적인 클래스 선언 방법부터 상속, 인터페이스, 믹스인, 제네릭 등을 사용하는 활용법을 배웠습니다. 플러터는 모든 위젯을 객체지향 프로그래밍을 통해 구현하기 때문에 이번 장을 제대로 이해하는 게 매우 중요합니다. 더나아가 프로젝트가 커질수록 객체지향 프로그래밍을 통해서 중복 코드를 줄이고 정리가 잘된 효율적인 코드를 작성하는 건 개발자의 필수 덕목이니 여러 번 반복해서 학습하길 바랍니다.

 

요약

  1. Class 키워드를 사용해서 클래스를 선언할 수 있습니다.
  2. 클래스를 인스턴스화하면 클래스의 인스턴스를 변수로 저장할 수 있습니다.
  3. 상속받으면 부모 클래스의 모든 속성을 물려받습니다.
  4. 오버라이드는 이미 선언되어 있는 속성을 덮어쓰는 기능입니다.
  5. 인터페이스는 클래스의 필수 속성들을 정의하고 강제할 수 있는 기능입니다.
  6. 믹스인은 상속처럼 모든 속성을 물려받지 않고 원하는 기능만 골라서 적용할 수 있습니다.
  7. 제네릭은 변수 타입의 정의를 인스턴스화까지 미룰 수 있습니다. < >를 사용해서 제네릭을 선언할 수 있습니다.
  8. 스태틱은 클래스에 직접 귀속되는 속성들입니다. static 키워드를 사용해서 선언합니다.
  9. 캐스케이드 연산자는 인스턴스에서 해당 인스턴스의 속성이나 멤버 함수를 연속해서 호출할 때 사용합니다.

 

여기까지 2장 객체지향 프로그래밍였습니다. 다트 언어 마스터하기는 3장 비동기 프로그래밍으로 이어집니다.

다트 입문하기

 

객체지향 프로그래밍

 

비동기 프로그래밍

 

다트 3.0 신규 문법

최지호(코드팩토리) 
임페리얼 칼리지 런던을 졸업하고 계리 컨설팅 회사 밀리만(Milliman) 한국 지사에서 소프트웨어 엔지니어로 일했습니다. 현재 주식회사 코드팩토리를 창업하여 개발을 하면서 초보자뿐만 아니라 현직 개발자에게도 유용한 개발 강의를 제작합니다. 밀리의서재 플러터 전환 차세대 프로젝트를 리드했습니다.

Leave a Reply

©2020 GoldenRabbit. All rights reserved.
상호명 : 골든래빗 주식회사
(04051) 서울특별시 마포구 양화로 186, 5층 512호, 514호 (동교동, LC타워)
TEL : 0505-398-0505 / FAX : 0505-537-0505
대표이사 : 최현우
사업자등록번호 : 475-87-01581
통신판매업신고 : 2023-서울마포-2391호
master@goldenrabbit.co.kr
개인정보처리방침
배송/반품/환불/교환 안내