Notice
Recent Posts
Recent Comments
Link
«   2025/06   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30
Tags
more
Archives
Today
Total
관리 메뉴

kym8821 님의 블로그

한 페이지로 공부하는 Dart 기초 - 클래스(class) 본문

Dart

한 페이지로 공부하는 Dart 기초 - 클래스(class)

kym8821 2025. 1. 9. 16:58

Class를 왜 사용해야 할까?

코드를 구조적이고 재사용 가능하게 설계하는데 있어 유리함.

정형화된 데이터를 관리하기 좋음

 

Class 선언 방법과 인스턴스

메서드 : 클래스 내부에 선언하는 함수

프로퍼티 : 클래스의 속성을 나타내는 값. 쉽게 말해서 클래스 내부에 선언하는 변수.

프로퍼티는 var가 아닌 명시적 타입 지정 방식으로 변수를 선언해야 함

선언 방법은 아래와 같음

class 클래스이름{
  프로퍼티속성 프로퍼티명;
  메서드
}

 

아래와 같은 예시를 들 수 있다.

class Player{
  late int age;
  late String name;
  String greet(){
    return("Hello Everyone!");
  }
}

위 예시에서 프로퍼티는 age와 name, 메서드는 greet이다.

class 생성 시점에서 이러한 변수를 초기화해주기 위해서 생성자를 사용할 수 있다.

 

클래스 내부에서 프로퍼티에 접근하기 위해서 this 키워드를 사용할 수 있다.

하지만, 클래스의 프로퍼티를 제외하고 동일한 이름의 변수가 없다면, this 키워드를 생략할 수 있다.

class Player{
  String name = "James";
  int age = 20;
  void introduce(){
    print("Hello! my name is ${name} and i'm ${age} years old");
  }
  void greet(String name){
    print("Hello ${name}! My name is ${this.name}");
  }
}

void main(){
  var p = Player();
  p.introduce(); // res : Hello! my name is James and i'm 20 years old
  p.greet("Kale"); // res : Hello Kale! My name is Kale
}

위 코드를 볼 때, Dart의 클래스는 아래와 같은 순서로 특정 이름의 변수를 찾는 것을 볼 수 있다.

  1. 가장 먼저, 메서드 내부에서 특정 이름의 변수를 찾는다.
  2. 클래스의 프로퍼티 중 특정 이름의 변수를 찾는다.

Dart에서는 메서드 내부에서 가능하다면 this 키워드를 사용하지 않는 것을 선호한다. 따라서 이러한 this 키워드를 사용해야 하는 상황에 대해서 구분할 수 있어야 한다.

 

이렇게 생성한 클래스를 통해서 클래스의 인스턴스를 생성할 수 있다.

인스턴스는 해당 클래스를 타입으로 갖는 개별 객체로, 우리가 class를 선언하는 이유는 인스턴스를 생성하여 구조화된 데이터를 다루고 구조적인 코드를 만들기 위함이다.

class Player{
  late int age;
  late String name;
}

void main(){
  // Player()는 인자로 생성자의 매개변수들을 받는다
  Player p = Player();
  print(p.runtimeType); // res : Player
}

위와 같이 클래스명(생성자에 넘겨줘야 하는 인자들) 꼴로 인스턴스를 생성할 수 있다.

현재는 생성자를 따로 생성하지 않았기에 매개변수가 없는 디폴트 생성자가 실행된 상태이다.

이제 클래스의 생성자에 대해서 알아보자

 

Class 생성자

생성자는 class의 인스턴스가 생성될 때 한 번만 실행되는 함수이다.

클래스와 같은 이름을 갖고, 반환 타입이 없는 함수라고 생각하면 편하다.

positional parameter 방식과 named parameter 방식이 있다.

Positional Parameter 방식의 생성자

positional parameter 방식은 함수 선언시 설정한 매개변수 순서에 따라 인자를 넘겨주는 방식이다.

예시를 들면 아래와 같다.

String makeString(int age, String name) 
  => "${name}'s age = ${age}";

void main(){
  print(makeString(20, "Jimmy"));
}

