로그는 버그가 발생할 수 있는 곳이나 애플리케이션의 중요한 이벤트나 상태 변화가 있을 때 찍습니다. 로그를 만들어 놓으면 문제가 발생했을 때 신속하게 대응할 수 있고 어디에서든 필수로 만들어 놓아야 합니다. 로그를 찍는 클래스를 어떻게 만들 수 있을까요?
로그 클래스의 특징을 생각해보면, 로그는 어느 위치에서든 사용될 수 있으며 각 클래스마다 인스턴스를 생성해서 로그를 찍으면 메모리 낭비가 발생할 수 있습니다. 이를 해결하기 위해 싱글턴 패턴을 사용하여 로그 클래스의 인스턴스를 하나만 생성하는 것이 좋습니다. 싱글턴 패턴은 단 하나의 인스턴스만 생성되도록 강제하는 패턴으로, 단 하나의 인스턴스를 생성하는 것입니다.

따라서 싱글턴 패턴을 사용한 로그 클래스를 만들면 여러 클래스에서 해당 클래스의 인스턴스를 생성하지 않고도 공통된 로그 기능을 사용할 수 있으며, 메모리를 효율적으로 관리할 수 있습니다.
이를 통해 싱글턴 패턴에 대해 알아보고, 이를 실제로 구현함으로써 싱글턴 패턴이 어떻게 동작하는지 이해할 수 있습니다.
먼저 싱글턴 패턴의 장단점입니다.
싱글턴 패턴의 장단점
| 장점 | 단점 |
|---|---|
| - 유일한 인스턴스를 보장함 | - 글로벌 상태로 복잡성 증가 |
| - 전역적인 접근 가능 | - 테스트가 어려워짐 |
| - 필요할 때 인스턴스 생성하여 메모리 절약 | - 의존성을 모킹하기 어려워짐 |
| - 상속을 통한 확장 용이 | - 멀티스레드 환경에서의 문제 가능성 |
장단점이 있기 때문에 상황에 맞게 필요할 때만 사용합니다.
구현에 들어가보겠습니다.
구현할 예시는 Logging을 하는 Logger class입니다.
이 클래스는 어디에서든 LOGGING이라는 함수를 통해 로그를 찍을 수 있어야 하고, 인스턴스는 어떠한 경우에서도 단 하나만 생성되어야 합니다.
먼저 어떤 구조로 짤 지 diagram을 그려봅니다. 예시는 main문에서도 Logger 클래스를 사용하고 SingletonTestClass에서도 Logger 클래스를 사용합니다.

다음은 코드입니다.
코드로 들어가기 전에 Logger class를 어떻게 구현해야 할 지 생각해봅니다.
1. 클래스의 생성자를 private으로 선언하여 외부에서 직접 인스턴스를 생성하지 못하도록 합니다.
2. 복사 생성자와 대입 연산자를 private으로 선언하거나 삭제하여 복사 및 대입을 막습니다. (C++에서는 복사 생성자 및 대입 연산자가 자동으로 생성됩니다. 다른 언어로 구현할 경우는 자동으로 생성되지 않을 수 있습니다.)
3. 정적 멤버 변수로 유일한 인스턴스를 보관하는 static 멤버 변수를 클래스 내부에 선언합니다
4. 인스턴스를 반환하는 static method를 구현합니다. 여러 thread에서 instance를 가져오면 인스턴스가 중복되어 생성할 수도 있습니다. mutex lock을 통해 하나의 thread씩 접근할 수 있게 합니다.
이렇게 정리했다면 구현해봅니다.
#include <iostream>
#include <string>
#include <mutex>
class Logger {
public:
// 유일한 인스턴스를 반환하는 정적 메서드
static Logger& getInstance() {
// 정적 로컬 변수는 처음 호출될 때 한 번만 초기화되므로 thread-safe합니다.
static std::mutex mtx;
// mutex lock을 통해 하나의 thread만 instance를 생성하도록 만듭니다.
std::lock_guard<std::mutex> lock(mtx);
static Logger instance;
return instance;
}
// 로그를 출력하는 메서드
void log(const std::string& message) {
std::lock_guard<std::mutex> lock(log_mtx);
std::cout << message << std::endl;
}
private:
// 외부에서 인스턴스 생성을 방지
Logger() {}
// 로그 메서드에 대한 뮤텍스
std::mutex log_mtx;
// 복사 생성자와 대입 연산자를 삭제하여 싱글턴 속성을 보장
Logger(const Logger&) = delete;
Logger& operator=(const Logger&) = delete;
};
class SingletonTestClass {
public:
SingletonTestClass() {};
~SingletonTestClass() {};
void testSingleton() {
Logger::getInstance().log("testSingleton success");
std::cout << "SingletonTestClass 에서의 Logger instance 주소" << &Logger::getInstance() << std::endl;
}
};
int main() {
Logger::getInstance().log("Application started");
std::cout << "main문에서의 Logger instance 주소" << &Logger::getInstance() << std::endl;
// 다른 클래스에서도 동일한 인스턴스를 사용
SingletonTestClass singletonTestClass;
singletonTestClass.testSingleton();
return 0;
}

