본문 바로가기
Flutter/Dart 입문

[Dart] 10-2. 클래스(class)

by Couldi 2021. 10. 27.
반응형

21. 10. 27.

- Could -

 

이 글은 프로그래밍 입문을 Flutter 때문에 Dart로 시작하는 사람들을 위한 글입니다.
프로그래밍 언어가 가지고 있는 기본 컨셉 자체를 Dart라는 언어를 통해 설명하고,
많은 분들이 Flutter를 위해 학습한다고 생각해, 추후 Flutter 학습에 도움이 되는 방향으로 작성되었습니다.
1. 클래스의 구성
2. 클래스의 분석해보기
3. 클래스와 Null Safety
4. 클래스의 심화된 내용들

 

1.  클래스의 구성

앞서 다뤘던 클래스를 다시 가져와본다.

class Human {
    String name;
    int hp;
    int mp;
    
    Human(this.name, this.hp, this.mp)

    void attack() {
        print('attack!!');
    }
    
    void defence() {
        print('defence!!');
    }
}

이제 너저분한 설명 없이 코드를 읽어보자. 곤충이 머리, 가슴, 배로 나눠지듯, 클래스도 인스턴스 변수(instance variable), 생성자(constructor), 메소드(method)로 나누어진다. 코드를 보고 아 이부분이 뭐고, 저부분이 뭐다를 읽을 수 있으면 클래스의 구성을 제대로 파악한 거다.

 

2.  클래스의 사용

flutter에서 class를 많이 사용하는가? 라고 묻는다면 그렇다. flutter를 공부하다 보면 모든것이 위젯이구나 라는 생각이 들게 되는데, 위젯도 결국은 클래스의 한 종류이다. 그러면 flutter에서 자주쓰게 되는 위젯의 코드를 가져와 어떤 식으로 사용하는지 해부해보고, 우리가 다루지 않았던 내용들을 채워보자.

 

처음 flutter를 접해서 사용하는 Text위젯의 코드이다. 모든 코드를 올려두면 정신이 없을 것 같으므로 몇개의 변수들과 메소드들, 그리고 수많은 주석들을 제거했다. 원래 어떻게 생겼는지 알고 싶다면 로컬에서 Text위젯을 커맨드 혹은 알트를 누른후 클릭해보면 해당 코드로 이동할 수 있다.

class Text extends StatelessWidget {
  const Text(
    String this.data, {
    Key? key,
    this.style,
    this.strutStyle,
    this.textAlign,
    this.textDirection,
  }) : assert(
         data != null,
         'A non-null String must be provided to a Text widget.',
       ),
       textSpan = null,
       super(key: key);

  final String? data;
  final InlineSpan? textSpan;
  final TextStyle? style;
  final StrutStyle? strutStyle;
  final TextAlign? textAlign;
  final TextDirection? textDirection;
  
 }

지금도 코드가 너저분해 보이니, 우리가 배운 내용과 지금 부터 다룰 내용만 남기고 다시 정리해본다.

class Text extends StatelessWidget {
  const Text(
    String this.data, {
    this.style,
    this.strutStyle,
    this.textAlign,
    this.textDirection,
  });

  final String? data;
  final InlineSpan? textSpan;
  final TextStyle? style;
  final StrutStyle? strutStyle;
  final TextAlign? textAlign;
  final TextDirection? textDirection;
  
 }

이렇게 정리하니까 눈에 생성자와 인스턴스 변수들이 눈에 들어온다. 위쪽의 const Text로 시작하는 부분은 생성자, 아래 final 하고 시작되는 애들은 인스턴스 변수들이다.

 

1) 인스턴스 변수

일단 인스턴스 변수들 부터 살펴보자. 앞서 다루지 않았던 final과 ?가 거슬리고, 변수명 앞에는 데이터타입을 적어야한다고 했던거 같은데 알것 같은 데이터타입은 String 하나뿐이고 나머지는 데이터타입이 맞는건지 아리송하다.

  • final
    final은 나중에 다시 다룰 내용이긴 하지만, const라는 녀석과 같이 알아둬야한다. 일반적인 변수들은 값을 집어넣고 추후 값을 변경하는 것이 가능한 상자라면, final은 처음에 변수를 집어넣고 나면 값이 고정되어 더이상 변경할 수 없는 상자를 의미한다. 그러니 Text의 인스턴스 변수들에 값이 할당 되고 나면, 해당 인스턴스가 가지는 값들은 전부 변경할 수 없다는 걸 의미한다. 
  • ?
    이 물음표는 프로그래밍에서 여기저기 요긴하게 쓰이는 기호이다. Null Safety가 적용된 Dart에서 위 처럼 변수의 데이터타입 뒤에 ?를 붙이면 이때는 Null Safety와 관련된 기능을 한다. String 변수명 과 String? 변수명 의 차이에 대해 이해하면 이 ?의 기능을 이해할 수 있다. 기본적으로 Dart에서는 모든 변수에 Null value가 오면 안된다. 상자를 빈상자로 내버려두면 안된다는 의미이다. 하지만 코드를 작성하다보면 가끔은 빈상자를 사용해야할 때가 있고, 이때 사용하는게 ? 기호이다.
    직관적으로 해석하면 다음과 같다.
    String data - 빈상자 불가능 (Null 이면 안됨!)
    String? data - 빈상자 가능, 그래서 일단 Null 넣고 시작.  (Null 가능!)
    추후 Null Safety를 다룰때 더 자세히 다룰 내용이다.
  • 데이터타입..(?)
    분명 데이터타입을 적는 위치인데, 데이터타입이 아닌 아이들이 잔뜩 적혀있다. 정체를 조사해보면 전부 클래스들의 이름이다. 클래스가 들어갈 변수 선언할때, 데이터타입 적는 위치에 클래스명을 적어주는 거라 이해하자.

