널 오브젝트 패턴

**널 오브젝트 패턴**(null object pattern) 또는 **널 객체 패턴**에 관한 내용이다. 객체 지향 컴퓨터 프로그래밍에서 **널 객체**는 참조된 값이 없거나 정의된 중립 (널) 동작을 가진 객체이다. 이러한 객체의 사용법과 동작 (또는 그 부재)을 설명하는 널 객체 디자인 패턴은 "Void Value"로 처음 출판되었고year = 1996 나중에 Pattern Languages of Program Design 서적 시리즈에서 "Null Object"로 출판되었다.publisher = Addison-Wesley
# 동기
자바 또는 C#과 같은 대부분의 객체 지향 언어에서 참조는 널일 수 있다. 메서드는 일반적으로 널 참조에 대해 호출될 수 없으므로, 어떤 메서드를 호출하기 전에 이러한 참조가 널이 아닌지 확인해야 한다. Objective-C 언어는 이 문제에 대해 다른 접근 방식을 취하며 `nil`에 메시지를 보낼 때 아무것도 하지 않는다. 반환 값이 예상되는 경우, 객체에 대해서는 `nil`, 숫자 값에 대해서는 0, `BOOL` 값에 대해서는 `NO`, 또는 모든 멤버가 `null`/0/`NO`/0으로 초기화된 구조체 (구조체 타입의 경우)가 반환된다.publisher = Apple, Inc.
# 설명
객체의 부재 (예를 들어, 존재하지 않는 고객)를 전달하기 위해 널 참조를 사용하는 대신, 예상되는 인터페이스를 구현하지만 메서드 본문이 비어 있는 객체를 사용한다. 널 객체 사용의 주요 목적은 다양한 종류의 조건문을 피하여 코드를 더 집중적이고 읽고 따르기 쉽게 만드는 것이다Snd 즉, 가독성을 향상시키는 것이다. 작동하는 기본 구현보다 이 접근 방식의 한 가지 장점은 널 객체가 매우 예측 가능하고 부작용이 없다는 것이다: 아무것도 하지 않는다. 예를 들어, 함수는 폴더의 파일 목록을 검색하고 각 파일에 대해 어떤 작업을 수행할 수 있다. 빈 폴더의 경우, 응답 중 하나는 예외를 발생시키거나 목록 대신 널 참조를 반환하는 것일 수 있다. 따라서 목록을 기대하는 코드는 계속 진행하기 전에 실제로 목록이 있는지 확인해야 하며, 이는 설계를 복잡하게 만들 수 있다. 대신 널 객체 (즉, 빈 목록)를 반환함으로써 반환 값이 실제로 목록인지 확인할 필요가 없다. 호출 함수는 평소와 같이 목록을 반복하여 효과적으로 아무것도 하지 않을 수 있다. 그러나 반환 값이 널 객체 (빈 목록)인지 확인하고 원하는 경우 다르게 반응할 수도 있다. 널 객체 패턴은 데이터베이스와 같은 특정 기능이 테스트에 사용할 수 없는 경우 테스트를 위한 스텁 역할도 할 수 있다.
# 예시
이 노드 구조를 가진 이진 트리가 주어졌을 때: 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; } 이는 일반 로직과 특수 사례 처리를 분리하여 코드를 이해하기 쉽게 만든다.
# 다른 패턴과의 관계
상태 패턴 및 전략 패턴의 특수 사례로 간주될 수 있다. 이것은 디자인 패턴 (책)의 패턴은 아니지만 마틴 파울러의 Refactoringurl = https://archive.org/details/isbn_9780201485677 및 조슈아 케리에브스키(Joshua Kerievsky)의 Refactoring To Patternsisbn = 0-321-21335-1에서 Insert Null Object 리팩토링으로 언급된다. 로버트 C. 마틴의 Agile Software Development: Principles, Patterns and Practicesurl = https://archive.org/details/agilesoftwaredev00robe 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; // "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
# 비판
이 패턴은 오류/버그가 정상 프로그램 실행처럼 보이게 만들 수 있으므로 신중하게 사용해야 한다.Fowler, Martin (1999). Refactoring, p. 216. 널 검사를 피하고 코드를 더 읽기 쉽게 만들려는 목적으로만 이 패턴을 구현하지 않도록 주의해야 한다. 왜냐하면 더 읽기 어려운 코드가 다른 곳으로 이동하여 표준이 아닐 수 있기 때문이다. 예를 들어, 제공된 객체가 실제로 널 객체인 경우 다른 로직이 실행되어야 할 때 그렇다. 대부분의 참조 타입을 가진 언어의 일반적인 패턴은 참조를 널 또는 nil이라고 불리는 단일 값과 비교하는 것이다. 또한, 어디에서도 널 객체 대신 널을 할당하지 않는지 테스트해야 할 필요가 있다. 왜냐하면 대부분의 경우, 그리고 정적 타이핑을 가진 언어에서는 널 객체가 참조 타입이라 할지라도 컴파일러 오류가 아니지만, 패턴이 널 검사를 피하는 데 사용된 코드 부분에서는 런타임에 오류를 유발할 것이기 때문이다. 게다가, 대부분의 언어에서, 그리고 많은 널 객체가 있을 수 있다고 가정할 때 (즉, 널 객체가 참조 타입이지만 어떤 방식으로든 싱글턴 패턴을 구현하지 않는 경우), 널 또는 nil 값 대신 널 객체를 확인하는 것은 오버헤드를 발생시키며, 싱글턴 패턴 자체도 싱글턴 참조를 얻을 때 마찬가지이다.