Singleton 패턴
Singleton 패턴이란?
싱글턴 패턴은 GoF 에 소개된 생성 패턴중 하나로 한 애플리케이션에 특정 클래스의 인스턴스를 1개만 생성하고 사용하는 디자인 패턴입니다. 싱글톤 패턴은 인스턴스가 1개만 생성되는 것을 보장할 필요가 있습니다.
왜 사용하나요?
싱글톤 패턴의 장점은 메모리를 절약할 수 있다는 점과, 하나의 인스턴스만 존재한다는 것을 보장하기 때문에 자원을 공유할 때 유리하다는 점이 있습니다. 이 때문에 싱글톤을 상태를 공유하거나, 자원을 공유해야할 때 사용합니다. 그래서 DB 커넥션 인스턴스, logger, 전역 상태 관리 스토어 등을 구현할 때 사용하기 좋습니다.
활용
예시 1. Logger 객체
class Logger { private static instance: Logger | null = null; private constructor() {} static getInstance(): Logger { if (!Logger.instance) { Logger.instance = new Logger(); } return Logger.instance; } log(message: string) { console.log(message); } } const logger = Object.freeze(new Logger()); export default logger;
logger 같은 경우에는 로그를 쌓아두는 객체 자체가 하나인 것이 유리합니다. 하나의 객체만 보고 로그들을 정리하는게 좋으니까요. 그리고
Object.freeze()
를 통해 객체 구조의 변경을 막습니다.예시 2. 더블 체크 락킹
public class Singleton { private static volatile Singleton instance; public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } }
volatile
키워드는 일반적으로 컴파일러의 재량을 제한하는 키워드로 자바에서는 GC 되지 않도록 하는 키워드입니다. 해당 객체는 사용자가 의도하여 인스턴스를 메모리에서 내리기 전까진 존재하는 것이 보장되어야 하므로, 가비지 컬렉터가 함부로 인스턴스를 메모리에서 내려가지 않게 하기 위함입니다. 더블 체크 락킹이라는 패턴은 인스턴스가
null
이 아닌 경우를 거르고 (이를 통해 성능 향상), synchronized
키워드를 통해 Singleton.class
에 락을 걸어 동기화 문제를 해결 합니다.부가적인 내용
프론트엔드 전역 상태 관리
전역 상태 관리 라이브러리에서 제공하는 전역 상태 관리 스토어도 하나만 사용하는 것을 강력히 권장합니다. 하지만, 라이브러리 내에서 스토어 인스턴스를 싱글톤 형태로 제공하지는 않습니다. 이 이유는 두가지가 있습니다. 첫번째는 앞서 말한 실제로 전역 상태 관리 스토어를 정말 한 앱에서 글로벌한 스토어로 사용할 때도 많지만, 리액트에서의 컨텍스트 API 를 대체하기 위한 수단으로도 많이 사용하기 때문이죠.
class ThemeStore { private _darkMode = false; get darkMode() { return this._darkMode; } toggle() { this._darkMode = !this._darkMode; } } const themeStore = new ThemeStore(); export default themeStore;
단점
결합도가 높다
여러 모듈들이 싱글톤에 직접 의존하기에, 싱글톤 관련 변화가 생기면 너무 많은 모듈에 영향을 미칠 수 있습니다.
테스트가 어렵다
하나의 인스턴스를 매번 초기화해 테스트를 진행해야하므로 테스트가 어렵습니다.
동시성 문제
여러 기법을 통해 해결은 가능하지만, 결국 복잡성이 증가한다는 것은 문제가 될 수 밖에 없습니다.
Proxy 패턴
개요
프록시 패턴은 어떤 실제 사용하려는 객체에 접근하기 전에, 우선적으로 해당 객체에 대한 기능을 보완하는 디자인 패턴입니다. 자바스크립트에서는 특히
Proxy
객체와 Reflect
객체를 통해 언어적으로 객체에 대한 기본 작업을 가로채는 것을 지원하기 때문에 Proxy 패턴을 구현하기 용이합니다.왜 사용하나요?
객체에 대한 기능 책임은 분리하면서, 부가적인 기능들을 분리해 중간에 끼워넣는 것이 용이한 패턴이 프록시 패턴 입니다. 프록시 패턴으로 어떤 기능을 가진 객체를 감싸서 기능을 추가할 때에는 일반적으로 객체에 대한 기능을 추가 보완한다기 보다는 보안이나 성능 최적화, 로깅 등의 부가적인 보완 기능을 끼워넣을 때 사용합니다.
활용
예시. API 클라이언트 인증 삽입
class HRService { getEmployeeList() { return fetch('/api/employees'); } } class HRServiceProxy { private realService: HRService; private token: string; constructor(realService: HRService, token: string) { this.realService = realService; this.token = token; } async getEmployeeList() { console.log("[LOG] 직원 리스트 요청 시작"); const res = await fetch("/api/employees", { headers: { Authorization: `Bearer ${this.token}`, }, }); if (!res.ok) { console.error("[ERROR] 직원 목록 불러오기 실패"); } return res; } }
공교롭게도 이런 API 호출 객체 등은 앞서 본 싱글턴 패턴으로 많이 구현 됩니다. 싱글턴 패턴으로 구현될만한 기능들은 프록시 패턴으로 보완 될 가능성이 높아보이네요.
사용시 주의점
성능
Proxy 패턴 특성상 정말 자주 일어나는 간단한 동작들(객체에 접근, 새 프로퍼티 추가 등)에 관련 Proxy 함수들이 호출 될 수 있기 때문에 과한 로직이 Proxy 기능으로 있다면 성능에 부정적인 영향을 줄 수 있습니다.
부가적인 내용
Reflect
객체
자바스크립트에서
Reflect
객체는 글로벌한 빌트인 객체로 Proxy
객체와 같이 쓸때 자주 쓰는 유틸 메서드를 담아놓은 객체입니다. 사용은 Math
객체 처럼 Reflect.set
과 같이 바로 사용하기도 합니다.Provider 패턴
개요
특정 객체의 생성과 전달 책임을 캡슐화(Provider)하여, 특정 객체들을 제공하는 설계적인 디자인 패턴입니다. 보통 객체를 직접 생성하거나, 의존성을 명시적으로 관리하지 않고, Provider 가 대신 관리하게 합니다.
왜 사용하나요?
Provider 패턴은 객체 생성과 전달을 관리함으로써 의존성 주입, 지연 초기화, 느슨한 결합을 가능하게 합니다. 이를 통해 Provider 와 Consumer 사이에 결합도를 낮추어 더 유연한 설계 및 구현을 하는 것이 주 목적입니다.
활용
예시 1. React 의 Context API
export const ThemeContext = React.createContext() const themes = { light: { background: '#fff', color: '#000', }, dark: { background: '#171717', color: '#fff', }, } export default function App() { const [theme, setTheme] = useState('dark') function toggleTheme() { setTheme(theme === 'light' ? 'dark' : 'light') } const providerValue = { theme: themes[theme], toggleTheme, } return ( <div className={`App theme-${theme}`}> <ThemeContext.Provider value={providerValue}> <Toggle /> <List /> </ThemeContext.Provider> </div> ) }
React Context API 는 Provider 패턴을 구현하여 React 구 버전의 여러 문제를 해결하기 위해 구현한 것 입니다.
Provider
의 value
prop 안에 들어간 값 providerValue
를 Toggle
컴포넌트와 List
컴포넌트가 사용할 수 있습니다. 예시 2. Spring
@Component public class NotificationService { private final Provider<MessageSender> senderProvider; public NotificationService(Provider<MessageSender> senderProvider) { this.senderProvider = senderProvider; } public void send(String msg) { MessageSender sender = senderProvider.get(); // 여기서 의존성을 제공받음 sender.send(msg); } }
예시 3. NestJS
@Injectable() export class NotificationService { constructor( @Inject('MessageSender') private readonly sender: MessageSender, ) {} notify() { this.sender.send('Hello'); } }
단점
구조적 복잡성
Provider 에 객체 구조나 생성 등이 감추어져 있어 Provider 가 많아진다면 객체 생성 파악 등이 어렵습니다.
런타임 오류 가능성 증가
느슨한 결합의 문제로 Provider 에서 제공하는 의존성이 제대로 주입되지 않았을 때 Consumer 가 객체를 사용하려고 하면 오류가 생길 수 있습니다.