널 오브젝트 패턴

널 오브젝트 패턴(null object pattern) 또는 널 객체 패턴에 관한 내용이다.

객체 지향 컴퓨터 프로그래밍에서 널 객체는 참조된 값이 없거나 정의된 중립 (널) 동작을 가진 객체이다. 이러한 객체의 사용법과 동작 (또는 그 부재)을 설명하는 널 객체 디자인 패턴은 "Void Value"로 처음 출판되었고
나중에 Pattern Languages of Program Design 서적 시리즈에서 "Null Object"로 출판되었다.

동기
자바 또는 C#과 같은 대부분의 객체 지향 언어에서 참조는 널일 수 있다. 메서드는 일반적으로 널 참조에 대해 호출될 수 없으므로, 어떤 메서드를 호출하기 전에 이러한 참조가 널이 아닌지 확인해야 한다.

Objective-C 언어는 이 문제에 대해 다른 접근 방식을 취하며 nil에 메시지를 보낼 때 아무것도 하지 않는다. 반환 값이 예상되는 경우, 객체에 대해서는 nil, 숫자 값에 대해서는 0, BOOL 값에 대해서는 NO, 또는 모든 멤버가 null/0/NO/0으로 초기화된 구조체 (구조체 타입의 경우)가 반환된다.

 설명 
객체의 부재 (예를 들어, 존재하지 않는 고객)를 전달하기 위해 널 참조를 사용하는 대신, 예상되는 인터페이스를 구현하지만 메서드 본문이 비어 있는 객체를 사용한다. 널 객체 사용의 주요 목적은 다양한 종류의 조건문을 피하여 코드를 더 집중적이고 읽고 따르기 쉽게 만드는 것이다 즉, 가독성을 향상시키는 것이다.
작동하는 기본 구현보다 이 접근 방식의 한 가지 장점은 널 객체가 매우 예측 가능하고 부작용이 없다는 것이다: 아무것도 하지 않는다.

예를 들어, 함수는 폴더의 파일 목록을 검색하고 각 파일에 대해 어떤 작업을 수행할 수 있다. 빈 폴더의 경우, 응답 중 하나는 예외를 발생시키거나 목록 대신 널 참조를 반환하는 것일 수 있다. 따라서 목록을 기대하는 코드는 계속 진행하기 전에 실제로 목록이 있는지 확인해야 하며, 이는 설계를 복잡하게 만들 수 있다.

대신 널 객체 (즉, 빈 목록)를 반환함으로써 반환 값이 실제로 목록인지 확인할 필요가 없다. 호출 함수는 평소와 같이 목록을 반복하여 효과적으로 아무것도 하지 않을 수 있다. 그러나 반환 값이 널 객체 (빈 목록)인지 확인하고 원하는 경우 다르게 반응할 수도 있다.

널 객체 패턴은 데이터베이스와 같은 특정 기능이 테스트에 사용할 수 없는 경우 테스트를 위한 스텁 역할도 할 수 있다.

예시
이 노드 구조를 가진 이진 트리가 주어졌을 때:

class Node {
 Node left;
 Node right;
}

재귀적으로 트리 크기 절차를 구현할 수 있다:

int treeSize(Node node) {
 return 1 + treeSize(node.left) + treeSize(node.right);
}

자식 노드가 존재하지 않을 수 있으므로, 비존재 또는 널 검사를 추가하여 절차를 수정해야 한다:

int treeSize(node) {
 int sum = 1;
 if (node.left != null) {
 sum += treeSize(node.left);
 }
 if (node.right != null) {
 sum += treeSize(node.right);
 }
 return sum;
}

그러나 이는 경계 검사와 일반 로직을 혼합하여 절차를 더 복잡하게 만들고 읽기 어렵게 만든다. 널 객체 패턴을 사용하여 특별한 버전의 절차를 만들 수 있지만 널 노드에 대해서만 가능하다:

int treeSize(Node node) {
 if (node == null) {
 return 0;
 } else {
 return 1 + treeSize(node.left) + treeSize(node.right);
 }
}

// 삼항 연산자를 사용하는 경우:
int treeSize(Node node) {
 return node != null ? 1 + treeSize(node.left) + treeSize(node.right) : 0;
}

