본문으로 건너뛰기
Previous
Next
C++ 디자인 패턴 | '싱글톤/팩토리/옵저버' 실전 가이드

C++ 디자인 패턴 | '싱글톤/팩토리/옵저버' 실전 가이드

C++ 디자인 패턴 | '싱글톤/팩토리/옵저버' 실전 가이드

이 글의 핵심

GoF 패턴 구현에 더해 가상 디스패치 비용, CRTP·런타임 다형성, 타입 소거, 정책 기반 설계, 프로덕션 적용 관점까지 다룹니다.

같은 싱글톤·팩토리 아이디어는 JavaScriptPython 데코레이터에서도 자주 쓰입니다. 심화는 C++ 생성 패턴 가이드·종합 가이드를 보세요.

1. 싱글톤 패턴 (Singleton)

기본 구현

class Singleton {
private:
    static Singleton* instance;
    
    Singleton() {}  // private 생성자
    
public:
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
    
    static Singleton* getInstance() {
        if (instance == nullptr) {
            instance = new Singleton();
        }
        return instance;
    }
};

Singleton* Singleton::instance = nullptr;

스레드 안전 싱글톤

#include <mutex>

class ThreadSafeSingleton {
private:
    static ThreadSafeSingleton* instance;
    static mutex mtx;
    
    ThreadSafeSingleton() {}
    
public:
    static ThreadSafeSingleton* getInstance() {
        if (instance == nullptr) {
            lock_guard<mutex> lock(mtx);
            if (instance == nullptr) {
                instance = new ThreadSafeSingleton();
            }
        }
        return instance;
    }
};

Meyers 싱글톤 (권장)

class MeyersSingleton {
private:
    MeyersSingleton() {}
    
public:
    static MeyersSingleton& getInstance() {
        static MeyersSingleton instance;  // C++11부터 스레드 안전
        return instance;
    }
    
    MeyersSingleton(const MeyersSingleton&) = delete;
    MeyersSingleton& operator=(const MeyersSingleton&) = delete;
};

2. 팩토리 패턴 (Factory)

심플 팩토리

enum class ShapeType { Circle, Rectangle, Triangle };

class Shape {
public:
    virtual void draw() = 0;
    virtual ~Shape() {}
};

class Circle : public Shape {
public:
    void draw() override { cout << "원 그리기" << endl; }
};

class Rectangle : public Shape {
public:
    void draw() override { cout << "사각형 그리기" << endl; }
};

class ShapeFactory {
public:
    static unique_ptr<Shape> createShape(ShapeType type) {
        switch (type) {
            case ShapeType::Circle:
                return make_unique<Circle>();
            case ShapeType::Rectangle:
                return make_unique<Rectangle>();
            default:
                return nullptr;
        }
    }
};

int main() {
    auto shape = ShapeFactory::createShape(ShapeType::Circle);
    shape->draw();
}

추상 팩토리

class Button {
public:
    virtual void render() = 0;
    virtual ~Button() {}
};

class WindowsButton : public Button {
public:
    void render() override { cout << "Windows 버튼" << endl; }
};

class MacButton : public Button {
public:
    void render() override { cout << "Mac 버튼" << endl; }
};

class GUIFactory {
public:
    virtual unique_ptr<Button> createButton() = 0;
    virtual ~GUIFactory() {}
};

class WindowsFactory : public GUIFactory {
public:
    unique_ptr<Button> createButton() override {
        return make_unique<WindowsButton>();
    }
};

class MacFactory : public GUIFactory {
public:
    unique_ptr<Button> createButton() override {
        return make_unique<MacButton>();
    }
};

3. 옵저버 패턴 (Observer)

#include <vector>
#include <algorithm>

class Observer {
public:
    virtual void update(int value) = 0;
    virtual ~Observer() {}
};

class Subject {
private:
    vector<Observer*> observers;
    int state;
    
public:
    void attach(Observer* observer) {
        observers.push_back(observer);
    }
    
    void detach(Observer* observer) {
        observers.erase(
            remove(observers.begin(), observers.end(), observer),
            observers.end()
        );
    }
    
