널 오브젝트 패턴

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

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

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

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

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

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

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

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

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


```java
class Node {
    Node left;
    Node right;
}
```


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


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


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


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


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


```java
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부터 "?." 연산자 (일명 널 조건부 연산자)를 사용할 수 있으며, 이는 왼쪽 피연산자가 널이면 단순히 널로 평가된다.

```csharp
// 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를 사용할 수도 있다. 다음 예제는 널의 존재를 신경 쓰지 않거나 널과 빈 문자열을 동일하게 취급하는 경우에만 작동한다. 이 가정은 다른 응용 프로그램에서는 적용되지 않을 수 있다.


```csharp
// 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<int> 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++

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


```cpp
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++에서는 함수 또는 메서드의 설계가 널을 허용하는지 여부를 명시적으로 명시할 수 있다.


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

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


### C#

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


```csharp
/* 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` 클래스에 특화될 수 있으며, 따라서 널 디자인 패턴을 구현한다는 의미이다. 즉, 객체 시스템에 본질적으로 내장되어 있다:

```lisp
;; 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에서는 예상되는 동작을 제공하기 위해 언어 상속이 필요하지 않다.

```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
덕 타이핑 언어인 자바스크립트에서는 예상되는 동작을 제공하기 위해 언어 상속이 필요하지 않다.

```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


```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

```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` 등)에서 자주 사용된다.


```vbnet
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의 널 객체 패턴

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