인터페이스를 마음대로 선택하여 사용할 수 없는 경우가 있습니다.
예를들어 어떤 모듈의 특정 기능을 사용할 경우 원하지 이 모듈이 인터페이스에 내장되어 있을 수 있습니다.
이럴때 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); // 크래시 발생 하지 않음
코드 참고 : https://cpp-design-patterns.readthedocs.io/en/latest/
'프로그래밍 일반 > 디자인 패턴' 카테고리의 다른 글
상태 패턴(State Pattern) (0) | 2020.03.04 |
---|---|
관찰자 패턴(Observer Pattern) (0) | 2020.03.04 |
메멘토 패턴(Memento Pattern) (0) | 2020.03.04 |
커맨드 패턴(Command Pattern) (0) | 2020.03.04 |
중재자 패턴(Mediator Pattern) (0) | 2020.03.04 |