    void setState(int newState) {
        state = newState;
        notify();
    }
    
    void notify() {
        for (auto observer : observers) {
            observer->update(state);
        }
    }
};

class ConcreteObserver : public Observer {
private:
    string name;
    
public:
    ConcreteObserver(string n) : name(n) {}
    
    void update(int value) override {
        cout << name << "이(가) 업데이트 받음: " << value << endl;
    }
};

int main() {
    Subject subject;
    
    ConcreteObserver obs1("관찰자1");
    ConcreteObserver obs2("관찰자2");
    
    subject.attach(&obs1);
    subject.attach(&obs2);
    
    subject.setState(10);
    subject.setState(20);
}

4. 전략 패턴 (Strategy)

class SortStrategy {
public:
    virtual void sort(vector<int>& data) = 0;
    virtual ~SortStrategy() {}
};

class BubbleSort : public SortStrategy {
public:
    void sort(vector<int>& data) override {
        cout << "버블 정렬" << endl;
        // 구현...
    }
};

class QuickSort : public SortStrategy {
public:
    void sort(vector<int>& data) override {
        cout << "퀵 정렬" << endl;
        // 구현...
    }
};

class Sorter {
private:
    unique_ptr<SortStrategy> strategy;
    
public:
    void setStrategy(unique_ptr<SortStrategy> s) {
        strategy = move(s);
    }
    
    void sort(vector<int>& data) {
        if (strategy) {
            strategy->sort(data);
        }
    }
};

int main() {
    Sorter sorter;
    vector<int> data = {5, 2, 8, 1, 9};
    
    sorter.setStrategy(make_unique<BubbleSort>());
    sorter.sort(data);
    
    sorter.setStrategy(make_unique<QuickSort>());
    sorter.sort(data);
}

5. 데코레이터 패턴 (Decorator)

class Coffee {
public:
    virtual string getDescription() = 0;
    virtual double cost() = 0;
    virtual ~Coffee() {}
};

class SimpleCoffee : public Coffee {
public:
    string getDescription() override {
        return "커피";
    }
    
    double cost() override {
        return 2.0;
    }
};

class CoffeeDecorator : public Coffee {
protected:
    unique_ptr<Coffee> coffee;
    
public:
    CoffeeDecorator(unique_ptr<Coffee> c) : coffee(move(c)) {}
};

class MilkDecorator : public CoffeeDecorator {
public:
    MilkDecorator(unique_ptr<Coffee> c) : CoffeeDecorator(move(c)) {}
    
    string getDescription() override {
        return coffee->getDescription() + " + 우유";
    }
    
    double cost() override {
        return coffee->cost() + 0.5;
    }
};

class SugarDecorator : public CoffeeDecorator {
public:
    SugarDecorator(unique_ptr<Coffee> c) : CoffeeDecorator(move(c)) {}
    
    string getDescription() override {
        return coffee->getDescription() + " + 설탕";
    }
    
    double cost() override {
        return coffee->cost() + 0.2;
    }
};

int main() {
    auto coffee = make_unique<SimpleCoffee>();
    coffee = make_unique<MilkDecorator>(move(coffee));
    coffee = make_unique<SugarDecorator>(move(coffee));
    
    cout << coffee->getDescription() << endl;  // 커피 + 우유 + 설탕
    cout << coffee->cost() << "달러" << endl;  // 2.7달러
}

6. 가상 디스패치(virtual dispatch) 비용과 내부

런타임 다형성은 virtual 함수를 통해 구현되며, 호출 시점에 실제 구현체가 결정된다. 이 과정은 가상 테이블(vtable)가상 포인터(vptr)에 의존한다. 객체가 최소 하나의 가상 함수를 갖는 순간, 컴파일러는 일반적으로 객체 레이아웃 앞쪽(구현에 따라 다름)에 vptr을 두고, 이 포인터가 가리키는 vtable 배열에서 함수 포인터를 읽어 간접 호출(indirect call)을 수행한다.

