
프로그래밍 패러다임은 프로그래밍의 관점, 즉 프로그램을 어떻게 바라보고 설계할 것인지에 대한 접근 방식이다. 문제를 해결하기 위한 방법론이자 사고방식으로, 프로그래밍 언어를 사용하는 철학이라고 할 수 있다.
JavaScript 창시자 Brendan Eich는 함수를 일급 객체로 다루는 함수형 언어를 만들고 싶었다. 하지만 당시 가장 인기 있던 Java 개발자들을 끌어들이기 위해 Java의 문법과 패러다임을 유지해야 했다.
그 결과 JavaScript는 클래스 없는 함수형 언어를 기반으로 하면서도 객체지향의 특징을 함께 가진 독특한 멀티 패러다임 언어로 탄생했다. 이는 의도된 설계였고, JavaScript만의 강점이 되었다.
JavaScript는 객체지향과 함수형 두 가지 관점을 모두 가진 멀티 패러다임 언어다. 이러한 균형을 잡는 것이 현대 JavaScript 개발의 핵심이다.
객체지향의 장점을 활용하는 방법
함수형의 장점을 결합하는 방법
자바스크립트는 초기부터 객체지향적으로 발전해왔고, 현재는 함수형 프로그래밍의 요소가 많이 도입되었다. 여전히 우리가 다루는 데이터의 대부분이 객체 형태이기 때문에, 객체를 효과적으로 다루기 위한 다양한 디자인 패턴을 이해하고 활용할 필요가 있다.
디자인 패턴은 소프트웨어 설계에서 자주 발생하는 문제에 대한 검증된 재사용 가능한 해결책이다. 모듈의 세분화된 역할이나 모듈 간의 인터페이스 구현 방식을 설계할 때 참조할 수 있는 전형적인 해결 방식을 제시한다.
디자인 패턴을 통해 다음을 이해할 수 있다
디자인 패턴은 고정된 것이 아니라 유연하다. 한 패턴에 변형을 가하거나 요구사항을 반영하면 다른 패턴으로 자연스럽게 변형될 수 있다.
GoF(Gang of Four) 디자인 패턴은 네 명의 저자가 1994년 출간한 'Design Patterns: Elements of Reusable Object-Oriented Software'에서 처음 체계화되었다.
이 책은 소프트웨어 개발 과정에서 반복적으로 나타나는 설계 문제에 대한 23가지 검증된 해결책을 제시한다. 각 패턴은 특정 상황에서 발생하는 문제, 해결 방법, 그리고 그 결과로 얻는 이점과 트레이드오프를 명확히 정의한다.
GoF 패턴은 단순히 코드 템플릿이 아니라, 개발자들이 공통의 어휘로 소통할 수 있게 해주는 설계 언어다. "이 부분은 팩토리 패턴으로 구현했습니다"라고 말하면, 다른 개발자들이 즉시 그 구조와 의도를 이해할 수 있다.
1. 목적에 따른 분류
패턴이 어떤 일을 하기 위한 것인지에 따라 분류한다.
2. 범위에 따른 분류
패턴을 주로 클래스에 적용하는지, 객체에 적용하는지로 구분한다.

