프로그래밍 일반/디자인 패턴

Null 객체

지노윈 2020. 3. 4. 21:41
반응형

인터페이스를 마음대로 선택하여 사용할 수 없는 경우가 있습니다.

예를들어 어떤 모듈의 특정 기능을 사용할 경우 원하지 이 모듈이 인터페이스에 내장되어 있을 수 있습니다.

이럴때 Null 객체를 이용합니다.

 

시나리오

다음과 같은 인터페이스를 가진 Logger 라이브러리를 사용한다고 가정합니다.

struct Logger
{
  virtual void info(const string& s) = 0;
};

Logger를 상속받은 ConsoleLogger를 구현합니다.

struct ConsoleLogger : Logger
{
  void info(const string& s) override
  {
    cout << s<< endl;
  }
};

상황에 따라서 로깅을 해서는 안 되는 경우가 있다면 어떻게 해야 할까?

Null 객체

BankAccount의 생성자는 다음과 같습니다.

BankAccount(const shared_ptr<Logger>& logger, const string& name, int balance)

Bacnk Account를 안전하게 생성할 수 있는 방법은 Logger의 Null 객체를 전달하는 것입니다.

Null 객체는 그 객체가 가진 인터페이스 규약을 모두 준수하면서도 실제 동작은 하지 않는 객체입니다.

struct NullLogger : Logger
{
  void info(const string& s) override {}
};

shared_ptr는 Null객체가 아니다

shared_ptr를 비롯한 스마트 포인터들은 Null 객체가 아닙니다.

Null 객체는 그 인터페이스가 약속에 따라 올바르게 동작하는 특성을 보존하면서도 실제로는 아무것도 하지 않는 객체입니다.

스마트 포인터는 역참조 연산자(operator*()), 포인터 멤버 참조 연산자(operator→())가 하는 역할이 동작하기 위해서는 초기화되지 않은 스마트 포인트는 있을 수 없습니다.

이러한 이유로 스마트 포인터는 Null 객체가 될 수 없습니다.

 

개선된 디자인

BankAccount를 좀더 쉽게 인터페이스를 바꿀 수 있을지에 대하여 생각해봅시다.

  • logger에 null을 지정 해도 되도록 합니다.
    logger를 사용하는 모든 코드에 null 인지를 검사하여 null이 아닌 겨우에만 호출 합니다.
    그렇지만 null이 되어도 된다는 사실을 사용자가 알아야만 합니다.
  • 디폴트 인자를 추가합니다. 즉, const shared_ptr<Logger>& logger = no_logging과 같이 설정합니다.
    이렇게 하더라도 null 체크는 마찬가지로 해주어야 합니다.
  • optional 타입을 이용합니다.
    이 방법은 관례적으로도 올바르고 의도적으로도 목적하는 바와 맞습니다.
    하지만 optional<shared_ptr<T>>을 전달하는 번거로움과 optional의 공백 여부를 확인 하는 작업들이 추가되어야만 합니다.

묵시적 Null 객체

또 다른 급진적인 아이디어가 있습니다. 호출과 집행 두 가지 절차로 나누어 로깅을 처리하는 것입니다.

즉, 인터페이스에 종속된 부분(호출)과 실제 로그를 저장하는 부분(집행)을 구분합니다.

다음은 OptionalLogger 클래스입니다.

struct OptionalLogger : Logger {
  shared_ptr<Logger> impl;
  static shared_ptr<Logger> no_logging;
  Logger(const shared_ptr<Logger>& logger) : impl{logger} {}
  virtual void info(const string& s) override {         // 로그를 저장하는 주분(집행)
    if(impl) impl->info(s);  // null 체크하여 logger가 no_logging이든 null이든 상관 없이 크래시 없이 동작 하도록 합니다.
  }
};
 
shared_ptr<Logger> BankAccount::no_logging{};

BankAccount의 생성자를 다음과 같이 정의합니다.

BankAccount(const sring& name, int balance, const shared_ptr<Logger>& logger = no_logging)    // 종속된 부분(호출)
 : logger{make_shared<OptionalLogger>(logger)},   // 핵심, OptionalLogger로 감쌉니다.
   name{name}, balance{balance} {}

logger 인자로 넘어온 객체를 바로 사용하지 않고 OptionLogger로 감쌌습니다.(프록시 패턴의 활용입니다.)

이렇게 함으로써 아래 코드는 크래시가 발생하지 않습니다.

BankAccount account{ "primary account", 1000, nullptr };
account.deposit(2000); // 크래시 발생 하지 않음

 

 

[독서 리뷰] - 모던 C++ 디자인 패턴

 

모던 C++ 디자인 패턴

[객체 지향 프로그래밍/디자인 패턴] - 빌더 패턴(Builder Pattern) [분류 전체보기] - 디자인 패턴 [객체 지향 프로그래밍/디자인 패턴] - 팩토리 패턴(Factory pattern) [객체 지향 프로그래밍/디자인 패턴] -..

devjino.tistory.com

코드 참고 : https://cpp-design-patterns.readthedocs.io/en/latest/
 

Welcome to C++ Design Patterns’s documentation! — C++ Design Patterns 0.0.1 documentation

© Copyright 2018, Hans-J. Schmid Revision 786c83f8.

cpp-design-patterns.readthedocs.io