2) 생성자

const Text(String this.data, {this.style, this.strutStyle, this.textAlign, this.textDirection});

이해를 돕기 위해 한줄로 정리해봤다. Text의 생성자이고, Text위젯을 사용할때 어떤 데이터들을 넣어야하는지 정의하는 부분이다.  위 생성자에서 작성한 내용에 따라 아래와 같이 사용한다.

const Text('please Like❤️ ️& Subscribe😉!', style: TextStyle(fontSize: 12)),

앞의 this.data 부분에는 문자열인 'please Like❤️ ️& Subscribe😉!' 가 들어갔고,

뒤의 this.style 부분에 따라 style: TextStyle(fontSize: 12) 의 데이터를 받는다.

{ }의 의미 : Named Parameter

앞서 보지 못했던 생성자 안에 { }는 무슨 기능을 하는걸까? this.data는 아무것도 없어서 그냥 문자열을 입력했고, this.style은 {}로 쌓여있어 style: 이 추가되었다. 만약 { }가 없이 Text의 생성자가 아래와 같이 정의 되었다면?

Text(this.data, this.style);

아래처럼 데이터를 넣어주어야한다.

const Text('please Like❤️ ️& Subscribe😉!', TextStyle(fontSize: 12)),

이렇게 style: 처럼 넣어야하는 변수 앞에 이름을 붙여야 하는 변수를 Named Parameter라 하고, 이걸 위해 사용하는게 { } 기호이다. { } 기호는 생성자를 선언할 때 많이 사용하지만, 함수에서도 사용가능한 기능이기 때문에 함수나 생성자를 선언할때 만드는 parameter가 {}안에 들어가 있으면, 이게 Named Parameter라고 알면 된다.

그리고 한가지 더 중요한게 있다.

Named Parameter를 만들었다면 이건 필수로 넣어야 하는 값이 아니다. 그렇기에 Text('please Like❤️ ️& Subscribe😉!') 이렇게만 적어도 동작이 가능해진다.

Named Parameter지만 필수로 데이터를 넣어야하게 만들고 싶어!
그렇다면 이때 사용하는 키워드는 required 키워드이다.
Text(this.data, {required this.style});​

위와 같이 적는다면, 추후 Text 클래스로 인스턴스를 만들때 꼭 style: 파라미터에 값을 적어주어야한다.

const Text('please Like❤️ ️& Subscribe😉!', style: TextStyle(fontSize: 12)),
                                         // 이부분이 필수가 된다.

 

3.  클래스와 Null Safety

flutter를 사용하면서 클래스를 다루다보면 이놈의 Null Safety관련한 에러를 자꾸 만나게 된다.

일단 Null Safety는 우리가 다룰 변수에 Null 값을 허용할지 말지에 관한 문제이다. 이걸 Dart언어차원에서 Null이 들어가도 되는 변수, Null이 들어가면 안되는 변수를 지정해서, 프로그래밍 중 발생할 수 있는 관련 문제들을 예방해주는 기능이다.

처음 프로그래밍을 공부하는 입장에서 뭐만 하면 null관련해서 에러메세지를 보게 되기 때문에 '정말 짜증나는 기능이다!' 라고 생각할 수도 있지만, 이 Null Safety가 적용안된 다른 언어였다면, '대체 어디서 에러가 난거야!!!' 라고 소리지르면서 원인 모를 에러때문에 머리를 싸매고 있었을 가능성이 크다. 그러니 Null Safety를 너무 미워하지 않았으면 한다.

 

문제는 클래스를 사용할때 유독 Null Safety와 관련된 이슈를 많이 접한다는데 있다. 그럴 수 밖에 없는게, 클래스라는 것이 실제 데이터를 가지고 만드는게 아니라, 들어올 데이터를 예측해서 설계도를 그리는 과정이기 때문이다. 데이터를 다루는 것도 아직 낯선 상태에서 설계도를 그리는데 에러가 안나면 그게더 이상한거다.

 

그래서 이 클래스를 다룰때 생각해봐야하는 부분을 인스턴스 변수 부분과 생성자 부분으로 나누어서 살펴보자. 보통 인스턴스 부분과 생성자 부분을 작성하는 중에 Null 관련 에러를 많이 마주하게된다.

 

다시 Human 클래스를 가져왔다.

class Human {
    String name;
    int hp;
    int mp;
    
