객체 지향 프로그래밍에서 디자인 패턴은 소프트웨어 개발 중에 발생하는 일반적인 문제에 대한 해결책의 모음이라 볼 수 있다. 이러한 패턴 중에서 싱글톤과 모노스테이트가 있으며, 두 패턴은 종종 같은 의미로 사용되지만 차이점이 있다. 이 글에서는 싱글톤과 모노스테이트 디자인 패턴을 살펴보고, 최신 소프트웨어 개발에서 종속성 주입이 중요한 이유에 정리해 보고자 한다.
싱글톤 디자인 패턴
싱글톤 패턴은 클래스에 인스턴스가 하나만 있도록 하고 전역 액세스 지점을 제공하는 디자인 패턴이다. 싱글톤 패턴은 비공개 생성자, 싱글톤 인스턴스를 가져오는 정적 메서드, 비공개 정적 인스턴스 변수가 특징이다.
C++ 11 이전 버전에서 싱글톤 패턴은 정적 멤버 함수를 사용하여 정적 인스턴스 변수를 반환하는 방식으로 구현할 수 있다. C++11 이상에서는 constexpr 지정자를 사용하여 정적 인스턴스 변수를 선언할 수 있다.
싱글톤 패턴은 특정 상황에서 유용할 수 있지만, 긴밀한 결합과 같은 문제를 일으킬 수 있으며 코드를 테스트하고 유지 관리하기 어렵게 만들 수 있다. 또한 싱글톤 패턴은 클래스를 변경할 이유가 하나만 있어야 한다는 단일 책임 원칙에 위배된다.
모노스테이트 디자인 패턴
“공유 정적(shared static)상태” 패턴이라고도 하는 모노스테이트(monostate) 패턴은 정적 데이터 멤버를 사용하여 클래스의 모든 인스턴스의 상태를 보유하는 디자인 패턴이다. 클래스의 모든 인스턴스는 동일한 데이터 멤버를 공유하며, 한 인스턴스가 데이터 멤버를 변경하면 다른 모든 인스턴스가 이를 볼 수 있다.
모노스테이트 패턴은 클래스에서 정적 데이터 멤버를 선언하고, 데이터 멤버를 조작하기 위해 게터 및 세터 메서드를 사용하여 구현한다.
class Database {
public:
void setConnectionString(const std::string& connectionString) {
connectionString_ = connectionString;
}
std::string getConnectionString() const {
return connectionString_;
}
private:
static std::string connectionString_;
};
std::string Database::connectionString_ = "default";
int main() {
Database db1;
db1.setConnectionString("mysql://localhost:3306/mydb");
Database db2;
std::cout << db2.getConnectionString() << std::endl;
return 0;
}
앞의 예제에서 db1과 db2는 모두 동일한 connectionString_ 데이터 멤버를 공유한다. db1에서 connectionString_을 “mysql://localhost:3306/mydb”로 설정하면 db2에서 동일한 값이 반환된다.
모노스테이트 패턴은 인스턴스화를 위해 전용 생성자나 정적 메서드에 의존하지 않는다는 점에서 싱글톤 패턴에 비해 장점이 있다. 그러나 싱글톤 패턴과 마찬가지로 클래스 간에 긴밀한 결합이 발생하게 된다.
싱글톤 패턴과 모노스테이트 패턴의 차이점
싱글톤과 모노스테이트 패턴은 비슷해 보일 수 있지만 차이점이 있다. 싱글톤 패턴에서는 클래스의 인스턴스가 하나만 존재하며, 해당 인스턴스에 대한 모든 액세스는 단일 함수 또는 접근자를 통해 이루어진다. 이와는 대조적으로 모노스테이트 패턴에서는 클래스의 인스턴스가 여러 개 있을 수 있지만 모두 동일한 상태를 공유한다.
실제 애플리케이션에서 모노스테이트 패턴 사용
모노스테이트 패턴은 클래스의 여러 인스턴스에 전역 상태를 적용하려는 상황에서 유용할 수 있다. 예를 들어 모든 플레이어가 달성한 최고 점수를 추적하려는 게임을 있다고 하자. 모노스테이트 클래스를 사용하여 최고 점수를 추적하면 플레이어 클래스의 모든 인스턴스가 동일한 최고 점수 값에 액세스하고 업데이트할 수 있다.
종속성 주입
종속성 주입의 정의 및 중요성
종속성 주입(DI)은 소프트웨어 엔지니어링에서 사용되는 기술로, 외부 소스로부터 종속성을 제공하여 애플리케이션의 구성 요소를 분리할 수 있다. DI를 통해 보다 모듈화되고 테스트 가능한 코드를 작성할 수 있는 이점을 얻을 수 있다.
DI에서는 종속성이 클래스 자체에서 생성되는 것이 아니라 런타임에 클래스에 주입된다. 따라서 유연성이 향상되고 코드를 더 쉽게 테스트할 수 있다.
파일에 메시지를 기록하는 Logger 클래스가 있다. 애플리케이션의 수명 기간 동안 Logger 클래스의 인스턴스가 하나만 존재하도록 싱글톤으로 구성한다. 싱글톤 패턴을 사용하는 대신 의존성 주입을 사용하여 Logger 클래스를 사용해야 하는 모든 클래스가 동일한 인스턴스를 참조하도록 할 수 있다.
먼저 파일 경로를 취하는 생성자가 있는 일반 클래스로 Logger 클래스를 생성한다.
class Logger {
public:
Logger(std::string filepath) {
file_.open(filepath, std::ofstream::out | std::ofstream::app);
}
void log(std::string message) {
file_ << message << std::endl;
}
private:
std::ofstream file_;
};
다음으로 Logger 클래스의 단일 인스턴스를 생성하고 여기에 shared_ptr을 반환하는 팩토리 함수를 생성한다.
std::shared_ptr<Logger> createLogger() {
static std::shared_ptr<Logger> logger = std::make_shared<Logger>("log.txt");
return logger;
}
createLogger() 함수는 정적 변수를 사용하여 Logger 클래스의 인스턴스가 하나만 생성되도록 한다.
이제 다른 클래스에서 Logger 클래스를 사용하려면 종속성으로 주입한다.
class MyClass {
public:
MyClass(std::shared_ptr<Logger> logger) : logger_(logger) {}
void doSomething() {
logger_->log("Doing something...");
}
private:
std::shared_ptr<Logger> logger_;
};
Logger 클래스를 종속성으로 주입하면 MyClass의 모든 인스턴스가 동일한 Logger 객체를 참조하도록 할 수 있다.
이러한 방식을 이용할 때 테스트 목적으로 Logger 클래스를 쉽게 모의 구현할 수 있다는 장점을 얻을 수 있다. 실제 Logger 클래스와 동일한 인터페이스를 구현하는 모의 Logger 클래스를 생성하고, 테스트를 위해 MyClass의 인스턴스에 주입할 수 있다. 이렇게 하면 Logger 클래스의 동작에 대해 걱정할 필요 없이 MyClass를 분리하여 테스트할 수 있다.
다음 MockLogger 클래스를 살펴보자.
class MockLogger : public Logger {
public:
void log(std::string message) override {
// Do nothing
}
};
이제 MyClass를 테스트하고 싶을 때 MockLogger 객체를 생성하여 전달하면 된다.
TEST(MyClassTest, DoSomethingLogsToLogger) {
MyClass myClass(std::make_shared<MockLogger>());
myClass.doSomething();
// Assert that logger.log() was called with the correct message
}
싱글톤 패턴 대신 의존성 주입을 사용함으로써 코드를 더 쉽게 테스트하고 유지 관리할 수 있다. 테스트 목적으로 종속성을 쉽게 mocking할 수 있으며 테스트에서 싱글톤 객체의 동작에 대해 걱정할 필요가 없다.
마무리
이 글에서는 싱글톤과 모노스테이트 디자인 패턴과 최신 소프트웨어 개발에서 의존성 주입의 중요성에 대해 살펴봤다. 싱글톤과 모노스테이트는 특정 상황에서 유용한 패턴이 될 수 있지만 단점이 있으며, 의존성 주입은 종속성 관리를 위한 보다 유연하고 유지 관리가 용이한 솔루션을 제공한다.
이러한 패턴과 개념을 이해함으로써 더 나은 코드를 작성하고 소프트웨어의 품질을 개선할 수 있다.