레이블이 null인 게시물을 표시합니다. 모든 게시물 표시
레이블이 null인 게시물을 표시합니다. 모든 게시물 표시

2018-02-08

[CppCoreGuidelines] not_null - null이 될 수 없는 값 구분하기

 Null pointer dereferencing은 C++을 사용하다 보면 자주 발생하는 문제다. 값이 없을 수 있는 객체를 지칭할 때 포인터를 사용하고 값이 없는 상태를 null로 표현하는 C++에서 이를 근본적으로 회피할 방법은 없다. 따라서 null일 수 있는 포인터는 사용하기 전에 항상 체크하고 사용해야 한다.

 하지만 모든 포인터가 null이 될 가능성을 가지고 있는 건 아니다. 로직 상으로 일부 포인터들은 null이 될 수 없다. 반드시 존재하는 객체의 주소를 가리키고 있을 수도 있고, 이미 null인지 체크한 포인터일 수도 있다.  이런 포인터까지 사용하기 전에 null인지 체크하고 사용하는 건 귀찮고, 추가 비용만 들어간다.  이런 경우 과거에는 레퍼런스를 이용했다. 레퍼런스는 선언 시 반드시 초기화해야 하므로 레퍼런스가 가리키는 객체는 null이 아닐 것이라는 생각에서였다.  하지만 사실 레퍼런스도 null pointer dereferencing에 대해서 그다지 안전하지 않다. 위와 같은 함수를 아래처럼 포인터를 받아서 부르는 경우를 생각해보자.
위의 코드는 여전히 null pointer dereferencing 문제를 가진다. f5가 인자로 받은 t의 null 체크를 하지 않고 f4로 넘겼기 때문이다.
 게다가 레퍼런스로 부르는 방식은 modern c++에 스마트 포인터가 들어오면서 일반적으로 사용할 수 있는 방법은 아니게 됐다. shared_ptrunique_ptr은 포인터의 semantic을 그대로 따르기 때문에 null이 될 수 있다. 하지만 null이 될 수 없는 shared_ptrunique_ptr를 reference로 표현할 수 없다. 따라서 포인터 시멘틱을 따르는 타입이지만, null이 될 수 없는 객체를 표현할 일반적인 방법이 필요하다.

 C++ Core Guidelinesnull이 될 수 없는 포인터 계열의 변수는 not_null<T>이라는 클래스를 사용하기를 권장한다. not_null<T>은 간단하게 T의 alias일 수도 있다. 하지만 GSL에서는 null 체크를 대신 해주면서 T의 포인터 문법을 그대로 따르는 객체로 구현했다. 이는 실수로 null을 대입하였을 때 빠르게 알기 위해서다. 어쨌든 중요한 점은 not_null<T>이 명시적으로 기존의 T와는 다른 이름으로 보인다는 것이다. 어떤 변수의 타입이 not_null<T>이면, 이 변수에 값을 할당하는 측에서 null 체크를 해야 하고, 이미 할당된 변수에 대해서는 더 이상 null 체크를 안 해도 된다.  간단하게 어떤 함수의 인자가 not_null<T>이라면, 이 인자의 null 체크는 caller가 담당하고, 어떤 함수의 결과 타입이 not_null<T>라면, 이 반환 값의 null 체크는 callee가 담당한다고 말할 수 있겠다.

2015-09-23