    Human(this.name, this.hp, this.mp)

    void attack() {
        print('attack!!');
    }
    
    void defence() {
        print('defence!!');
    }
}

1) 인스턴스 변수

앞서 String과 String?의 차이는 Null이 올 수 있는지, 없는지라고 얘기했었다. String으로 인스턴스 변수를 선언하면 생성자에서 인스턴스변수에 들어갈 값을 받을 때 null이 들어가지 못하게 작업을 해줘야 한다.

String?으로 인스턴스 변수를 선언했다면, Null이 초기값으로 들어간 상태의 변수가 생성이 된다. 즉, 이미 변수가 초기화 되어 있으므로 생성자에서 변수를 받지 않아도 되고, Null이 들어가지 못하게 추가작업을 해줄 필요도 없다.

 

2) 생성자

앞서 인스턴스 변수 선언에 따라 생성자를 어떻게 작성해야하는지 달라진다고 얘기했다.

전부 Null을 허용하지 않는 경우

class Human {
    String name;
    int hp;
    int mp;
    
    Human(this.name, this.hp, this.mp)
}

이렇게 작성해주면 된다. 여기서 만약 Named Parameter를 사용해 생성자를 작성해준다면,  required 키워드를 사용해야한다.

 왜냐하면, 일반적인 파라미터가 인스턴스생성시 필수값인데 반해, 네임드 파라미터의 경우는 필수값이 아니다. 필수값이 아니라는 이야기는 값을 생성자 입장에서는 값을 입력받을 필요가 없다는 얘기다. 하지만 그렇게 코드를 짠순간 class에서는 인스턴스 변수가 Null이 되게 되고 null관련 에러를 띄우게 된다.
class Human {
    String name;
    int hp;
    int mp;
    
    Human(this.name,{this.hp, this.mp})
}​

 

위와 같이 짠 코드를 읽어보자면 인스턴스 변수의 선언 부분에서 '여기에 null이 오면 안돼!' 라고 얘기해 놓고, 생성자에게는 'name은 꼭 받아야하고, hp랑 mp는 이름 붙여서 데이터를 받아야하는데 안받아도 상관없어' 라고 이야기하는 꼴이다.
그래서 아래 와 같이 코드를 작성해야 에러를 피할 수 있다.

class Human {
    String name;
    int hp;
    int mp;
    
    Human(this.name, {required this.hp, required this.mp})
}​​
'생성자야, 이름은 꼭 받고, hp랑 mp도 이름 붙여서 받는데 꼭받아야해!' 라고 말하고 있는 모습이다.

 

 

전부 Null을 허용하는 경우

class Human {
    String? name;
    int? hp;
    int? mp;
    
    Human(this.name, this.hp, this.mp)
}

위와 같이 코드를 작성해도 아무 문제 없다. Null을 허용하지 않는 경우보다 자유도가 높은 구조니까. 하지만 별로 권장하지 않는다. Null은 꼭 필요한 곳에만 허용해주는게 중요하다.

인스턴스 변수에 Null을 허용해 주는 경우는 class를 작성할때 신경쓸 부분이 더 적다.

// 이렇게 짜도 되고
class Human {
    String? name;
    int? hp;
    int? mp;
    
    Human(this.name, this.hp, this.mp);
}

// 이렇게 짜도 되고
class Human {
    String? name;
    int? hp;
    int? mp;
    
    Human({this.name, this.hp, this.mp});
}

// 이렇게 짜도 된다.
class Human {
    String? name;
    int? hp;
    int? mp;
    
    Human({required this.name, required this.hp, required this.mp});
}

하지만 이렇게 무지성으로 클래스에 Null을 허용했다가는 클래스 작성시에 에러는 안나겠지만 추후 클래스를 이용해 인스턴스를 생성하고 사용하는 과정에서 얘기치 못한 문제를 마주할 수 있다. 그러니 Null을 허용하는건 신중을 기하자.

 

4.  클래스의 심화된 내용들

클래스를 더 깊게 들어가자면 상속캡슐화, 접근제어자, 추상화, 다형성들의 내용을 다뤄야한다. 지난 글에도 다뤄야한다고 얘기만 해놓고 이번 글에도 다루지 않았다. 클래스가 중요한 내용이기에 자세히 다뤄보고자 하는 생각으로 얘기를 꺼냈으나, 처음 프로그래밍을 하는 사람들이 굳이 저 개념을 알아야할까? 일단 클래스를 잘 다루는게 우선이 아닐까? 라는 생각으로 글을 고치면서 내용을 통째로 바꿨다.

그래도 키워드를 남겨두었으니, 혹시나 공부하다가 저 부분에서 막힌다면, 구글링을 통해 해결할 수 있을 거라 생각한다.

추후 블로그글로도 '언젠가는' 다룰 예정이다. 막상 알아보면 별거아니다. 이론적인 부분이고 클래스를 왜 사용하는지, 어떻게 더 잘 활용할 수 있는지에 대해 도움이 되는 부분이기에 클래스가 익숙해지면 꼭 공부해보길 추천한다.

반응형

댓글