icon

메티의 블로그

디자인패턴: Singleton, Proxy, Provider
디자인패턴: Singleton, Proxy, Provider

디자인패턴: Singleton, Proxy, Provider

summary
이 글에서는 디자인 패턴 중 싱글턴, 프록시, 그리고 프로바이더 패턴에 대해 설명하고 있습니다. 각 패턴의 특징과 장점, 사용 사례를 다루며, 특히 자원 관리와 의존성 주입에 대한 중요성을 강조합니다.
Tags
Design Pattern
view_count
날짜
Jul 23, 2025
상태
공개

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 패턴
공교롭게도 이런 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 Context API 는 Provider 패턴을 구현하여 React 구 버전의 여러 문제를 해결하기 위해 구현한 것 입니다. Providervalue prop 안에 들어간 값 providerValueToggle 컴포넌트와 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'); } }
NestJS 는 DI 컨테이너를 기반으로 하며, Provider 라는 용어 자체를 공식적으로 사용

단점

구조적 복잡성

Provider 에 객체 구조나 생성 등이 감추어져 있어 Provider 가 많아진다면 객체 생성 파악 등이 어렵습니다.

런타임 오류 가능성 증가

느슨한 결합의 문제로 Provider 에서 제공하는 의존성이 제대로 주입되지 않았을 때 Consumer 가 객체를 사용하려고 하면 오류가 생길 수 있습니다.