어떤 클래스에서도 같은 instance를 사용하는 Logger 인스턴스를 만들어 사용했습니다.
추가로 Logger class를 작성할 때 아래 고려 사항이 있는데, singleton 을 구현하기 위한 클래스로 이는 생략 했습니다.
- 로그 레벨 지정: 로그는 다양한 레벨로 구분되어 중요도에 따라 기록됩니다. 이를 위해 로그 레벨을 설정할 수 있도록 하여 필요에 따라 로그의 상세도를 조절할 수 있습니다.
- 로그 형식 지정: 로그 메시지의 형식을 지정하여 시간, 발생 위치, 메시지 내용 등을 포함하여 가독성을 높입니다.
- 로그 저장: 로그는 파일이나 데이터베이스 등에 저장되어야 합니다. 이를 위해 로그를 저장하는 메커니즘을 클래스 내에 구현하여 로그를 영구적으로 기록할 수 있도록 합니다.
아래는 장단점에 대한 좀 더 자세한 설명입니다.
장점
유일한 인스턴스 보장
- 싱글턴 패턴을 사용하면 클래스의 인스턴스가 단 하나만 생성되므로, 인스턴스가 여러 개 생성되지 않도록 보장할 수 있습니다.
예를 들어, 로그 관리, 설정 관리, 스레드 풀 등과 같은 리소스가 제한된 경우에 유용합니다.
글로벌 접근
- 싱글턴 인스턴스는 전역적으로 접근할 수 있으므로, 어디에서나 동일한 인스턴스를 사용할 수 있습니다.
코드 전반에서 동일한 인스턴스를 사용하여 일관성을 유지할 수 있습니다.
지연 초기화 (Lazy Initialization)
- 필요할 때까지 인스턴스를 생성하지 않으므로, 메모리와 리소스를 절약할 수 있습니다.
- 이점은 초기화 비용이 큰 객체에 특히 유용합니다.
상속을 통한 확장 용이
- 싱글턴 클래스를 상속하여 확장할 수 있습니다. 이를 통해 유연성과 재사용성을 높일 수 있습니다.
단점
글로벌 상태
- 싱글턴 패턴은 전역 상태를 만들기 때문에, 프로그램이 복잡해질 수 있습니다. 전역 상태는 디버깅과 테스트를 어렵게 만듭니다.
- 글로벌 상태로 인해 예상치 못한 사이드 이펙트가 발생할 수 있습니다.
테스트 어려움
- 싱글턴 패턴을 사용하면 의존성을 모킹(mocking)하기 어려워질 수 있어 단위 테스트가 어려워질 수 있습니다.
- 테스트 중에 싱글턴 인스턴스를 재설정하거나 재초기화하기가 어렵습니다.
멀티스레드 환경에서의 문제
- 싱글턴 패턴을 멀티스레드 환경에서 안전하게 구현하지 않으면 레이스 컨디션(race condition) 문제가 발생할 수 있습니다.
- 이를 방지하기 위해 추가적인 동기화 메커니즘이 필요합니다.
SRP (단일 책임 원칙) 위반 가능성
- 싱글턴 클래스가 너무 많은 책임을 가지게 되어 단일 책임 원칙(Single Responsibility Principle)을 위반할 수 있습니다.
- 이는 클래스의 응집도를 낮추고 유지보수를 어렵게 만듭니다.
'IT > Design Pattern' 카테고리의 다른 글
| Observer Pattern (0) | 2024.05.15 |
|---|---|
| Factory pattern (0) | 2024.05.12 |