이처럼 함수를 호출할 때, 함수 선언시 설정한 매개변수 순서에 따라서 인자를 넘겨준다. 이 순서를 지키지 않으면 오류가 발생한다.

Dart의 positional parameter 방식은 인자를 넘겨주는 것을 강제한다.

 

class 생성자도 positional parameter 방식으로 생성할 수 있다. 

class Player{
  late int age;
  late String name;
  // 생성자
  Player(int age, String name){
    this.age = age;
    this.name = name;
  }
}

void main(){
  Player p = Player(20, "Kale");
  print(p.runtimeType); // res : Player
}

Player의 생성자는 age와 name을 차례대로 인자로 받는다.

따라서 main 함수에서 인스턴스를 생성할 때 age와 name에 해당하는 값을 순서대로 생성자의 인자로 넘겨주었다.

 

Dart에서는 아래와 같이 생성자에서 프로퍼티 값을 바로 넣어줄 수 있다.

class Player{
  late int age;
  late String name;
  // 생성자
  Player(this.age, this.name);
}

void main(){
  Player p = Player(20, "Kale");
  print(p.runtimeType); // res : Player
}

마찬가지로 첫 번째 인자로 age를, 두 번째 인자로 name을 받는다.

Named Parameter 방식의 생성자

만약 함수의 매개변수가 10가 있다면 어떨까?

지금은 기억할 수 있겠지만, 1주일만 지나도 인자를 넘겨주는 순서를 잊게 될 확률이 높다.

 

named parameter 방식은 인자를 넘겨줄 때 어떤 매개변수에 값을 할당하는지를 직접 지정해주는 방식이다.

예시는 아래와 같다.

String makeString({required int age, String name="NoName"}) 
  => "${name}'s age = ${age}";

void main(){
  print(makeString(age:20, name:"Kale"));
  print(makeString(age:20));
}

named parameter 방식은 매개변수 선언 순서에 따라서 인자를 넘겨주는 것을 강제하지 않는다.

하지만, 인자를 강제하지 않기에 required를 통해 매개변수를 강제하거나 따로 디폴트값을 넣어줘야 한다.

위 코드처럼 디폴트값이 들어간 매개변수의 경우, 인자를 넘기지 않아도 정상 동작한다.

 

마찬가지로 named parameter 방식을 사용해서 class 생성자를 선언할 수 있다.

class Player{
  late int age;
  late String name;
  // 생성자
  Player({required int age, required String name}){
    this.age = age;
    this.name = name;
  }
}

void main(){
  Player p = Player(name:"James", age:20);
  print(p.runtimeType); // res : Player
}

positional parameter 방식과 비교하면 아래와 같은 차이가 있다.

  • 생성자 매개변수 선언 시 중괄호로 매개변수들을 묶어줌
  • 인자를 넘겨줄 때는 매개변수명:인자값 꼴로 넘겨줌 (순서무관)
  • 생성자 매개변수 선언 시 required로 강제하거나 기본값을 넣어줘야 함

이러한 사항들을 인지하고 진행해야 한다.

 

Named Constructor를 통해 생성자 여러개 관리하기

만약 클래스에서 여러개의 생성자를 관리하고 싶다면 named constructor 방식으로 생성자에 별칭을 부여한다.

 

별칭을 갖는 생성자는 아래와 같이 클래스명.생성자명(매개변수) 꼴로 선언한다.

해당 생성자로 인스턴스 생성시에도 동일하게 클래스명.생성자명(인자) 꼴로 호출한다.

class Player{
  late int age;
  late String name;
  late String team;
  // 디폴트 생성자
  Player({required int age, required String name, required String team}){
    this.age = age;
    this.name = name;
    this.team = team;
  }
  // team은 red로 따로 지정해주는 생성자
  Player.redTeamPlayer({required int age, required String name}){
    this.age = age;
    this.name = name;
    this.team = "red";
  }
  // team은 blue로 따로 지정해주는 생성자
  Player.blueTeamPlayer({required int age, required String name}){
    this.age = age;
    this.name = name;
    this.team = "blue";
  }
  void introduce(){
    print("My name is ${name} and my team is ${team}");
  }
}