비용이 발생하는 지점

  1. 간접 분기: 직접 호출(call 고정 주소)이 아니라 vtable을 거친 포인터 역참조 후 호출이므로, 분기 예측(branch prediction) 실패 시 페널티가 커질 수 있다.
  2. 인라인 제약: 가상 호출은 대부분의 경우 인라인화되지 않는다. 핫 루프에서 수백만 번 호출되는 가상 함수 한 줄이 병목이 될 수 있다.
  3. 추가 메모리: 각 다형 타입마다 vtable(정적·공유)과 객체당 vptr 오버헤드가 있다. 대규모 배열에서 수백만 개 객체가 다형이면 vptr만으로도 수 메가바이트가 될 수 있다.
  4. 캐시 지역성: vtable 접근은 객체 본문과 다른 캐시 라인을 건드릴 수 있어, 데이터 지역성이 나쁜 패턴과 결합하면 성능이 떨어진다.

최적화 관점에서의 실무 팁

  • 핫 패스에서는 가상 호출 줄이기: 인터페이스 경계를 모듈 밖으로만 두고, 내부 루프는 템플릿·CRTP·함수 객체로 정적 바인딩한다.
  • final/override: 계층이 확정되면 final을 붙여 디컴파일러·컴파일러가 더 공격적으로 최적화할 여지를 준다(구현체가 사실상 하나일 때 특히).
  • 소수의 구현체만 존재할 때: switch/if로 타입 분기하는 수동 디스패치가 가상 호출보다 빠를 때가 있다(분기 예측 가능·인라인 가능).

아래는 동일한 연산을 가상 호출과 비가상(또는 템플릿)으로 나누었을 때 개념적 차이를 보여준다(<cstdint>, <vector>).

#include <cstdint>
#include <vector>

// --- 런타임 다형성: vtable 경유 ---
struct Base {
    virtual std::int64_t compute(std::int64_t x) const = 0;
    virtual ~Base() = default;
};

struct ImplA final : Base {
    std::int64_t compute(std::int64_t x) const override { return x * 2; }
};

// 핫 루프에서 Base* 벡터를 순회하면 매번 간접 호출
std::int64_t sum_polymorphic(const std::vector<Base*>& items, std::int64_t seed) {
    std::int64_t acc = seed;
    for (auto* p : items) {
        acc += p->compute(acc);  // vtable 조회 + 간접 호출
    }
    return acc;
}

// --- 컴파일 타임 다형성(템플릿): 인라인·고정 주소 호출 가능 ---
template <typename T>
std::int64_t sum_static(const std::vector<T>& items, std::int64_t seed) {
    std::int64_t acc = seed;
    for (const auto& item : items) {
        acc += item.compute(acc);  // T::compute가 비가상이면 직접 호출
    }
    return acc;
}

실제 마이크로벤치마크는 컴파일러·CPU·데이터 레이아웃에 따라 크게 달라진다. 중요한 것은 “가상 호출이 나쁘다”가 아니라 “핫 루프에서 불필요한 동적 바인딩을 제거할 수 있는가”이다.

7. CRTP와 런타임 다형성

CRTP(Curiously Recurring Template Pattern)는 기본 클래스가 파생 클래스를 템플릿 인자로 받는 관용구이다. 컴파일 시점에 호출이 정적으로 바인딩되므로 virtual 없이 다형적 인터페이스를 흉내 낼 수 있다.

구분런타임 다형성 (virtual)CRTP
바인딩 시점실행 시(vtable)컴파일 시
바이너리 호환기본 클래스 포인터로 통일 가능템플릿 인스턴스마다 타입이 달라짐
인라인어렵다가능
확장동적 로딩·플러그인에 적합컴파일 의존성 증가
// CRTP: Derived가 Base<Derived>를 상속 — 정적 바인딩으로 interface() 호출
template <typename Derived>
struct BaseCRTP {
    void interface() {
        static_cast<Derived*>(this)->implementation();  // 컴파일 타임에 결정
    }
};

struct Derived1 : BaseCRTP<Derived1> {
    void implementation() { /* ... */ }
};

struct Derived2 : BaseCRTP<Derived2> {
    void implementation() { /* ... */ }
};