이는 일반 로직과 특수 사례 처리를 분리하여 코드를 이해하기 쉽게 만든다.

다른 패턴과의 관계
상태 패턴 및 전략 패턴의 특수 사례로 간주될 수 있다.

이것은 디자인 패턴 (책)의 패턴은 아니지만 마틴 파울러의 Refactoring 및 조슈아 케리에브스키(Joshua Kerievsky)의 Refactoring To Patterns에서 Insert Null Object 리팩토링으로 언급된다.

로버트 C. 마틴의 Agile Software Development: Principles, Patterns and Practices 17장은 이 패턴에 전념한다.

대안
C# 6.0부터 "?." 연산자 (일명 널 조건부 연산자)를 사용할 수 있으며, 이는 왼쪽 피연산자가 널이면 단순히 널로 평가된다.
// compile as Console Application, requires C# 6.0 or higher
using System;

namespace ConsoleApplication2
{
 class Program
 {
 static void Main(string[] args)
 {
 string str = "test";
 Console.WriteLine(str?.Length);
 Console.ReadKey();
 }
 }
}
// The output will be:
// 4

확장 메서드 및 널 병합

일부 Microsoft .NET 언어에서는 확장 메서드를 사용하여 '널 병합'이라고 하는 작업을 수행할 수 있다. 이는 확장 메서드가 '인스턴스 메서드 호출'인 것처럼 널 값에 대해 호출될 수 있지만 실제로는 확장 메서드가 정적이기 때문이다. 확장 메서드는 널 값을 확인하도록 만들어질 수 있으므로, 이를 사용하는 코드가 그렇게 할 필요가 없어진다. 아래 예제는 널 병합 연산자를 사용하여 오류 없는 호출을 보장하지만, 더 평범한 if...then...else를 사용할 수도 있다. 다음 예제는 널의 존재를 신경 쓰지 않거나 널과 빈 문자열을 동일하게 취급하는 경우에만 작동한다. 이 가정은 다른 응용 프로그램에서는 적용되지 않을 수 있다.

// compile as Console Application, requires C# 3.0 or higher
using System;
using System.Linq;
namespace MyExtensionWithExample {
 public static class StringExtensions {
 public static int SafeGetLength(this string valueOrNull) {
 return (valueOrNull ?? String.Empty).Length;
 }
 }

 public static class Program {
 // define some strings
 static readonly string[] strings = new[] { "Mr X.", "Katrien Duck", null, "Q" };
 // write the total length of all the strings in the array
 public static void Main(string[] args) {
 IEnumerable
 query = from text in strings select text.SafeGetLength(); // no need to do any checks here
 Console.WriteLine(query.Sum());
 }
 }
}
// The output will be:
// 18

다양한 언어에서
C++

객체에 대한 정적 타입 참조를 가진 언어는 널 객체가 어떻게 더 복잡한 패턴이 되는지를 보여준다:

import std;

class Animal {
public:
 virtual ~Animal() = default;

 virtual void makeSound() const = 0;
};

class Dog: public Animal {
public:
 virtual void makeSound() const override {
 std::println("woof!");
 }
};

class NullAnimal: public Animal {
public:
 virtual void makeSound() const override {}
};

여기서 아이디어는 Animal 객체에 대한 포인터 또는 참조가 필요하지만 적절한 객체가 없는 상황이 있다는 것이다. 표준을 준수하는 C++에서는 널 참조가 불가능하다. 널 Animal* 포인터는 가능하며, 자리 표시자로 유용할 수 있지만 직접 디스패치에는 사용할 수 없다: a가 널 포인터이면 a->MakeSound()는 미정의 동작이다.

널 객체 패턴은 Animal 포인터 또는 참조에 바인딩될 수 있는 특수 NullAnimal 클래스를 제공함으로써 이 문제를 해결한다.

널 객체를 가져야 하는 각 클래스 계층에 대해 특수 널 클래스를 생성해야 한다. NullAnimal은 Animal 계층과 관련이 없는 Widget 기본 클래스에 대한 널 객체가 필요할 때 쓸모가 없기 때문이다.