void main(){
  // 디폴트 생성자로 생성
  Player p = Player(name:"James", age:20, team:"yellow");
  // redTeamPlayer 생성자로 생성
  Player rp = Player.redTeamPlayer(name:"red team player", age:20);
  // blueTeamPlayer 생성자로 생성
  Player bp = Player.blueTeamPlayer(name:"blue team player", age:20);
  p.introduce(); // My name is James and my team is yellow
  rp.introduce(); // My name is red team player and my team is red
  bp.introduce(); // My name is blue team player and my team is blue
}

 

인스턴스 생성 및 접근과 Cascade Notation

위에서 알아본 인스턴스 생성을 복습하고, cascade notation에 대해 알아보자

인스턴스 생성 후 해당 인스턴스의 프로퍼티나 메서드에 접근할 때는 dot 연산자를 사용한다.

class Player{
  late int age;
  late String name;
  // 생성자
  Player({required int age, required String name}){
    this.age = age;
    this.name = name;
  }
  void greet(){
    print("Hello Everone!, my name is ${name}");
  }
}

void main(){
  Player p = Player(name:"James", age:20);
  p.name = "UpdatedName";
  p.greet(); // res : Hello Everone!, my name is UpdatedName
  p.age = 21;
  print(p.name); // res : UpdatedName
}

위 코드에서는 인스턴스 p의 name과 age에 dot 연산자(쉽게 말해서 마침표)로 접근하고 있다.

 

cascade notation은 객체를 생성 및 사용한 후 ".."를 통해 연속적으로 메서드 호출 및 선언할 수 있는 구문이다.

class Player{
  late int age;
  late String name;
  // 생성자
  Player({required int age, required String name}){
    this.age = age;
    this.name = name;
  }
  void greet(){
    print("Hello Everone!, my name is ${name}");
  }
}

void main(){
  Player p = Player(name:"James", age:20)
  ..name = "UpdatedName"
  ..greet() // res : Hello Everone!, my name is UpdatedName
  ..age = 21;
  print(p.name); // res : UpdatedName
}

기존 코드와의 차이는 다음과 같다.

  • 인스턴스 생성 및 사용 후 세미콜론( ; )로 구문을 종료시키는 것이 아닌 cacade notation으로 추가적인 작업 수항
  • cascade notation 구문들이 모두 종료되는 시점에 세미콜론을 붙임
  • cascade notation을 통해 프로퍼티에 접근할 수도 있고, 메서드에 접근할 수도 있다

cascade notation을 사용하기 위해서는 코드상 cascade notation 바로 이전에 인스턴스가 오면 된다.

즉 아래와 같은 경우에는 cascade notation을 사용할 수 없다.

// 이 코드는 정상적으로 작동하지 않는다
class Player{
  late int age;
  late String name;
  // 생성자
  Player({required int age, required String name}){
    this.age = age;
    this.name = name;
  }
  void greet(){
    print("Hello Everone!, my name is ${name}");
  }
}

void main(){
  Player p = Player(name:"James", age:20);
  p.name = "Updated"
  ..age = 22; // error : 인스턴스 p가 아닌 "Updated"라는 문자열에서 age 속성을 찾고 있음
}

위 코드에서는 cascade notation 앞에 인스턴스가 아닌 문자열 "Updated"가 있기 때문에 문제가 발생한다.

 

enums (열거형)

열거형은 관련된 상수들의 집합을 정의할 수 있는 데이터 타입이다. enum 키워드를 사용해서 선언한다.

특정 값들을 미리 선언해두고 열거형에서 선택할 수 있도록 하기에, 하드코딩 시 발생하는 문제점들을 보완할 수 있다.

enum Team{
  red,
  blue,
  green
}

void main(){
  print(Team.blue); // Team.blue (type : Team)
  print(Team.blue.name); // blue (type : String)
}

Team에 red, blue, green이라는 상수값을 미리 선언해두었고, Team에 접근해서 값을 사용할 수 있도록 했다.

기본적으로 열거형의 값들은 해당 열거형을 타입으로 갖고, name 속성을 통해 값을 추출할 수 있다.

 

