코틀린 개발자는 null을 어떻게 바라봐야 하는가
27 Oct 2020 | kotlin type_system(널 포인터는) 내 10억 달러짜리 실수였다. 1965년 당시, 나는 ALGOL W라는 객체 지향 언어에 쓰기 위해 포괄적인 타입 시스템을 설계하고 있었다. 내 원래 목표는 어떤 데이터를 읽든 항상 안전하도록 컴파일러가 자동으로 확인해 주는 것이었다. 그러나 나는 널 포인터를 집어넣으려는 유혹을 이길 수가 없었다. 그렇게 하는 게 훨씬 쉬웠기 때문이다. 이 결정은 셀 수도 없는 오류와 보안 버그, 시스템 다운을 낳았다. 지난 40년 동안 이러한 문제들 때문에 입은 고통과 손해는 10억 달러는 될 것이다. - 토니 호어
Null
이라는 단어를 보면 무슨 느낌이 드는가. 쳐다보기도 싫고, 스트레스를 받고, 두렵고, 피하고 싶다면 당신은 아마 높은 확률로 코틀린 개발자는 아닐 것이다(그래야만 한다).
Java를 프로로써 다뤄본 적은 없어도 한 번쯤은 모두 접해보았을 거라 생각한다. 나 역시도 자바를 다뤄본 적이 있다. 그때를 생각해보면, null
은 항상 회피와 두려움의 대상이었다. NullPointException
을 마주칠까 두려워했었다. 그래서 null
을 어떻게 다뤄야 하는지에 대한 레퍼런스를 검색하곤 했었다.
Java에서 null을 다루는 몇 가지 지침
지금도 Java와 null을 함께 검색하면, null을 다룰때 권장하는 지침들이 나온다. 아래는 대표적인 3가지 지침이다.
- API에 null을 최대한 쓰지 말아라.
- 초기화를 명확히 해라.
- Optional을 잘 익혀 사용해라.
3번 같은 경우 실제로 JAVA코드에서 많이 보이고 있는것 같다.
그러나 코틀린 개발자 입장에서 위의 3가지 지침들이 안타깝게 느껴진다. 위의 지침들은 null을 회피하게 만들거나, 많은 boilerplate 코드들을 필요로 하기 때문이다. 자바를 다룬다면 지키는게 좋은 사항들이지만 아쉬운건 어쩔 수 없다.
null의 본질
null의 본질을 생각해보자. 자바의 지침들이 말하고 있는 것 처럼 null은 정말 나쁜놈이며, 피해야할 대상일까?
나는 그렇게 생각하지 않는다. 현대 많은 programming lanaguage 디자이너들도 그렇게 생각하지 않는것 같다.
이 글의 가장 첫 부분에 토니 호어의 말을 인용했다.
(널 포인터는) 내 10억 달러짜리 실수였다. 1965년 당시, 나는 ALGOL W라는 객체 지향 언어에 쓰기 위해 포괄적인 타입 시스템을 설계하고 있었다. 내 원래 목표는 어떤 데이터를 읽든 항상 안전하도록 컴파일러가 자동으로 확인해 주는 것이었다. 그러나 나는 널 포인터를 집어넣으려는 유혹을 이길 수가 없었다. 그렇게 하는 게 훨씬 쉬웠기 때문이다. 이 결정은 셀 수도 없는 오류와 보안 버그, 시스템 다운을 낳았다. 지난 40년 동안 이러한 문제들 때문에 입은 고통과 손해는 10억 달러는 될 것이다. - 토니 호어
이 글을 자세히 보자. 토니 호어는 null pointer, 즉 null을 가르키는 것이 실수이지 null 그 자체의 개념적인 문제라고 한 것이 아니다. 소프트웨어는 어찌되었든 값이 없음을 나타낼 무언가가 필요하고, 실제로 null은 값이 없음을 나타내는 가장 명확하고 효율적인 방법이다. 따라서 null은 null의 의미를 살려서 같이 가져가야 할 존재이지, 두려움과 회피의 대상이 아니다.
결국 자바 진영에서 “null을 회피하는 지침”들이 나온다는 것은, 자바 타입시스템 설계에 결함이 있다고 볼 수 밖에 없다. 왜 자바 타입 시스템이 명백한 오류인지를 조금 더 구체적으로 증명해 보자.
Type이란?
타입 시스템을 다루기 전에 타입이 무엇인지를 명확히 알아야 한다. 그렇다면 객체지향 관점에서 타입은 무엇일까?
타입이란, 객체의 퍼블릭 메서드들, 즉 퍼블릭 인터페이스의 집합을 의미한다.
예를들어 A라는 객체에 날기, 먹기, 숨쉬기 라는 퍼블릭 메서드가 있다면, 이 것을 하나의 집합으로 보아 타입으로 분류할 수 있다. 이 세가지 메서드 집합을 Bird라는 타입으로 분류해보자. B와 C역시 동일한 퍼블릭 메서드 집합을 구현하고 있으므로 Bird 타입이라고 볼 수 있게 된다. 여기서 중요한 점은, 객체가 어떤 상태를 가지고 있는지(어떤 지역변수를 가지고 있는지)는 관심 대상이 아니다. 오로지 객체의 퍼블릭 인터페이스를 보고 타입을 정한다. 즉 객체지향 관점에서 Type이란, 객체의 퍼블릭 인터페이스의 집합이다.
실생활에서의 Type
아주 간단한 실생활의 예를 들어보자. 우리는 가끔 이런 말을 하곤 한다.
너 되게 짜증나는 타입이구나??
이 말에는 엄청나게 객체지향적인 관점이 담겨있다. “너의 속사정은 모르겠지만, 너는 굉장히 사람을 짜증나게 하는 public 메서드를 가지고 있구나”라고 판단하여 짜증 유발자 타입으로 분류하곤 한다. 이렇듯 객체지향에서의 타입과 실생활에서 말하는 type은 거의 동일하다. 어려운 개념이 아니다.
타입이 무엇인지 알아보았으니 타입 시스템과 연관시켜보자.
Type System
여기 8개의 객체가 있다. 아마 이 객체들을 보고 본능적으로 “자동차”라는 타입으로 분류했을 것이다. 그 본능의 내막을 살펴보면 나름대로 객체지향적인 논리가 숨어있다.
우리가 흔히 “자동차” 타입에 기대하는 퍼블릭 인터페이스는 이렇다.
- 시동을 걸 수 있다.
- 전진, 후진, 좌회전, 우회전을 할 수 있다.
- 사람을 태울 수 있다.
- 땅 위를 달릴 수 있다.
위 객체들은 모두 이 퍼블릭 메서드를 잘 수행(수신)할 수 있기 때문에 “자동차”라는 타입으로 분류한 것이다.
그렇다면 상상을 해보자. 겉모습을 전혀 모르는 내용물이 빨간 박스 안에 숨겨져 있다. 이 객체는 시동을 걸 수 있는 인터페이스가 전혀 없다. 앞뒤로 움직일 수도 없고 좌회전 우회전도 못한다. 심지어 사람을 태우려고하면 완전히 먹통이 되어버린다. 우리는 이 빨간 박스 안의 내용물을 “자동차”타입으로 분류할 수 있을까? 아무도 그렇게 분류하지는 않을 것이다. “자동차”라면 마땅히 가져야 할 public interface를 아무것도 가지지 않기 때문이다.
이 내용을 그대로 코드 레벨에서 살펴보자.
Java의 타입시스템
String str = ...;
여기 자바의 String 타입 변수가 하나 있다. String에 대해서 우리가 기대하는 퍼블릭 인터페이스들이 여럿 있다. 예를 들어 substring
,split
등이 있을 것이다. 그러나 분명히 String 타입이라고 해놓았으면서 String의 퍼블릭 인터페이스를 호출할 때 마다 터져버리는 경우가 있다. 바로 String 타입에 null을 할당했을 때이다.
String str = null;
null은 String이 제공하는 퍼블릭 interface를 모두 수행할 수 없다. 이러한 null을 String 타입으로 간주하는건 Int를 String에 넣는 것과 마찬가지로 너무나도 위험하고 말도 안되는 행위다. 즉, 이것은 null 자체에 문제가 있는게 아니라, String 타입에 뜬금없이 null을 허용하는 자바 타입 시스템의 문제라고 볼 수 밖에 없다.
null을 여기저기 어느 타입에도 끼워 넣을 수 있게 허용한 자바의 타입 시스템 때문에 많은 자바 개발자들이 null을 두려워 하게 된게 아닐까 싶다. 자바 개발자들은 잘못이 없다. 타입 시스템의 문제다.
또한 자바 개발자들이 상당히 많고 커뮤니티가 방대하다 보니 자연스레 null을 피하는 식의 프로그래밍이 표준이 되어버린 느낌이다(물론 자바에서는 피하는게 답일수도..).
자연스레 Java로 입문하는 뉴비들은 자신이 왜 null을 피하고 있는지도 모르고 그저 관습처럼 피하게 된다. 여기서 중요한 점은, null을 왜 피하는지도 모른채 관습처럼 null을 피해온 자바 개발자가 kotlin으로 넘어올 경우 여전히 java스러운 코드를 짤 가능성이 크다. 타입에 대한 고찰이 없으면 null을 바라보는 관점이 다를 수 있다는 사실조차 모르기 때문이다.
아무튼 자바의 타입 시스템은 그렇게 생겼기 때문에 어쩔 수 없는 현실이지만 많이 아쉬운 부분이다.
더 나은 타입 시스템
그렇다면 좀 더 나은 타입 시스템은 어떻게 생겼을까. 현재로써는 코틀린과 스위프트처럼 널을 허용하는 타입과 그렇지 않은 타입으로 나누는 것으로 보인다.
var str1: String = null // 컴파일 에러
var str2: String? = null // 컴파일 성공
str2.split(':') // 컴파일 에러
str2?.split(':') // 컴파일 성공
첫 줄 str
은 순수한 String 타입이다. null일 수 없는 순수한 String이다. 따라서 null을 허용하면 런타임 에러가 아니라 컴파일 에러가 나야 더 안전한 타입 시스템이다.
str2
는 null일수도 있는 String타입이므로 null을 할당해도 에러가 나지 않는다. str2
은 null일 수도 있기 때문에, 사용하는 측에서는 null check가 강제되어야만 한다.
3번째 라인에서 보듯 null check를 하지 않으면 당연히 컴파일 에러가 나야 안전한 타입 시스템이다.
더 나은 API
좀 더 세세하게 들어가서 API수준을 들여다보자. 좀 더 명확하고 안전한 API는 어떻게 생겼을까.
코틀린 Collection API에 있는 List의 확장 함수, getOrNull(), firstOrNull()같은 함수가 좋은 함수라고 생각한다.
public fun <T> List<T>.getOrNull(index: Int): T?
getOrNull
함수는 인덱스에 해당하는 T객체가 있으면 T를 반환하고, 없으면 null을 반환한다. 값이 없기 때문에 값이 없음을 나타내는 가장 명확하고 효율적인 표현인 null을 반환하는게 상식적이다.
또한 T?를 반환하기 때문에 사용하는 측에서는 null check가 강제된다.
자바스러운 API
더 나은 API와 반대로, 불명확하고 불안전한 API를 살펴보자. 대표적으로 자바 ArrayList의 indexOf함수를 예로 들 수 있겠다.
public int indexOf(Object o)
indexOf
함수는 리스트에서 o객체와 동일한 객체가 있다면 그 인덱스를 추출한다. 그러나 만족하는 인덱스가 없다면?
다들 알고 있겠지만 -1을 반환한다. 그러나 사실 만족하는 인덱스가 없을 경우 null을 반환하는지, -1을 반환하는지, -100을 반환하는지, 에러를 내뿜는지는 이 함수를 까보지 않고는 모르는 일이다. 누군가는 값이 없을 때 당연히 null을 반환하리라고 확신하며 코드를 짤 수도 있으나 큰 코 다칠 것이다.
즉 타입 시스템이 null을 회피하게끔 설계되어있다면 이렇게 모호한 api가 만들어 질 수 밖에 없다. 값이 없다면 null을 반환하는게 가장 상식적인 방법이다.
마무리
코틀린이 nullable 타입을 도입한건 단순히 ?. 나 ?: 연산자를 통해 자바보다 null을 쉽게 피하기 위해서가 아니다. 오히려 값이 없을 때 null을 적극 사용하여 명확하고 안전하게 코딩하기 위해 도입되었다. 이러한 사실을 알고 코딩을 하는 것과 모르고 코딩을 하는 것은 코틀린 스러운 코드를 짤 수 있냐 없냐의 문제로 이어지기 때문에 중요한 점이라고 생각한다.
또한 자바의 타입시스템은 익히 알려진대로 문제가 있고, 쉽게 바뀔 것 처럼 보이지 않는다. 최신 자바는 나름의 발전을 해오고 있지만 타입 시스템은 바뀌지 않고 있다. 하위호환이라는 철학 때문에 쉽게 바뀌지 않을 것 같아 보인다. 그렇다면 더 안전하고, 명확한 소프트웨어를 위해 kotlin을 도입해보는건 어떨까?
Comments