"모든 것이 참조"인 언어 (예: Java 및 C#)와 달리 널 클래스가 전혀 없다는 것이 중요한 기능이라는 점에 유의해야 한다. C++에서는 함수 또는 메서드의 설계가 널을 허용하는지 여부를 명시적으로 명시할 수 있다.

// Animal 인스턴스를 요구하며 널을 허용하지 않는 함수.
void doSomething(const Animal& animal) {
 // animal은 여기서 절대로 널이 될 수 없다.
}

// Animal 인스턴스 또는 널을 허용할 수 있는 함수.
void doSomething(const Animal* animal) {
 // animal은 널일 수 있다.
}

C#

C#은 널 객체 패턴을 제대로 구현할 수 있는 언어이다. 이 예시는 소리를 내는 동물 객체와 C# 널 키워드를 대신하여 사용되는 NullAnimal 인스턴스를 보여준다. 널 객체는 일관된 동작을 제공하고, C# 널 키워드를 대신 사용했을 때 발생할 수 있는 런타임 널 참조 예외를 방지한다.

/* Null object pattern implementation:
 */
using System;

// Animal interface is the key to compatibility for Animal implementations below.
interface IAnimal
{
 void MakeSound();
}

// Animal is the base case.
abstract class Animal : IAnimal
{
 // A shared instance that can be used for comparisons
 public static readonly IAnimal Null = new NullAnimal();

 // The Null Case: this NullAnimal class should be used in place of C# null keyword.
 private class NullAnimal : Animal
 {
 public override void MakeSound()
 {
 // Purposefully provides no behaviour.
 }
 }
 public abstract void MakeSound();
}

// Dog is a real animal.
class Dog : Animal
{
 public override void MakeSound()
 {
 Console.WriteLine("Woof!");
 }
}

/* =========================
 * Simplistic usage example in a Main entry point.
 */
static class Program
{
 static void Main()
 {
 IAnimal dog = new Dog();
 dog.MakeSound(); // outputs "Woof!"

 /* Instead of using C# null, use the Animal.Null instance.
 * This example is simplistic but conveys the idea that if the Animal.Null instance is used then the program
 * will never experience a .NET System.NullReferenceException at runtime, unlike if C# null were used.
 */
 IAnimal unknown = Animal.Null; //<< replaces: IAnimal unknown = null;
 unknown.MakeSound(); // outputs nothing, but does not throw a runtime exception
 }
}

Smalltalk

Smalltalk 원칙에 따라, 모든 것은 객체이며, 객체의 부재는 nil이라는 객체로 모델링된다. 예를 들어 GNU Smalltalk에서 nil의 클래스는 UndefinedObject이며, Object의 직계 후손이다.

목적에 맞는 합리적인 객체를 반환하지 못하는 모든 연산은 대신 nil을 반환하여 Smalltalk 설계자들이 지원하지 않는 "객체 없음"이라는 특수 사례를 피한다. 이 방법은 고전적인 "널" 또는 "객체 없음" 또는 "널 참조" 접근 방식보다 단순하다는 장점 (특수 사례가 필요 없음)이 있다. nil과 함께 사용하기 특히 유용한 메시지는 isNil, ifNil: 또는 ifNotNil:이며, Smalltalk 프로그램에서 nil에 대한 가능한 참조를 처리하는 것을 실용적이고 안전하게 만든다.

Common Lisp

Lisp에서 함수는 특수 객체 nil을 우아하게 받아들일 수 있어 응용 프로그램 코드에서 특수 사례 테스트의 양을 줄인다. 예를 들어, nil은 원자이고 어떤 필드도 없지만, 함수 car 및 cdr은 nil을 받아들이고 그냥 반환하는데, 이는 매우 유용하며 코드를 더 짧게 만든다.

nil이 Lisp에서 빈 목록이기 때문에 위 서론에서 설명한 상황은 존재하지 않는다. nil을 반환하는 코드는 실제로 빈 목록을 반환하는 것이므로 (목록 타입에 대한 널 참조와 유사한 것이 아님), 호출자는 값이 목록인지 아닌지 테스트할 필요가 없다.

널 객체 패턴은 다중 값 처리에서도 지원된다. 프로그램이 값을 반환하지 않는 식에서 값을 추출하려고 하면, 널 객체 nil이 대체된다.
따라서 (list (values))는 (nil) (nil을 포함하는 한 요소 목록)을 반환한다. (values) 식은 전혀 값을 반환하지 않지만, list 함수 호출이 인자 식을 값으로 줄여야 하므로 널 객체가 자동으로 대체된다.

CLOS

Common Lisp에서 객체 nil은 특수 클래스 null의 유일한 인스턴스이다. 이것은 메서드가 null 클래스에 특화될 수 있으며, 따라서 널 디자인 패턴을 구현한다는 의미이다. 즉, 객체 시스템에 본질적으로 내장되어 있다:
;; empty dog class

(defclass dog () ())

;; a dog object makes a sound by barking: woof! is printed on standard output
;; when (make-sound x) is called, if x is an instance of the dog class.

(defmethod make-sound ((obj dog))
 (format t "woof!~%"))

;; allow (make-sound nil) to work via specialization to null class.
;; innocuous empty body: nil makes no sound.
(defmethod make-sound ((obj null)))
클래스 null은 nil이 심볼이기 때문에 symbol 클래스의 서브클래스이다.
nil은 또한 빈 목록을 나타내므로, null은 list 클래스의 서브클래스이기도 하다. symbol 또는 list에 특화된 메서드 매개변수는 따라서 nil 인수를 취할 것이다. 물론, nil에 대해 더 구체적인 일치인 null 특화가 여전히 정의될 수 있다.

Scheme

Common Lisp 및 많은 Lisp 방언과 달리 Scheme 방언은 이런 식으로 작동하는 nil 값을 가지고 있지 않다. car 및 cdr 함수는 빈 목록에 적용할 수 없다. 따라서 Scheme 응용 프로그램 코드는 empty? 또는 pair? 술어 함수를 사용하여 이 상황을 회피해야 한다. 이는 nil의 동작 덕분에 빈 경우와 비어 있지 않은 경우를 구별할 필요가 없는 매우 유사한 Lisp 상황에서도 마찬가지이다.

Ruby

덕 타이핑 언어인 Ruby에서는 예상되는 동작을 제공하기 위해 언어 상속이 필요하지 않다.
class Dog
 def sound
 "bark"
 end
end

class NilAnimal
 def sound(*); end
end

def get_animal(animal=NilAnimal.new)
 animal
end

get_animal(Dog.new).sound
 => "bark"
get_animal.sound
 => nil
명시적 구현을 제공하는 대신 NilClass를 직접 몽키 패치하려는 시도는 이점보다 더 예상치 못한 부작용을 낳는다.

JavaScript
덕 타이핑 언어인 자바스크립트에서는 예상되는 동작을 제공하기 위해 언어 상속이 필요하지 않다.
class Dog {
 sound() {
 return 'bark';
 }
}

class NullAnimal {
 sound() {
 return null;
 }
}

function getAnimal(type) {
 return type === 'dog' ? new Dog() : new NullAnimal();
}

['dog', null].map((animal) => getAnimal(animal).sound());
// Returns ["bark", null]

Java

public interface Animal {
 void makeSound();
}

public class Dog implements Animal {
 public void makeSound() {
 System.out.println("woof!");
 }
}

public class NullAnimal implements Animal {
 public void makeSound() {
 // silence...
 }
}

이 코드는 위 C++ 예제의 변형을 Java 언어를 사용하여 보여준다. C++와 마찬가지로, Animal 객체에 대한 참조가 필요하지만 적절한 객체가 없는 상황에서 널 클래스를 인스턴스화할 수 있다. 널 Animal 객체는 가능하지만 (Animal myAnimal = null;) 자리 표시자로 유용할 수 있지만 메서드 호출에는 사용할 수 없다. 이 예제에서 myAnimal.makeSound();는 NullPointerException을 발생시킨다. 따라서 널 객체를 테스트하기 위해 추가 코드가 필요할 수 있다.

널 객체 패턴은 Animal 타입의 객체로 인스턴스화될 수 있는 특수 NullAnimal 클래스를 제공함으로써 이 문제를 해결한다. C++ 및 관련 언어와 마찬가지로, 이 특수 널 클래스는 널 객체가 필요한 각 클래스 계층에 대해 생성되어야 한다. NullAnimal은 Animal 인터페이스를 구현하지 않는 널 객체가 필요할 때 쓸모가 없기 때문이다.

 PHP 
interface Animal
{
 public function makeSound();
}

class Dog implements Animal
{
 public function makeSound()
 {
 echo "Woof...\n";
 }
}

class Cat implements Animal
{
 public function makeSound()
 {
 echo "Meowww...\n";
 }
}

class NullAnimal implements Animal
{
 public function makeSound()
 {
 // silence...
 }
}

$animalType = 'elephant';

function makeAnimalFromAnimalType(string $animalType): Animal
{
 switch ($animalType) {
 case 'dog':
 return new Dog();
 case 'cat':
 return new Cat();
 default:
 return new NullAnimal();
 }
}

makeAnimalFromAnimalType($animalType)->makeSound(); // ..the null animal makes no sound

function animalMakeSound(Animal $animal): void
{
 $animal->makeSound();
}

foreach ([
 makeAnimalFromAnimalType('dog'),
 makeAnimalFromAnimalType('NullAnimal'),
 makeAnimalFromAnimalType('cat'),
 ] as $animal) {
 // That's also reduce null handling code
 animalMakeSound($animal);
}

 Visual Basic .NET 
다음 널 객체 패턴 구현은 구체적인 클래스가 해당 널 객체를 정적 필드 Empty로 제공하는 것을 보여준다. 이 접근 방식은 .NET 프레임워크(String.Empty, EventArgs.Empty, Guid.Empty 등)에서 자주 사용된다.

Public Class Animal
 Public Shared ReadOnly Empty As Animal = New AnimalEmpty()

 Public Overridable Sub MakeSound()
 Console.WriteLine("Woof!")
 End Sub
End Class

Friend NotInheritable Class AnimalEmpty
 Inherits Animal

 Public Overrides Sub MakeSound()
 '
 End Sub
End Class

비판
이 패턴은 오류/버그가 정상 프로그램 실행처럼 보이게 만들 수 있으므로 신중하게 사용해야 한다.

널 검사를 피하고 코드를 더 읽기 쉽게 만들려는 목적으로만 이 패턴을 구현하지 않도록 주의해야 한다. 왜냐하면 더 읽기 어려운 코드가 다른 곳으로 이동하여 표준이 아닐 수 있기 때문이다. 예를 들어, 제공된 객체가 실제로 널 객체인 경우 다른 로직이 실행되어야 할 때 그렇다. 대부분의 참조 타입을 가진 언어의 일반적인 패턴은 참조를 널 또는 nil이라고 불리는 단일 값과 비교하는 것이다. 또한, 어디에서도 널 객체 대신 널을 할당하지 않는지 테스트해야 할 필요가 있다. 왜냐하면 대부분의 경우, 그리고 정적 타이핑을 가진 언어에서는 널 객체가 참조 타입이라 할지라도 컴파일러 오류가 아니지만, 패턴이 널 검사를 피하는 데 사용된 코드 부분에서는 런타임에 오류를 유발할 것이기 때문이다. 게다가, 대부분의 언어에서, 그리고 많은 널 객체가 있을 수 있다고 가정할 때 (즉, 널 객체가 참조 타입이지만 어떤 방식으로든 싱글턴 패턴을 구현하지 않는 경우), 널 또는 nil 값 대신 널 객체를 확인하는 것은 오버헤드를 발생시키며, 싱글턴 패턴 자체도 싱글턴 참조를 얻을 때 마찬가지이다.

 같이 보기 
 널러블 타입
 옵션 타입

 각주 

 외부 링크 
 제프리 워커(Jeffery Walker)의 널 객체 패턴 설명
 마틴 파울러(Martin Fowler)의 Special Case에 대한 설명, 약간 더 일반적인 패턴
 널 객체 패턴 재방문
 널 객체 도입 리팩토링
 SourceMaking 튜토리얼
 Swift의 널 객체 패턴

분류:소프트웨어 디자인 패턴