열거형을 클래스에 적용해보겠다. 아래와 같이 Team이라는 열거형 타입을 갖는 속성을 추가했다.

enum Team{
  red,
  blue,
  green
}

class Player{
  late int age;
  late String name;
  late Team team;
  // 생성자
  Player({required int age, required String name, required Team team}){
    this.age = age;
    this.name = name;
    this.team = team;
  }
}

void main(){
  Player p = Player(name:"James", age:20, team:Team.red);
  print(p.team); // res : Team.red
}

이처럼 열거형을 사용하여 상수값을 관리하면, 철자 실수 등의 간단하지만 발견하기 어려운 오류를 해결할 수 있다.

또한 상수값의 수정이 필요한 경우 일괄적으로 값을 수정할 수 있기에 편리하다.

 

추상 클래스 (abstract class)

이번에는 추상 클래스에 대해서 알아보자. 추상 클래스는 공통 기능을 구현하거나 자식 클래스에서 필수적으로 구현해야 하는 메서드를 지정하기 위해 사용한다.

아래와 같이 abstract class 키워드를 통해서 선언 및 사용한다.

abstract class Person{
  // 공통기능
  void greet(){
    print("Hello! Nice to meet you");
  }
  // 자식 클래스에서 구현해야 하는 기능
  void sayJob();
}

class Player extends Person{
  late String job;
  Player(this.job);
  // 추상 클래스의 메서드 구현
  void sayJob(){
    print("My job is ${job}");
  }
}

void main(){
  Player p = Player("Player");
  p.greet(); // res : Hello! Nice to meet you
  p.sayJob(); // res : My job is Player
}

extends 키워드를 통해 추상 클래스 Person과 자식 클래스 Player 간 부모-자식 관계를 형성했다.

  • 자식 클래스인 Player는 Person에서 선언한 공통 기능인 greet를 사용할 수 있다.
  • 자식 클래스인 Player는 추상 클래스 Person에서 정의했지만 구현하지 않은 모든 메서드를 구현하도록 강제된다

이처럼 추상 클래스는 자식 클래스의 공통 기능 정의 및 구현 강제를 담당함을 알 수 있다.

 

상속

우리가 객체 지향 프로그래밍을 한다면, 반드시 알고 있어야 하는 개념 중 하나는 상속이다.

상속은 말 그대로 자식 클래스가 부모 클래스의 기능을 확장할 수 있도록 하는 기능이다. 아래와 같은 특징을 갖는다.

  1. 자식 클래스는 부모 클래스의 모든 프로퍼티와 메서드에 접근 할 수 있다.
  2. 자식 클래스는 부모 클래스의 메서드를 재정의(override)할 수 있다.
  3. Dart는 다중 상속은 지원하지 않는다.
  4. 자식 클래스와 부모 클래스 간 is-a 관계를 형성한다.

간단하게 상속을 예시로 알아보자

 

extends 키워드를 사용하고, 자식클래스 extends 부모클래스 꼴로 부모-자식 관계를 형성할 수 있다.

부모 클래스를 지칭할 때 super 키워드를 사용하고, 자식 클래스는 아래와 같이 부모 클래스의 생성자를 호출해야 한다.

class Person{
  late String name;
  late int age;
  Person(this.name, this.age);
}

class Player extends Person{
  late String team;
  Player(String name, int age, String team):super(name, age){
    this.team = team;
  }
}

위 코드에서 자식클래스생성자 : super(부모클래스 생성자 인자들) 꼴로 자식 클래스와 부모 클래스의 생성자를 호출하도록 구현했다. 이 코드를 아래와 같이 보다 간단하게 나타낼 수 있다.

class Person{
  late String name;
  late int age;
  Person(this.name, this.age);
}

class Player extends Person{
  late String team;
  Player(String name, int age, this.team):super(name, age);
}

 

@override 어노테이션을 활용하여 부모 클래스의 메서드를 자식 클래스에서 재정의할 수 있다. 아래는 그 예시이다.

class Person{
  late String name;
  late int age;
  Person(this.name, this.age);
  void introduce(){
    print("Hello my name is ${name}");
  }
}

