가장 많이 사용하는 패턴 중 하나인 싱글톤 패턴은 특정 클래스의 인스턴스가 프로그램 전체에 하나만 존재하도록 보장하는 것이다. 싱글톤 패턴을 사용하면 동일한 개체를 여러 번 생성하지 않고 일관된 접근성을 제공할 수 있다. 클래스가 스스로 자신의 유일한 인스턴스를 관리하고 이에 대해서 전역 접근 지점을 제공하는데 주로 리소스를 공유하거나 구성을 설정하는 데 사용한다.
싱글턴에 대해 간단하게 예시로 설명하면 아파트 단지에 하나만 있는 관리사무소로 이해하면 쉬울 것 같다. 아파트단지에 공통적으로 사용하는 관리사무소는 단지 내에 유일하고, 아파트 주민들은 관리 사무소를 통해 다양한 서비스를 받는다. 이 때문에 아파트 주민들은 관리 사무소를 직접 만들거나 복제할 필요가 없고, 이미 존재하는 관리사무소를 이용하면 된다.
1. 클래스의 생성자를 프라이빗(Private)으로 선언하고 외부에서 직접 인스턴스를 성성할 수 없게 한다.
public class Singleton {
private Singleton() {
// 생성자 내부 로직
}
}
2. 클래스 자신이 유일한 인스턴스를 Private Static 변수로 선언한다. 클래스가 로드될 때 초기화 되고, 클래스의 유일한 인스턴스를 참조함으로써 변수는 클래스 내부에서만 접근 가능하고 외부에서 접근할 수 없다.
public class Singleton {
private static Singleton instance;
private Singleton() {
// 생성자 내부 로직
}
}
3. 외부에서 인스턴스에 접근할 수 있는 퍼블릭 메서드를 제공하고 이 메서드는 내부적으로 인스턴스가 생성되어 있는지 확인하고 없으면 생성하고 이를 반환한다.
public class Singleton {
private static Singleton instance;
private Singleton() {
// 생성자 내부 로직
}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
그렇다면 싱글턴 패턴의 장점은 무엇일까?
인스턴스를 한 번만 생성하기 때문에 메모리 사용을 최소화할 수 있다. 공유 자원에 대해 중복 생성을 방지하였기 때문에 그만큼 리소스를 효율적으로 사용할 수 있는 것이다. 그리고 어디서나 접근할 수 있는 전역적인 지점을 제공함으로써 일관된 접근 방법을 갖고 갈 수 있다. 개체 생성 시점과 방법을 제어해 주기 때문에 Lazy Initialization (초기화를 지연시켜 주는)과 같은 기법으로 구현할 수도 있다.
단점으로는 싱글턴은 유연하지 못하다는 것이다. 전역상태를 가지고 있다는 것은 누군가 수정하면 예상하지 못한 문제가 발생할 수 있는 것이다. 또한 멀티스레드 환경에서 사용할 때 동기화문제가 발생할 수 있다. 여러 스레드가 동시에 인스턴스를 초기화하려 할 때 한 번만 생성되도록 관리하는 게 어려울 수 있다. 물론 이를 해결하기 위해 동기화를 추가하면 될 수 있지만 성능에 영향이 끼칠 수 있는 부분이다. 그리고 의존성을 코드에 직접 숨기기 때문에 어떤 개체들이 싱글톤을 사용하는지 명확하지 않을 수 있다. 이를 해소하기 위해 의존성을 주입하는 방법을 사용할 수 있다.
싱글톤 패턴은 가장 많이 사용하고 있다고 앞서 설명했는데 그중에서도 가장 대표적인 사례는 무엇일까?
내 생각에는 로깅을 구현할 때가 아닐까 싶다. 일반적으로 로깅 하나의 로그 파일에 메시지를 기록하는데 모든 컴포넌트에서 로거 인스턴스에 접근할 수 있게 해야 하기 때문이다. 로깅을 싱글톤으로 구현하면 로거 인스턴스가 중복 생성되는 것을 방지할 수 있고 전체적으로 일관된 방식으로 사용할 수 있게 된다.
public class Logger {
private static Logger instance;
private File logFile;
private Logger() {
// 파일 로거 설정
try {
this.logFile = new File("app.log");
// 파일이 없다면 새 파일 생성
if (!logFile.exists()) {
logFile.createNewFile();
}
} catch (IOException e) {
e.printStackTrace();
}
}
// 멀티 스레드 환경에서 안전하게 사용하기 위해서 synchronized로 선언
public static synchronized Logger getInstance() {
if (instance == null) {
instance = new Logger();
}
return instance;
}
public void log(String message) {
try {
// FileWriter를 사용하여 파일에 로그 메시지를 기록
FileWriter fileWriter = new FileWriter(this.logFile, true);
BufferedWriter bufferedWriter = new BufferedWriter(fileWriter);
bufferedWriter.write(message);
bufferedWriter.newLine();
bufferedWriter.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
이와 같이 로깅에 대한 로직을 만들었다면 이것을 사용할 때는 아래와 같이 코드를 작성할 수 있다.
public class Application {
public static void main(String[] args) {
Logger logger = Logger.getInstance();
logger.log("시작");
logger.log("실행중");
logger.log("종료");
}
}
이 외에도 알게 모르게 싱글턴 패턴을 적용한 부분이 많을 것이다. 특히 서비스 구성 부분의 소스 코드들을 보면 많이 보일 것이다. 알고 보면 참 많이 쓰이는 패턴이다. 아니 그냥 너무 많이 쓰이는 패턴이다.
그러면 Java나 C# 같은 언어가 아니고 싱글프로세스로 동작하는 Node.js 로도 가능할까?
당연히 가능하다.
Javascript에서 모듈은 자체적으로 싱글톤처럼 동작한다. 한번 로드되면 Node.js 는 그 모듈의 인스턴스를 캐시 하므로 동일한 모듈을 요청할 때마다 동일한 인스턴스를 반환한다.
// logger.js
const fs = require('fs');
const path = require('path');
class Logger {
constructor() {
this.logFile = path.join(__dirname, 'app.log');
}
log(message) {
fs.appendFile(this.logFile, `${new Date().toISOString()} - ${message}\n`, (err) => {
if (err) {
console.error('Error writing to log file', err);
}
});
}
}
module.exports = new Logger(); // 모듈을 요청할 때마다 같은 Logger 인스턴스 반환
Logger 클래스를 정의하고 인스턴스를 생성해서 모듈로 바로 내보낸다. require()를 통해서 logger.js 모듈을 어디서든 가져와서 Logger 인스턴스를 받을 수 있다.
// app.js
const logger = require('./logger');
logger.log("시작");
logger.log("실행중");
logger.log("종료");
그런데 Node.js에서 싱글턴 패턴을 사용해서 로깅을 구현할 때 주의할 부분이 많다.
가장 큰 것은 비동기로 파일을 쓸 때 동시성 관리가 필요하다는 것이다. 이를 해결하기 위해서 메모리 내에 Queue를 사용하여 순차적으로 기록하게 하거나 스트림을 이용하여 파일을 작성하는 방법을 사용할 수 있다.
물론 Java나 C# 그리고 Node.js에서 일어날 수 있는 문제점들은 더 있긴 하다. 예를 들어 파일의 크기가 너무 커지거나 하는 것 들이다. 하지만 이번 글은 싱글턴에 대해서만 작성했기 때문에 여기서 글을 정리한다.
'Design Pattern > 생성 패턴(Creational Patterns)' 카테고리의 다른 글
프로토타입(Prototype) - 생성 패턴(Creational Patterns) (0) | 2024.05.15 |
---|---|
빌더(Builder) - 생성 패턴(Creational Patterns) (0) | 2024.05.14 |
추상 팩토리(Abstract Factory) - 생성 패턴(Creational Patterns) (0) | 2024.05.14 |
팩토리 메서드(Factory Method) - 생성 패턴(Creational Patterns) (0) | 2024.05.13 |