언제 CRTP인가: 성능이 중요하고 구현체 집합이 빌드 시점에 고정될 때. 언제 가상 함수인가: 인터페이스만 공유하고 구현체를 동적으로 로드하거나, 바이너리 ABI 안정성이 필요할 때.

8. 타입 소거(type erasure) 구현

std::function이 대표적인 타입 소거이다. 서로 다른 구체 타입을 동일한 핸들(std::function<void()>)에 넣기 위해, 내부적으로 소형 최적화(SBO)·힙 할당·가상 호출 또는 함수 포인터 테이블 등으로 “구체 타입 정보를 지우고” 공통 연산만 노출한다.

직접 최소 타입 소거를 만들면 패턴 이해에 도움이 된다. 아래는 호출 가능 객체만 추상화한 극단적으로 단순화한 예이다.

#include <memory>
#include <utility>

class AnyCallable {
    struct Concept {
        virtual void invoke() = 0;
        virtual ~Concept() = default;
    };

    template <typename F>
    struct Model final : Concept {
        F f;
        explicit Model(F fn) : f(std::move(fn)) {}
        void invoke() override { f(); }
    };

    std::unique_ptr<Concept> self_;

public:
    template <typename F>
    AnyCallable(F&& f)  // F가 복사/이동 가능하다고 가정
        : self_(std::make_unique<Model<std::decay_t<F>>>(std::forward<F>(f))) {}

    void operator()() { self_->invoke(); }  // 타입 F는 밖에서 보이지 않음 — 소거됨
};

실무에서는 std::function, 커스텀 any, llvm::function_ref(비소유 참조) 등을 상황에 맞게 쓴다. 타입 소거는 유연성을 주지만 간접 호출·할당 비용을 동반하므로, 핫 패스에는 std::function 남발을 피하는 것이 좋다.

9. 정책 기반 설계(policy-based design)

정책 기반 설계는 동작을 독립된 “정책” 클래스로 쪼개 템플릿 매개변수로 조합하는 방식이다. Andrei Alexandrescu의 Modern C++ Design에서 policy라는 이름으로 널리 알려졌다. 상속 깊이를 늘리는 대신 컴파일 타임 조합으로 기능을 확장한다.

#include <iostream>

// 정책: 로깅 여부(정적 인터페이스만 맞추면 조합 가능)
struct VerboseLog {
    static void on_event(const char* msg) {
        std::cerr << "[event] " << msg << '\n';
    }
};
struct SilentLog {
    static void on_event(const char*) {}
};

// 본체: LoggingPolicy를 주입 — 동일한 Service 템플릿, 다른 바이너리 특성
template <typename LoggingPolicy>
class Service {
public:
    void do_work() {
        LoggingPolicy::on_event("do_work");
        // 실제 비즈니스 로직…
    }
};

using MonitoredService = Service<VerboseLog>;
using QuietService = Service<SilentLog>;

정책 기반 설계의 장점은 보일러플레이트 상속 트리를 피하고, 필요한 조합만 인스턴스화한다는 점이다. 단점은 템플릿 에러 메시지가 길어지고, 바이너리 크기가 조합 수에 따라 늘 수 있다는 것이다. 표준 라이브러리의 std::allocator 주입, 커스텀 할당자·락 정책 등이 같은 계열 사고이다.

10. 프로덕션에서의 디자인 패턴

실서비스 코드에서는 “교과서 패턴” 그대로보다 제약 조건에 맞는 변형이 많다.

  1. 싱글톤: 로거·설정처럼 프로세스 단일 진실 공급원이 필요할 때는 Meyers 싱글톤이 흔하지만, 테스트 가능성을 위해 의존성 주입으로 대체하는 팀도 많다. 전역 상태는 동시성·초기화 순서 이슈를 만든다.
  2. 팩토리·추상 팩토리: 플러그인 로딩, 플랫폼별 구현 선택(OS API 래퍼), 프로토콜 버전별 핸들러 생성에 쓰인다. std::function 레지스트리나 map<string, Creator> 패턴이 실무에서 자주 보인다.
  3. 옵저버·이벤트: GUI 프레임워크 외에도 메시지 버스·리액티브 스트림으로 확장된다. raw 포인터 대신 weak_ptr·핸들 ID로 생명주기를 분리하는 것이 안전하다.
  4. 전략: 정렬·직렬화·압축 알고리즘 교체에 적합하다. 인터페이스가 안정적이면 virtual, 초고속 경로면 템플릿 전략을 병행한다.
  5. 데코레이터: HTTP 미들웨어, 스트림 필터 체인에서 동일한 패턴이 나온다. unique_ptr로 소유권을 명확히 하는 것이 C++에서는 특히 중요하다.

