안녕하세요 오늘은 다트 언어의 널 세이프티란 무엇인지 알아보겟습니다.
다트 언어에 대해 코딩 테스트를 준비하는 분들이나 플러터를 더욱 잘 개발하고 싶은 분들에게 이 글을 추천드립니다.
대쉬(Dash)로 살펴보는 플러터 진영의 대중화
혹시, Dash(대쉬)라는 마스코트를 알고 계시나요? 플러터와 다트의 마스코트(mascot)입니다. 구글이 마케팅 전략이 돋보이는 귀여운 캐릭터들이네요. 파이썬의 구렁이가 있다면 Dash는 좀더 귀여운 버전입니다. 대쉬의 귀여운 스토리는 아래의 링크를 참조해주세요.
널 세이프티란 무엇일까요?
혹자들은 "null 안전"이라고 하며 "널 세이프"이라고도 부릅니다. 그리고 플러터 진영에서는 신뢰할만한 널 세이프티(sound null safety)라고 부릅니다. 왜 sound 를 붙였을지는 이 글 후반에 설명드리겠습니다. 다만, 영어적 표기를 한글로 전달하기 위해서 널 세이프티라고 부르도록 하겠습니다.
"널 세이프티(sound-null-safety)"를 잘 이해하기 위해서 nullable 그리고 non-nullable 이라는 표현을 우선 이해해야 합니다. null-able 이란 한마디로 "null(널) 값을 할당 가능한" 이라는 뜻입니다. 프로그래밍 언어에서는 "값이 없다"를 표현하기 위해 null 이라는 기호를 사용합니다.
그렇다면 널(null)이 무엇을 하였기에 세이프티라는 단어까지 붙여가며 중요하게 다룰까요? 바로 이 null 할당이 가능한 혹은 그렇지 않은이라는 애매한 속성이 디버깅이 어려운 에러를 만들어 내기 때문입니다. 즉, 널 세이프티(null safety)는 null(널)로 인한 오류로부터 여러분을 지켜내는 역할을 합니다.
nullable 속성의 잠재적인 오류 가능성
우리는 보통 변수를 사용할 때, 어디에(where), 무엇을(what), 언제(when) 이 변수를 쓸지에 대해 미리 생각해둡니다. 하지만, 초기값을 결정하기는 어려운 일입니다. 그리고 적절한 초기값은 프로그램의 전체 진행 상태를 살펴서 결정되는 성격을 갖습니다. 혹은, 아직 값이 할당되지 않은 변수이다로 간단하게 퉁쳐서 코딩하고 싶을지도 모릅니다. (null 을 할당해두고 로직 중간 중간에 null 인지 체크하는 코딩 방법)
이러한 모호성(null 일수도 아닐수도 있음)이 잠재적인 에러의 가능성을 높여주는 역할을 하게됩니다. 그리고 이 때 발생하는 에러는 에러 중에 가장 잡기가 힘든 "널 역참조 에러(null de-reference error)"입니다. (아시는 분들은 아시겠지만 매우 끔찍한 에러 중에 하나 입니다.)
이러한 에러의 발생 가능성은 우리를 엔지니어링 측면에서 매우 곤혹스럽게 합니다. 가령, 잘 돌아가던 앱이 갑자기 실행이 중단되면서 뻗어버리게 되는 경우이죠. 그런데, 로그를 아무리 살펴보아도 추적할만한 단서가 없습니다. 단지 대략적인 오류의 위치만 알수 있게됩니다.
그런데, 오류가 발생한 로직이 아직은 단위테스트(unittest)로 분할되지 않은 UI 컴포넌트 한 중간에 떡하니 위치한 그리고 수 십개의 변수들을 한번에 처리하는 골치덩어리 같은 녀석이라고 생각해볼까요? 머리를 움켜쥐고 고통에 빠진 여러분들의 모습이 상상됩니다.
널 역참조 에러란 무엇이고 해결방안은?
널 역참조 에러의 케이스를 간단히 설명해볼까요? String 타입을 가지는 string 변수는 null 값이 함수의 인자로 전달될 것을 고려하지 않았습니다. 그리고, length 라는 속성에 접근을 시도하게됩니다. 당연히 이 함수에 null 을 전달하면 프로그램은 실패할 것이고 여러분의 프로그램은 도중에 갑작스럽게 종료되어 버립니다.
# language: dart
// Without null safety:
bool isEmpty(String string) => string.length == 0;
main() {
isEmpty(null);
}
아직은 티스토리 블로그에서 dart 언어에 대한 코드 칼라 스킴을 지원하지 않기에 유사한 golang color scheme 을 지정하였습니다. 유사한 문법적 특성을 지니는 언어이기에 keyword coloring 은 적절한듯합니다.
그렇다면, 이 오류에 대응하는 가장 좋은 방법은 무엇일까요? null 인지 체크하는 코드들을 계속 작성해야 될까요? 이는 프로그램의 가독성을 해치며 코드의 간결성이 떨어지게됩니다.
코드를 글로 비유하면 아래와 같은 것이죠. 계속 중복된 코드들이 영희가 타고 있는 자동차의 속성이 null이 아닌지 체크하면서 로직을 힘겹게 이어나갈 것입니다. 극단적인 비유라고요? 아닙니다. 영희가 타고 있는 자동차의 색깔이 null 인지 계속 체크하는 로직이 군데 군데 들어가있다면 아래와 같은 괴랄하고 중복이 넘쳐나는 코드가 가능합니다. 혹은 오히려 더욱 빈번합니다.
영희는 붉은색 스포츠 자동차를 타고 집으로 가는길이었다.
영희가 타고 있는 붉은색 스포츠 자동차는 3000cc 의 고급휘발유를 먹는다.
영희가 타고 있는 3000cc, 고급휘발유를 먹는, 붉은색 스포츠 자동차는 강변도로를 달리는 중이다.
영희가 타고 있는 강변도로를 달리고 있는, 3000cc, 고급휘발유를 먹는, 붉은색 스포츠 자동차는 어느새 여의도를 지나 여의도 IFC 몰로 향하는 길이었다.
.....
자 이제 해결 방안 중에 하나인 null을 로직적으로 체크하는 구현은 좋은 옵션이 못되는 것을 알 수 있습니다. 그렇다면 또 다른 아이디어가 있을까요? 맞습니다!
null 자체를 변수에 할당하지 못하도록 하자!
(Eureka!) non-nullable 의 발견입니다.
Non-nullable 구현의 어려움
변수에 null을 아예 할당조차 못하게끔 한다는 것은 매우 좋은 발상입니다. 그리고 다른 언어들도 이러한 개념(타입 선언 및 타입핑 힌트)을 차용하고 있습니다. 그런데 null 을 할당조차 못하도록 제어하는 것이 어려운 일임을 알고 있나요? 그것은 바로 타입이 없는 변수들입니다. (악마 자식이 등장)
타입이 없는 변수는 과거에 상상도 못할 일이었습니다. 아니 변수에 타입이 없으면 어떻게 코딩할건데? 가 일반적인 정서였죠. 그리고 이걸 VB 스크립트 등의 고전 인터프리터 언어들이 해내었습니다. (저는 타입이 있는 언어를 선호하기에 javascript 보다는 typescript 로 작업하는 스타일입니다.) 아래의 예시를 한번 살펴볼까요?
# language: php
$foo = "나는 정수일수도, 문자열일수도, 부동소수점일수도 있다";
# language: python
foo = "나는 인라인 함수일수도, 문자열일수도, 부동소수점일수도, 정수일수도, shallow copy 일수도, None 일수도 있다."
# language: javascript
var foo = "위와 동";
위와 같은 난장판인 상황에서 변수의 타입 정보를 제공하는 것은 타입 힌트(Typing Hint)일 것입니다. 그러나, 이는 개발 진행 중에 도움을 주는 보조적인 수단이지 null safety(널 안전)를 지켜주는 수단은 아닙니다. PEP 같은 규칙 체계가 있지만 충분하지 않은 것으로 보입니다.
다트(Dart)의 Sound Null Safety
지금까지 널 세이프티(null safety)가 어려운 과제이며 프로그램에 잠재적인 오류를 발생시키는 독약 같은 녀석임을 충분히 알게되었습니다. 그렇다면 구글이 스폰서를 자처한 플러터의 핵심 언어인 다트의 Sound Null Safety 란 어떤 특성이 있는지 살펴보겠습니다.
다트 언어는 아래의 세가지 원칙을 바탕으로 "널 세이프티"를 구현하였습니다. 믿음직한 갓구글의 향기가 느껴집니다. 구글은 코딩 컨벤션 등 개발 방법론에 있어서도 업계 최상위 수준의 워크플로우를 자랑하는데요. 구글이 지향하는 프로그래밍의 원칙에 대해서도 한번 감을 잡아보시길 바랍니다.
- non-nullable를 기본으로 한다.
- 점진적인 마이그레이션을 지원한다.
- 완전한 신뢰성을 제공한다.
첫 번째, non-nullable를 기본으로 제공합니다. 즉, 명시적으로 nullable 속성을 부여하지 않는다면 non-nullable 로 타입 추론 엔진이 구동됩니다. 구글이 이렇게 구현한데에는 리서치가 수반되었는데요. 기존에 사용중인 API 들에 대해 조사를 한 결과 대부분의 경우 공통적으로 non-nullable 을 전제로하여 구현되었음을 기반으로 의사결정을 내렸다고 합니다. (데이터 사이언스의 지존인 구글이라면 AI를 사용했을지도 모른다는 합리적인 추측입니다.)
두 번째, 점진적인 마이그레이션을 지원합니다. 안타깝게도 다트 언어는 널 세이프티 개념을 2.12.0 버전부터 적용하였습니다. 즉, 2.12 버전 미만들(< 2.12.0)에 한하여서 널 세이프티를 지원하지 않는다는 말이기도 합니다. 그렇다면 개발자들의 큰 고민은 어떻게 마이그레이션을 하느냐이겠네요. 맞습니다. 예전 파이썬2에서 파이썬3로 넘어갈때 개발자들의 고통은 그들은 알고 있었고 이 부분을 충분히 공감한다는 것이 핵심입니다. 다트는 점진적인 마이그레이션을 허용하기 위해서 한 프로젝트에 널 세이프한 코드와 그렇지 않은 코드가 공존하도록 허용하였습니다. 즉, 이제부터는 안된다라고 못을 박기 보다는 공존을 허용하도록 배려한 것입니다. 이것이 어렵다는 것은 모든 개발자들이 알것입니다. 그렇기에 개발진들의 배려에 웅장해지는 가슴을 주체할 수 없네요.
세 번째, 완전한 신뢰성을 제공합니다. 이 부분이 가장 중요합니다. 널 세이프를 구현하는 방법이 어려운 점에 대해 위에서 기술하였습니다. 그런데, 다트 언어는 강력한 타입 추론 엔진이 있습니다. 즉, 타입 시스템이 임의의 변수(타입이 선언되지 않은 변수)의 타입을 추론하여 nullable이 아니라고 판단한다면 이 변수는 절대 null 값을 할당 받을 수 없습니다. 보통 이러한 보증에는 여러 단서들이 붙습니다. "이러한 경우에는 코딩에 주의해야 한다" 라고 말입니다. 여기서 Sound Null Safety 라는 단어가 쓰인 이유가 있습니다. "신뢰할 수 있는 널 세이프티"라고 이제 읽혀지시나요? 닷넷 등의 타입 추론 시스템을 제공하는 언어들보다 더욱 강력한 워딩을 사용하였고 면책조항(Disclaimer)도 없습니다.
즉, 프로젝트와 종속성들이 null safe 화 되면, 다트의 추론 엔진이 완전한 신뢰성을 제공하고 프로젝트들은 null safe 를 보장 받습니다. 그리고 컴파일러 최적화, 적은 버그, 작아진 바이너리 크기 및 훨씬 빨라진 실행 속도 등을 보장합니다. 다시말해 다트가 제공하는 엔진의 수혜를 어서 받으라는 이야기입니다.
맺음말
지금까지 다트 언어의 Sound Null Safety 에 대해 알아보았습니다. "널 세이프티", "널 역참조 에러", "버전별 지원 정책", "nullable 과 non-nullable에 대한 이해", "다트 언어의 타입 추론 엔진의 효과", "점진적 마이그레이션 허용 정책" 등등 수많은 내용을 다루었습니다.
그리고 중간 중간에 코딩에 있어서 어려운 점을 야기하는 추론형 변수 타입의 단점과 코드의 복잡성이 가중되는 어려움에 대해서도 간단히 코멘트를 드리기도 하였네요.
저는 문법보다는 백그라운드를 중시하는 타입입니다. 그래서 코드를 기술하면서 이렇다라고 설명하기보다는 설계와 구현에 담긴 함의를 이해하는데 초점을 맞추는 스타일입니다. 그런데, 코드를 아예 작성하지 않을 순 없습니다. 따라서, 간략하게 필요한 정보들을 나열하는 방식으로 설명드리면서 글을 마치도록 하겠습니다.
Dart 2.x 의 Sound Null Safety 활성화 정책
Dart 2.x SDK는 Sound Null Safety 지원을 프로젝트 설정을 통해서 활성화 비활성화 하도록 지원하고 있습니다. pubsec.yaml 파일에 아래와 같이 환경 정보를 제공해주시면 됩니다.
environment:
sdk: '>=2.12.0 <3.0.0'
Dart 3.0의 릴리즈 계획과 Default Null Safety 정책
Dart 3(다트3)는 2023년도 중반에 릴리즈 계획이 잡혀있습니다. 3 버전부터는 Sound Null Safety를 필수입니다. 즉, Null Safe 하지 않다면 컴파일 시간에 오류가 발생합니다. (C++ 영향으로 컴파일 시간이라고 하지만 공식문서에서는 edit-time 이라고 표현합니다.) 다트 3 버전을 준비하는 분들이라면 Null Safety 에 대한 준비가 필요하니 마이그레이션 계획에 참고가 필요합니다.
Null Safety 관련 코드 예시 및 Nullable 명시적 선언
Null Safety 를 설정하게 되면 아래의 코드에 대한 해석이 바뀝니다. 추론형 변수 선언에도 잘 동작합니다. 그리고 아래의 모든 변수들은 non-nullable 입니다.
var i = 42; // 추론형 변수 선언
String name = getFileName();
final b = Foo();
그렇다면, nullable 이 정말 필요할 때도 있습니다. 이 경우에는 명시적으로 선언하여야 합니다. 명시적으로 선언하는 방법은 물음표(?)를 타입 뒤에 명시해주는 방법을 선택하였습니다. (타입스크립트를 해보신 분들이라면 아시겠죠?)
int? aNullableInt = null;
지금까지 여러분의 플밍 동반자 K-IN 이었습니다.
이상입니다.
K-IN 드림.
'프로그래밍' 카테고리의 다른 글
[vscode] Visual Studio Code 확장 프로그램 추천 - MySQL (대부분 DB 연결이 한번에 가능) 데이터 베이스 매니저 확장 프로그램! (0) | 2023.03.03 |
---|---|
Memory Footprint 란? (1) | 2023.03.01 |
플러터(Flutter) | 현업 개발자가 알려주는 효과적인 플러터(Flutter) 학습 방법 (1) | 2023.01.24 |
플러터(Flutter) | Tutorial 02 | 개발환경 구성하기 (MacOS) (1) | 2022.11.06 |
플러터(Flutter) | Tutorial 01 | Flutter 란 무엇? - 장점/단점 따져보기 (0) | 2022.11.06 |