class Player extends Person{
  late String team;
  Player(String name, int age, this.team):super(name, age);
  @override
  void introduce(){
    print("${name}'s team is ${team}");
  }
}

void main(){
  var p = Player("Jimmy", 20, "red");
  p.introduce(); // res : Jimmy's team is red
}

@override 어노테이션을 활용하여 Person의 introduce를 재정의했고, Player의 인스턴스에서 introduce를 호출했을 때 재정의된 introduce가 호출되었다.

 

이처럼 상속은 부모 클래스의 기능을 확장하고 재사용할 수 있기에 매우 용이하다.

 

 

Mixin

만약 어떤 클래스의 프로퍼티와 메서드를 사용하고는 싶은데, 상속은 하고 싶지 않다면 어떻게 할까?

with 키워드를 사용하면 상속 없이 어떤 클래스에서 특정 mixin의 프로퍼티와 메서드에 접근할 수 있도록 해준다.

 

mixin과 상속의 차이는 아래와 같다.

특징 상속 mixin
키워드 extends with
다중 관계 여부 단일 클래스 상속만 가능 여러개의 클래스 mixin 가능
관계 자식 클래스는 부모 클래스이다.(is-a) 클래스 A가 mixin을 갖고 있다.(has-a)
목적 클래스 간 계층 구조 형성 코드 재사용 및 기능 추가
독립성 부모 클래서에 의존적 독립적으로 동작 가능

 

간단한 예시를 통해 살펴보자

mixin PersonMixin{
  late String name;
  void introduce(){
    print("My name is ${name}");
  }
}

mixin PlayerMixin{
  late String team;
  void introduceTeam(){
    print("My team is ${team}");
  }
  void canItOverride(){
    print("Im PlayerMixin");
  }
}

class HumanPlayer with PersonMixin, PlayerMixin{
  HumanPlayer(String name, String team){
    this.name = name;
    this.team = team;
  }
  void canItOverride(){
    print("Im HumanPlayer");
  }
}

void main(){
  var humanPlayer = HumanPlayer("James", "red");
  humanPlayer.introduce();
  humanPlayer.introduceTeam();
  humanPlayer.canItOverride();
}

PersonMixin과 PlayerMixin을 갖는 HumanPlayer를 생성했다. 아래와 같은 상황들을 확인하자.

  1. mixin은 상속이 아니다. 따라서 with로 가져온 mixin의 프로퍼티들을 생성자를 호출하듯 초기화해주면 된다.
  2. HumanPlayer 클래스 내부에서 mixin의 메서드를 재정의할 수 있다.
  3. 상속에서는 불가능했던 다중 mixin이 가능하다

 

마무리

어떤 개념들을 학습했는지 간단하게 짚고 넘어가자

  1. class 키워드를 활용하여 클래스를 선언할 수 있다.
  2. 생성자는 class의 인스턴스 생성 시 한 번만 호출된다. positional parameter 방식과 named parameter 방식을 사용할 수 있다.
  3. 인스턴스는 클래스이름(생성자의 인자값들) 꼴로 생성할 수 있다.
  4. 여러개의 생성자를 선언해야 한다면 생성자의 별칭을 지정해줄 수 있다.
  5. this 키워드를 통해 클래스 내부에서 자신의 프로퍼티와 메서드에 접근할 수 있다.
  6. 인스턴스 바로 뒤에서 cascade notation( .. )를 활용하여 보다 간단하게 코드를 작성할 수 있다.
  7. 열거형은 상수들의 집합이다. 철자 실수 보완과 유지보수성 측면 등 여러 이점이 있다.
  8. 추상 클래스는 자식 클래스들의 공통 기능을 정의하고 특정 메서드를 정의하는 것을 강제한다.
  9. 상속은 extends 키워드를 통해 클래스 간 부모-자식 관계를 형성한다. 자식 클래스는 부모 클래스의 기능을 확장한다.
  10. mixin은 다중 상속처럼 여러 기능을 조합할 수 있기에 코드 재사용성을 높인다. 하지만 상속은 아니기에 has-a 관계를 갖는다.