생성 패턴은 객체의 생성과 관련된 패턴이다. 객체 인스턴스 생성 과정을 추상화하여, 객체의 생성과 참조 과정을 캡슐화한다. 이를 통해 객체가 생성되거나 변경되어도 프로그램 구조에 영향을 최소화하여 유연성을 제공한다.
| 패턴 | 의도 |
| 추상 팩토리(Abstract Factory) | 구체적인 클래스를 지정하지 않고 인터페이스를 통해 서로 연관되는 객체들을 그룹으로 생성 |
| 빌더(Builder) | 복합 객체의 생성과 표현을 분리하여 동일한 생성 절차에서도 다른 표현 결과를 만들어낼 수 있음 |
| 팩토리 메서드(Factory Method) | 객체 생성을 서브클래스로 위임하여 캡슐화 |
| 프로토타입(Prototype) | 원본 객체를 복사하여 새로운 객체를 생성 |
| 싱글턴(Singleton) | 클래스의 인스턴스가 하나만 존재하도록 보장하고 전역 접근 지점을 제공 |
구조 패턴은 클래스나 객체들을 조합해 더 큰 구조로 만들 수 있게 해주는 패턴이다. 작은 단위의 객체들을 조합하여 더 큰 기능을 수행하는 구조를 만드는 방법을 제시한다.
| 패턴 | 의도 |
| 어댑터(Adapter) | 클래스의 인터페이스를 다른 인터페이스로 변환하여 호환성을 제공 |
| 브리지(Bridge) | 구현부에서 추상층을 분리하여 각자 독립적으로 확장 가능하게 함 |
| 컴포지트(Composite) | 객체들의 관계를 트리 구조로 구성하여 복합 객체와 단일 객체를 동일하게 취급 |
| 데코레이터(Decorator) | 주어진 상황 및 용도에 따라 객체에 동적으로 새로운 책임을 추가 |
| 퍼사드(Facade) | 서브시스템의 인터페이스 집합에 대해 하나의 통합된 인터페이스를 제공 |
| 플라이웨이트(Flyweight) | 크기가 작은 여러 객체를 공유하여 메모리를 절약 |
| 프록시(Proxy) | 접근이 어려운 객체에 대한 대리자를 제공하여 접근을 제어 |
행위 패턴은 클래스나 객체들이 서로 상호작용하는 방법과 책임을 분배하는 방법을 정의하는 패턴이다. 객체나 클래스 간의 교류 방법과 알고리즘을 어떤 객체에 할당하는 것이 적절한지를 다룬다.
행위 패턴은 하나의 객체로 수행할 수 없는 작업을 여러 객체로 분배하면서, 객체 간의 결합도를 최소화하는 데 중점을 둔다.
| 패턴 | 의도 |
| 책임 연쇄(Chain of Responsibility) | 요청을 받는 객체를 연쇄적으로 묶어 처리 객체를 만날 때까지 요청을 전달 |
| 커맨드(Command) | 요청을 객체의 형태로 캡슐화하여 재사용하거나 취소할 수 있도록 저장 |
| 인터프리터(Interpreter) | 특정 언어의 문법 표현을 정의하고 해석 |
| 반복자(Iterator) | 내부 구조를 노출하지 않고 컬렉션의 원소를 순차적으로 접근할 수 있는 인터페이스 제공 |
| 중재자(Mediator) | 객체 집합의 상호작용을 캡슐화하여 객체 간 결합도를 낮춤 |
| 메멘토(Memento) | 객체의 내부 상태를 저장하여 이전 상태로 복원할 수 있게 함 |
| 옵저버(Observer) | 객체 상태가 변할 때 의존 객체들이 자동으로 통지받고 갱신되도록 함 |
싱글턴은 클래스에 인스턴스가 하나만 존재하도록 보장하면서 이 인스턴스에 대한 전역 접근 지점을 제공하는 생성 디자인 패턴이다.
class Singleton {// private static 필드: 클래스 내부에서만 접근 가능한 정적 프로퍼티static #instance = null; // Singleton 인스턴스를 단 하나만 보관하기 위한 저장소constructor() {// 인스턴스가 이미 존재한다면, 새로 생성하지 못하도록 예외를 발생시킴if (Singleton.#instance) {throw
적용이 권장되는 경우
적용을 피해야 하는 경우
얻을 수 있는 이점
감수해야 하는 트레이드오프
싱글턴 패턴은 강력하지만, 남용하면 코드 품질을 해칠 수 있다.
const Singleton = (function() {let instance;return {getInstance: () => {if (!instance) {instance =
자바스크립트에서는 클로저와 즉시 실행 함수(IIFE)를 활용해 싱글턴을 구현할 수 있다. 즉시 실행 함수 내부에 비공개 인스턴스 변수를 두고, 인스턴스가 없을 때만 생성하는 방식으로 구조적으로 두 개 이상의 인스턴스 생성을 막는다.
이 방식은 new 키워드 사용을 차단하고, 클로저를 통해 인스턴스와 내부 상태가 외부에서 직접 접근되지 않도록 보호한다.
싱글턴 인스턴스 내에 버전 정보를 포함하면 시스템이 업데이트될 때 변경 사항을 추적하기 쉬워진다. 하지만 싱글턴 객체를 잘못 수정하면 시스템 전체에 영향을 미칠 수 있으므로, 변경 시에는 반드시 버전 관리와 영향 범위 파악이 필요하다.
const ApiClient = (function() {let instance;function init(version) {const apiVersion = version;let callCount = 0;
버전 관리는 API 클라이언트, 기능 플래그, 설정 관리 등에서 유용하게 활용된다.
데이터베이스 연결은 시스템 리소스를 많이 소모하므로, 싱글턴 패턴으로 관리하면 불필요한 연결 생성을 방지하고 성능을 최적화할 수 있다. Node.js 모듈 시스템의 캐싱 특성을 활용하면 자연스럽게 싱글턴을 구현할 수 있다.
// db.js - 데이터베이스 연결 싱글톤 관리 모듈let connection = null; // 모듈 스코프의 단일 연결 인스턴스 저장소let connectionCount = 0; // 연결 생성 순서 추적용 카운터export default {/*** DB 연결을 반환하는 싱글톤 메서드* - 최초 호출: 새로운 연결 생성* - 이후 호출: 기존 연결 재사용 (새 연결 생성 X)*/connect
실제 프로덕션 환경에서는 MySQL의 mysql2/promise, PostgreSQL의 pg, MongoDB의 mongoose 같은 라이브러리가 내부적으로 연결 풀(Connection Pool)을 싱글턴으로 관리한다.
Firebase 같은 서드파티 SDK를 사용할 때도 싱글턴 패턴이 자주 활용된다. 초기화 비용이 큰 SDK를 매번 새로 생성하면 성능 저하와 메모리 낭비가 발생하기 때문이다.
관련 내용을 검색하던 중, 개발자가 직접 싱글턴 로직을 구현하는 코드를 볼 수 있었다.
import { initializeApp, getApps, FirebaseApp } from "firebase/app";import { getAuth } from "firebase/auth";import { getFirestore } from "firebase/firestore";import { getStorage
하지만 이런 수동 체크가 정말 필요할까? SDK를 제공하는 Firebase에서 이미 싱글턴을 구현해놓지 않았을까? 실제로 Firebase의 initializeApp 소스코드를 확인해보면 (2026.01.09 기준), 내부적으로 이미 싱글턴 패턴이 구현되어 있다.
// firebase/app 내부 코드const _apps = new Map(); // 앱 인스턴스를 저장하는 Mapexport function initializeApp(options, config) {const name = config?.name || '[DEFAULT]';// 1. 이미 같은 이름의 앱이 있는지 확인
동작 순서
Firebase가 내부적으로 싱글턴을 보장하므로, 굳이 getApps()로 체크할 필요가 없다. 아래처럼 단순하게 작성해도 안전하다.
import { initializeApp } from "firebase/app";import { getAuth } from "firebase/auth";import { getStorage } from "firebase/storage";import { getFirestore } from "firebase/firestore";
복잡한 로직을 SDK 내부에 캡슐화하여, 사용자는 간단한 API만 사용하면 되도록 만들었다.
싱글턴 패턴은 독특하게도 생성 패턴과 행동 패턴의 특성을 동시에 가진다. 이러한 이중성은 싱글턴의 강력함이자 주의해야 할 지점이기도 하다.
싱글턴은 객체 생성 과정을 제어하는 생성 패턴이다.
이러한 특성으로 인해 생성 비용이 큰 객체(데이터베이스 연결, 파일 핸들러)의 효율적 관리가 가능하다.
동시에 싱글턴은 시스템의 행동과 협력 방식을 정의하는 행동 패턴이기도 하다.
이 특성 덕분에 로깅, 이벤트 버스, 상태 관리 같은 시스템 전반의 행동을 조율할 수 있다.
싱글턴의 이중적 특성은 강력한 기능을 제공하지만, 동시에 신중한 사용을 요구한다.
테스트 가능성 확보하기
문제: 전역 상태는 테스트 간 격리를 어렵게 만든다.
// Bad: 테스트 격리 불가능class Logger {static #instance = null;#logs = [];static getInstance() {if (!Logger.#instance) {
해결책: 테스트를 위한 리셋 메커니즘이나 의존성 주입 활용
// Good: 테스트 격리 가능class Logger {static #instance = null;#logs = [];static getInstance() {if (!Logger.#instance) {
JavaScript는 싱글 스레드이지만, Web Workers나 Node.js의 Worker Threads를 사용할 때는 주의가 필요하다. 각 워커는 독립적인 메모리 공간을 가지므로, 싱글턴이 워커마다 따로 생성될 수 있다.
2. 멀티스레드 환경에서의 동기화
해결책: 워커 간 상태 공유가 필요하면 SharedArrayBuffer나 메시지 패싱 사용
3. 확장성과 유연성 유지하기
문제: 싱글턴은 상속이나 인터페이스 교체가 어렵다.
// Bad: 직접 의존으로 교체 불가능class UserService {getUsers() {const logger = Logger.getInstance(); // 직접 의존logger.log('Fetching users');// ...}
해결책: 의존성 주입으로 유연성 확보
// Good: 주입으로 교체 가능class UserService {constructor(logger = Logger.getInstance()) {this.logger = logger; // 외부에서 주입}getUsers()
4. 변경의 파급 효과 관리하기
싱글턴 변경은 시스템 전체에 영향을 미치므로 버전 관리와 추적성이 중요하다:
class ConfigManager {static #instance = null;#version = '1.0.0';#config = {};static getInstance() {if (!ConfigManager.#instance)
싱글턴은 정말로 전역적으로 단일 인스턴스가 필요한 경우에만 사용하고, 그 외의 경우에는 더 유연한 대안을 선택하는 것이 바람직하다.
| 객체의 상태에 따라 동일한 동작을 다르게 처리 |
| 전략(Strategy) | 동일 계열의 알고리즘군을 정의하고 캡슐화하여 상호 교환 가능하게 함 |
| 템플릿 메서드(Template Method) | 상위 클래스는 알고리즘의 골격만 작성하고 구체적인 처리는 서브클래스로 위임 |
| 방문자(Visitor) | 객체 구조에서 알고리즘을 분리하여 새로운 연산을 추가하기 쉽게 함 |