요약: 프로덕션에서는 패턴 이름보다 경계(인터페이스)와 소유권(RAII)·동시성 모델이 먼저다. 가상 호출과 타입 소거는 편의를 주는 대신 비용이 있으므로, 측정(profiling) 후 경계를 잡는 것이 설계 패턴의 최종 단계이다.

실전 예시

예시 1: 로깅 시스템 (싱글톤 + 전략)

enum class LogLevel { Debug, Info, Warning, Error };

class LogStrategy {
public:
    virtual void log(const string& message) = 0;
    virtual ~LogStrategy() {}
};

class ConsoleLogger : public LogStrategy {
public:
    void log(const string& message) override {
        cout << "[Console] " << message << endl;
    }
};

class FileLogger : public LogStrategy {
public:
    void log(const string& message) override {
        // 파일에 로그 작성
        cout << "[File] " << message << endl;
    }
};

class Logger {
private:
    unique_ptr<LogStrategy> strategy;
    LogLevel minLevel;
    
    Logger() : minLevel(LogLevel::Info) {
        strategy = make_unique<ConsoleLogger>();
    }
    
public:
    static Logger& getInstance() {
        static Logger instance;
        return instance;
    }
    
    void setStrategy(unique_ptr<LogStrategy> s) {
        strategy = move(s);
    }
    
    void log(LogLevel level, const string& message) {
        if (level >= minLevel && strategy) {
            strategy->log(message);
        }
    }
};

int main() {
    Logger::getInstance().log(LogLevel::Info, "시스템 시작");
    Logger::getInstance().setStrategy(make_unique<FileLogger>());
    Logger::getInstance().log(LogLevel::Error, "에러 발생");
}

예시 2: 플러그인 시스템 (팩토리)

class Plugin {
public:
    virtual void execute() = 0;
    virtual string getName() = 0;
    virtual ~Plugin() {}
};

class AudioPlugin : public Plugin {
public:
    void execute() override { cout << "오디오 처리" << endl; }
    string getName() override { return "AudioPlugin"; }
};

class VideoPlugin : public Plugin {
public:
    void execute() override { cout << "비디오 처리" << endl; }
    string getName() override { return "VideoPlugin"; }
};

class PluginFactory {
private:
    map<string, function<unique_ptr<Plugin>()>> creators;
    
public:
    void registerPlugin(const string& name, function<unique_ptr<Plugin>()> creator) {
        creators[name] = creator;
    }
    
    unique_ptr<Plugin> create(const string& name) {
        if (creators.find(name) != creators.end()) {
            return creators[name]();
        }
        return nullptr;
    }
};

int main() {
    PluginFactory factory;
    
    factory.registerPlugin("audio", [] { return make_unique<AudioPlugin>(); });
    factory.registerPlugin("video", [] { return make_unique<VideoPlugin>(); });
    
    auto plugin = factory.create("audio");
    plugin->execute();
}

자주 발생하는 문제

문제 1: 싱글톤 소멸 순서

증상: 프로그램 종료 시 크래시

원인: 싱글톤 간 의존성

해결법: Meyers 싱글톤 사용 또는 의존성 제거

문제 2: 팩토리에서 메모리 누수

증상: 메모리 누수

원인: raw 포인터 반환

해결법: unique_ptr 반환

문제 3: 옵저버 댕글링 포인터

증상: 크래시

원인: 옵저버 소멸 후에도 Subject가 참조

