소프트웨어 디자인 패턴 중 하나인 싱글톤 패턴(Singleton Pattern)은 애플리케이션 내에서 클래스의 인스턴스를 하나만 생성하도록 보장하는 패턴입니다. 이 패턴은 주로 데이터베이스 연결, 설정 파일 관리, 로그 처리 등과 같이 리소스를 효율적으로 관리해야 하는 상황에서 사용됩니다. 하지만 싱글톤 패턴은 테스트의 독립성을 해칠 수 있는 단점도 가지고 있습니다. 이번 글에서는 싱글톤 패턴의 개념, 장단점, 그리고 의존성 주입(Dependency Injection)을 통해 어떻게 이러한 단점을 극복할 수 있는지 살펴보겠습니다.
싱글톤 패턴이란?
싱글톤 패턴은 특정 클래스의 인스턴스가 프로그램 내에서 오직 하나만 존재하도록 보장하는 디자인 패턴입니다. 이는 전역적으로 접근 가능한 인스턴스를 제공하여, 여러 객체가 동일한 자원을 공유하도록 합니다.
특징
- 전역 접근성: 어디서든 동일한 인스턴스에 접근할 수 있습니다.
- 인스턴스 제어: 클래스 자체에서 인스턴스 생성과 관리를 담당합니다.
사용 예시
- 데이터베이스 연결 관리: 다수의 연결을 생성하는 대신 하나의 연결을 공유하여 리소스를 절약합니다.
- 설정 파일 관리: 애플리케이션 전역에서 동일한 설정을 사용하도록 보장합니다.
- 로그 처리: 하나의 로그 인스턴스를 통해 일관된 로그 기록을 유지합니다.
싱글톤 패턴의 구현 (TypeScript 예시)
class Singleton {
private static instance: Singleton;
// 생성자를 private으로 설정하여 외부에서 인스턴스 생성을 막습니다.
private constructor() {}
// 인스턴스를 가져오는 정적 메서드
public static getInstance(): Singleton {
if (!Singleton.instance) {
Singleton.instance = new Singleton();
}
return Singleton.instance;
}
public someMethod() {
console.log('싱글톤 인스턴스 메서드 호출');
}
}
// 사용 예시
const singleton1 = Singleton.getInstance();
const singleton2 = Singleton.getInstance();
console.log(singleton1 === singleton2); // true
위 코드에서 Singleton
클래스는 자체적으로 인스턴스를 관리하며, getInstance
메서드를 통해서만 접근 가능합니다.
싱글톤 패턴의 장점
- 리소스 효율성: 인스턴스 생성 비용이 큰 경우, 한 번만 생성하여 재사용하므로 성능 향상에 도움이 됩니다.
- 일관성 유지: 애플리케이션 전역에서 동일한 인스턴스를 사용하므로 상태 일관성을 유지할 수 있습니다.
- 명확한 접근 방법: 전역 접근 포인트를 제공하므로 어디서든 쉽게 사용할 수 있습니다.
싱글톤 패턴의 단점
- 테스트 어려움: 전역 상태를 가지므로 테스트 간 독립성이 떨어집니다.
- 강한 결합: 코드가 싱글톤 인스턴스에 의존하므로 유연성이 감소합니다.
- 멀티스레드 이슈: 동시성 문제가 발생할 수 있어 추가적인 처리 필요합니다.
테스트에서의 문제점
싱글톤 패턴은 전역 상태를 가지므로, 하나의 테스트에서 변경된 상태가 다른 테스트에 영향을 줄 수 있습니다. 이는 테스트 결과의 신뢰성을 떨어뜨리고, 디버깅을 어렵게 만듭니다.
예시: 문제 발생 코드
// 싱글톤 클래스
class Config {
private static instance: Config;
public settings: { [key: string]: any } = {};
private constructor() {}
public static getInstance(): Config {
if (!Config.instance) {
Config.instance = new Config();
}
return Config.instance;
}
}
// 테스트 코드
describe('Config Tests', () => {
it('첫 번째 테스트', () => {
const config = Config.getInstance();
config.settings['mode'] = 'test';
expect(config.settings['mode']).toBe('test');
});
it('두 번째 테스트', () => {
const config = Config.getInstance();
expect(config.settings['mode']).toBeUndefined(); // 실패합니다.
});
});
위 테스트에서 첫 번째 테스트에서 설정한 값이 두 번째 테스트에 영향을 미칩니다.
의존성 주입을 통한 문제 해결
의존성 주입(Dependency Injection)은 객체 간의 결합도를 낮추고, 테스트 용이성을 높여주는 설계 패턴입니다. 이를 통해 싱글톤 패턴의 단점을 완화할 수 있습니다.
의존성 주입의 장점
- 테스트 용이성 향상: 모의 객체(Mock Object)를 주입하여 테스트할 수 있습니다.
- 유연성 증가: 구현체를 쉽게 교체할 수 있어 확장성이 높아집니다.
- 결합도 감소: 객체 간 의존성을 명시적으로 주입하므로 결합도가 낮아집니다.
수정된 코드 예시
// 인터페이스 정의
interface IConfiguration {
settings: { [key: string]: any };
}
// 싱글톤 클래스
class Config implements IConfiguration {
private static instance: Config;
public settings: { [key: string]: any } = {};
private constructor() {}
public static getInstance(): Config {
if (!Config.instance) {
Config.instance = new Config();
}
return Config.instance;
}
}
// 서비스 클래스
class Service {
private config: IConfiguration;
constructor(config: IConfiguration) {
this.config = config;
}
public getMode() {
return this.config.settings['mode'];
}
}
// 테스트 코드
describe('Service Tests', () => {
it('모의 객체를 사용한 테스트', () => {
const mockConfig: IConfiguration = { settings: { mode: 'mock' } };
const service = new Service(mockConfig);
expect(service.getMode()).toBe('mock');
});
});
위 코드에서 Service
클래스는 IConfiguration
인터페이스를 구현한 객체를 주입받습니다. 테스트 시에는 실제 싱글톤 인스턴스 대신 모의 객체를 주입하여 테스트의 독립성을 보장합니다.
오픈소스 예시: InversifyJS를 활용한 싱글톤 패턴
InversifyJS는 TypeScript로 작성된 의존성 주입 라이브러리로, 싱글톤 패턴과 의존성 주입을 결합하여 사용할 수 있습니다.
설치
npm install inversify reflect-metadata
코드 구현
import 'reflect-metadata';
import { injectable, inject, Container } from 'inversify';
// 인터페이스 정의
interface ILogger {
log(message: string): void;
}
// 싱글톤으로 사용할 클래스
@injectable()
class ConsoleLogger implements ILogger {
log(message: string): void {
console.log(`로그: ${message}`);
}
}
// 서비스를 사용하는 클래스
@injectable()
class App {
private logger: ILogger;
constructor(@inject('ILogger') logger: ILogger) {
this.logger = logger;
}
run() {
this.logger.log('애플리케이션 시작');
}
}
// 컨테이너 설정
const container = new Container();
container.bind<ILogger>('ILogger').to(ConsoleLogger).inSingletonScope();
container.bind<App>(App).toSelf();
// 애플리케이션 실행
const app = container.get<App>(App);
app.run();
위 코드에서 ConsoleLogger
는 싱글톤 스코프로 등록되며, App
클래스는 의존성 주입을 통해 ILogger
를 주입받습니다.
테스트 코드
import { Container } from 'inversify';
// 테스트용 모의 로거
class MockLogger implements ILogger {
log(message: string): void {
// 테스트용 로직
}
}
describe('App Tests', () => {
it('MockLogger를 사용한 테스트', () => {
const testContainer = new Container();
testContainer.bind<ILogger>('ILogger').to(MockLogger);
testContainer.bind<App>(App).toSelf();
const app = testContainer.get<App>(App);
app.run();
// MockLogger를 사용하므로 실제 로그가 출력되지 않습니다.
});
});
테스트 시에는 실제 ConsoleLogger
대신 MockLogger
를 주입하여 테스트의 독립성을 유지합니다.
결론
싱글톤 패턴은 리소스 관리 측면에서 유용하지만, 테스트의 독립성을 해칠 수 있는 단점을 가지고 있습니다. 이를 극복하기 위해 의존성 주입을 활용하면 객체 간 결합도를 낮추고, 테스트 용이성을 높일 수 있습니다. 오픈소스 라이브러리인 InversifyJS와 같은 도구를 사용하면 이러한 패턴을 더 쉽게 적용할 수 있습니다.
'JavaScript & TypeScript' 카테고리의 다른 글
JavaScript의 내장 함수 - match() (1) | 2024.10.11 |
---|---|
JavaScript의 내장 함수 - test() (0) | 2024.10.09 |
JavaScript의 내장 함수 - some() (0) | 2024.10.08 |
JavaScript의 내장 함수 - reduce() (0) | 2024.10.07 |
JavaScript의 내장 함수 - filter() (0) | 2024.10.07 |