Null Object pattern - null 사용하지 않기

 C#, Java 등 현대의 많은 언어는 객체를 레퍼런스로 다룬다. 그리고 모든 레퍼런스는 nullptr이 될 수 있는데, 이 nullptr은 컴파일 타임에 검사할 수 없는 런타임 에러인 NullPointerException을 일으킨다. 그래서 nullptr은 최대한 조심해서 사용해야 하며, nullptr이 될 수 있는 변수는 사용하기 전에 반드시 nullptr인지 검사해야 한다. 하지만 변수를 사용하기 전에 매번 null check를 하는 코드는 예쁘지 못하고 실수하기 쉽다. 이런 문제를 해결하기 위해서 나온 것이 null object pattern이다.

 하지만 null object pattern은 그리 널리 사랑받는 패턴은 아니었다. 사용하기 불편한 여러 가지 부분이 있어 오히려 안티 패턴으로 불리기도 했다. 대체재가 없어서 꼭 필요한 경우에만 어쩔 수 없이 사용하는 패턴이었다.
 하지만 이제는 그마저도 사용할 필요가 없다. null object를 사용할 부분은 전부 Option 모나드로 대체할 수 있다. Option 모나드를 사용하는 것이 훨씬 사용하기 쉽고, 문제도 적다. 그래서 필자는 null object pattern을 사용해야 할 곳은 대신 Option 모나드를 사용하는 것을 추천한다.

 그런데도 null object pattern을 설명하는 이유는 어쨌든 null object pattern이 nullptr이 가지는 문제를 해결하고자 나온 패턴으로 과거 여러 프로젝트에서 쓰였기 때문에, 알고 있는 것이 기존의 코드를 읽는 데 도움이 되기 때문이다. 결코, 사용을 권장하기 위해서는 아니다.

 null object pattern은 아무것도 안 하는 객체(null object)를 제공함으로써 NullPointerException을 피하는 패턴이다. 조금 더 구체적으로는 nullptr를 써야 하는 클래스의 부모 되는 interface를 구현하는 null class를 만들고, 그 클래스의 객체를 nullptr 대신해서 사용하는 것이다. 이렇게 함으로써 verbose 하게 null check 하는 코드를 없애고, 실수로 null check를 하지 않는 경우를 예방할 수 있다.

 null object pattern은 사용하는데 주의해야 할 것이 있다. null object는 아무것도 하지 않는 클래스다. 즉, 아무런 내부 상태도 가지지 않는다. 따라서 이는 empty class가 되고 null object를 공짜로 만들 수 있을 것 같다. 하지만 아쉽게도 C#, C++, Java, C 등 대부분 언어에서 빈 클래스의 객체는 크기가 0이 아니다.1) 따라서 null object를 여러 개 만드는 것은 쓸데없이 메모리를 사용할 뿐이다. 그래서 보통 null object는 전역에 하나의 변수만을 두고 이 변수를 사용하도록 한다. null object를 싱글톤으로 만들어야 한다고 하는 사람도 있지만, 이는 명백히 오버킬이다. null object는 전역 변수로 선언하면 충분하다.

 그리고 null object가 올 수 있는 곳과 null object가 오지 않을 곳을 구분하지 못한다. 만약 Option monad를 썼다면, Option monad는 자신이 가지는 T 타입에 제약이 없다. 값이 없을 수 있는 곳에서만 Option[T] 타입을 사용하면 된다. Option[T]가 오는 곳은 값이 없을 수 있는 곳이고, 무조건 값이 있게 하려면 그냥 T 타입이 오도록 하면 된다. 이는 컴파일 타임에 검증이 된다. 하지만 null object는 어디에서는 null object를 올 수 있고, 어디에서는 null object가 올 수 없다는 제약을 타입에 줄 수 없다. 따라서 컴파일 타임에 에러를 검증하지 못하고, 무조건 assert를 통해서 검출해야 한다.

 그보다 더 찾기 어려운 문제는 null object pattern이 문제를 숨김으로써 버그를 늦게 발견하게 될 수 있다는 것이다. null object는 아무것도 하지 않는다. 그래서 실제로 존재해야 하는 객체가 null object가 되었어도 프로그램은 죽지 않는다. 문제는 지금 당장 죽지 않기 때문에 나중에 다른 어디선가 문제를 발생시킬 수도 있다는 것이다. 이런 단점 때문에 앞에서 말했듯이 null object pattern이 안티 패턴이라고 불리기도 하는 것이다.

2015-09-24 00:03 1) 링크 추가