해결법: detach 호출 또는 weak_ptr 사용

FAQ

Q1: 디자인 패턴은 언제 사용하나요?

A: 반복되는 문제에 대한 검증된 해결책이 필요할 때 사용합니다.

Q2: 모든 패턴을 알아야 하나요?

A: 아니요, 자주 쓰이는 5-10개 패턴만 알아도 충분합니다.

Q3: 패턴을 무조건 사용해야 하나요?

A: 아니요, 과도한 패턴 사용은 복잡도만 높입니다. 필요할 때만 사용하세요.

Q4: C++에서 가장 유용한 패턴은?

A: RAII, 싱글톤, 팩토리, 옵저버, 전략 패턴이 가장 자주 사용됩니다.

Q5: 패턴 학습 순서는?

A:

  1. 싱글톤, 팩토리 (생성 패턴)
  2. 전략, 옵저버 (행동 패턴)
  3. 데코레이터, 어댑터 (구조 패턴)

Q6: 디자인 패턴 책 추천은?

A: “Design Patterns” (GoF), “Head First Design Patterns”

Q7: 가상 함수가 느리다고 하는데 항상 피해야 하나요?

A: 항상 피할 필요는 없습니다. 모듈 경계·플러그인·공개 ABI처럼 런타임 다형성이 필요한 곳에서는 가상 호출이 적절합니다. 문제는 측정되지 않은 채 핫 루프에 가상 호출을 남발하는 경우이므로, 프로파일링으로 병목을 확인한 뒤 CRTP·템플릿·수동 디스패치를 검토하면 됩니다.

Q8: CRTP와 virtual 중 어떤 것을 써야 하나요?

A: 구현체가 빌드 시점에 모두 알려져 있고 성능·인라인이 중요하면 CRTP 후보입니다. 동적 로딩·플러그인·런타임에만 타입이 정해지는 경우에는 virtual이 맞습니다. 둘 다 “다형성”이지만 바인딩 시점과 바이너리 경계 요구가 다릅니다.

Q9: std::function과 직접 만든 타입 소거는 언제 쓰나요?

A: std::function은 구현이 검증되어 있고 SBO 등 최적화가 들어 있습니다. 커스텀 타입 소거는 할당 없음·ABI 고정·특수 연산만 노출 같은 제약이 있을 때 고려합니다. 핫 패스에서는 std::function 대신 템플릿 제약(std::invocable)이나 함수 참조 타입을 우선 검토하는 편이 좋습니다.


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

관련 글

심화 부록: 구현·운영 관점

이 부록은 앞선 본문에서 다룬 주제(「C++ 디자인 패턴 | ‘싱글톤/팩토리/옵저버’ 실전 가이드」)를 구현·런타임·운영 관점에서 다시 압축합니다. 도메인별 세부 구현은 글마다 다르지만, 입력 검증 → 핵심 연산 → 부작용(I/O·네트워크·동시성) → 관측의 흐름으로 장애를 나누면 원인 추적이 빨라집니다.

내부 동작과 핵심 메커니즘

flowchart TD
  A[입력·요청·이벤트] --> B[파싱·검증·디코딩]
  B --> C[핵심 연산·상태 전이]
  C --> D[부작용: I/O·네트워크·동시성]
  D --> E[결과·관측·저장]
sequenceDiagram
  participant C as 클라이언트/호출자
  participant B as 경계(런타임·게이트웨이·프로세스)
  participant D as 의존성(API·DB·큐·파일)
  C->>B: 요청/이벤트
  B->>D: 조회·쓰기·RPC
  D-->>B: 지연·부분 실패·재시도 가능
  B-->>C: 응답 또는 오류(코드·상관 ID)
  • 불변 조건(Invariant): 버퍼 경계, 프로토콜 상태, 트랜잭션 격리, FD 상한 등 단계별로 문장으로 적어 두면 디버깅 비용이 줄어듭니다.
  • 결정성: 순수 층과 시간·네트워크·스케줄에 의존하는 층을 분리해야 테스트와 장애 분석이 쉬워집니다.
  • 경계 비용: 직렬화, 인코딩, syscall 횟수, 락 경합, 할당·GC, 캐시 미스를 의심 목록에 둡니다.
  • 백프레셔: 생산자가 소비자보다 빠를 때 버퍼·큐·스트림에서 속도를 줄이는 신호를 어디에 둘지 정의합니다.

프로덕션 운영 패턴

영역운영 관점 질문
관측성요청 단위 상관 ID, 에러율·지연 p95/p99, 의존성 타임아웃·재시도가 대시보드에 보이는가
안전성입력 검증·권한·비밀·감사 로그가 코드 경로마다 일관적인가
신뢰성재시도는 멱등 연산에만 적용되는가, 서킷 브레이커·백오프·DLQ가 있는가
성능캐시·배치 크기·커넥션 풀·인덱스·백프레셔가 데이터 규모에 맞는가
배포롤백 룬북, 카나리/블루그린, 마이그레이션·피처 플래그가 문서화되어 있는가
용량피크 트래픽·디스크·FD·스레드 풀 상한을 주기적으로 검증하는가

스테이징은 데이터 양·네트워크 RTT·동시성을 프로덕션에 가깝게 맞출수록 재현율이 올라갑니다.

확장 예시: 엔드투엔드 미니 시나리오

앞선 본문 주제(「C++ 디자인 패턴 | ‘싱글톤/팩토리/옵저버’ 실전 가이드」)를 배포·운영 흐름에 맞춰 옮긴 체크리스트입니다. 도메인에 맞게 단계 이름만 바꿔 적용할 수 있습니다.

  1. 입력 계약 고정: 스키마·버전·최대 페이로드·타임아웃·에러 코드를 경계에 둔다.
  2. 핵심 경로 계측: 요청 ID, 단계별 지연, 외부 호출 결과 코드를 로그·메트릭·트레이스에서 한 흐름으로 본다.
  3. 실패 주입: 의존성 타임아웃·5xx·부분 데이터·락 대기를 스테이징에서 재현한다.
  4. 호환·롤백: 설정/마이그레이션/클라이언트 버전을 되돌릴 수 있는지 확인한다.
  5. 부하 후 검증: 피크 대비 p95/p99, 에러율, 리소스 상한, 알림 임계값을 점검한다.
handle(request):
  ctx = newCorrelationId()
  validated = validateSchema(request)
  authorize(validated, ctx)
  result = domainCore(validated)
  persistOrEmit(result, idempotentKey)
  recordMetrics(ctx, latency, outcome)
  return result

문제 해결(Troubleshooting)

증상가능 원인조치
간헐적 실패레이스, 타임아웃, 외부 의존성, DNS최소 재현 스크립트, 분산 트레이스·로그 상관관계, 재시도·서킷 설정 점검
성능 저하N+1, 동기 I/O, 락 경합, 과도한 직렬화, 캐시 미스프로파일러·APM으로 핫스팟 확인 후 한 가지씩 제거
메모리 증가캐시 무제한, 구독/리스너 누수, 대용량 버퍼, 커넥션 미반납상한·TTL·힙/FD 스냅샷 비교
빌드·배포만 실패환경 변수, 권한, 플랫폼 차이, lockfileCI 로그와 로컬 diff, 런타임·이미지 버전 핀
설정 불일치프로필·시크릿·기본값, 리전스키마 검증된 설정 단일 소스와 배포 매트릭스 표준화
데이터 불일치비멱등 재시도, 부분 쓰기, 캐시 무효화 누락멱등 키·아웃박스·트랜잭션 경계 재검토

권장 순서: (1) 최소 재현 (2) 최근 변경 범위 축소 (3) 환경·의존성 차이 (4) 관측으로 가설 검증 (5) 수정 후 회귀·부하 테스트.

배포 전에는 git addgit commitgit pushnpm run deploy 순서를 권장합니다.


이 글에서 다루는 키워드 (관련 검색어)

C++, 디자인패턴, design-pattern, CRTP, 타입소거, 가상함수 등으로 검색하시면 이 글이 도움이 됩니다.