기본 콘텐츠로 건너뛰기

디자인 패턴 총정리: GoF 23 + 실무 확장 5가지


 


디자인 패턴은 특정 맥락의 문제를 체계적으로 해결하기 위한 ‘재사용 가능한 솔루션’입니다.

  • 목표: 코드의 재사용성유지보수성확장성을 높이는 것
  • 종류: 크게 생성(Creational), 구조(Structural), 행위(Behavioral)로 나누며, 23개 GoF 패턴 이외에도 실무 환경에서 자주 쓰이는 여러 패턴이 있습니다.

본 글에서는 다음과 같이 28개 패턴을 소개합니다.

  1. 생성(Creational) 패턴
    1. Abstract Factory
    2. Builder
    3. Factory Method
    4. Prototype
    5. Singleton
    6. Object Pool (추가)
  2. 구조(Structural) 패턴
    7. Adapter
    8. Bridge
    9. Composite
    10. Decorator
    11. Facade
    12. Flyweight
    13. Proxy
  3. 행위(Behavioral) 패턴
    14. Chain of Responsibility
    15. Command
    16. Interpreter
    17. Iterator
    18. Mediator
    19. Memento
    20. Null Object (추가)
    21. Observer
    22. State
    23. Strategy
    24. Template Method
    25. Visitor
  4. 추가적인 패턴
    26. Model-View-Controller (MVC)
    27. Repository
    28. Dependency Injection

1. 생성(Creational) 패턴

1-1. Abstract Factory

개요

  • 의도: 관련된 객체들의 ‘가족’을 생성하기 위해, 구체적인 클래스를 지정하지 않고도 객체를 생성하는 인터페이스 제공
  • 특징: 여러 종류의 제품을 한 번에 만들어야 할 때, 제품군별로 호환성을 유지하면서 객체 생성 로직을 추상화

C# 예시 (간단 버전)

// 추상 제품
public interface IButton { void Render(); }
public interface ITextBox { void Render(); }

// 구체 제품: Win 스타일
public class WinButton : IButton { public void Render() => Console.WriteLine("윈도우 버튼"); }
public class WinTextBox : ITextBox { public void Render() => Console.WriteLine("윈도우 텍스트박스"); }

// 구체 제품: Mac 스타일
public class MacButton : IButton { public void Render() => Console.WriteLine("맥 버튼"); }
public class MacTextBox : ITextBox { public void Render() => Console.WriteLine("맥 텍스트박스"); }

// 추상 팩토리
public interface IGUIFactory
{
    IButton CreateButton();
    ITextBox CreateTextBox();
}

// 구체 팩토리
public class WinFactory : IGUIFactory
{
    public IButton CreateButton() => new WinButton();
    public ITextBox CreateTextBox() => new WinTextBox();
}

public class MacFactory : IGUIFactory
{
    public IButton CreateButton() => new MacButton();
    public ITextBox CreateTextBox() => new MacTextBox();
}

// 사용 예시
class Program
{
    static void Main()
    {
        IGUIFactory factory = new WinFactory(); // 또는 new MacFactory();
        IButton button = factory.CreateButton();
        ITextBox textBox = factory.CreateTextBox();

        button.Render();
        textBox.Render();
    }
}

1-2. Builder

개요

  • 의도: 복합 객체(예: 복잡한 생성 과정이 필요한 객체)를 단계별로 조립할 때, 구현 방법을 분리하고 유연성을 제공
  • 특징: ‘조립 과정을 동일’하게 유지하면서, ‘각 단계별 구현체’를 바꿔 다른 결과물을 만들 수 있음

C# 예시 (간단 버전)

public class Computer
{
    public string CPU { get; set; }
    public string GPU { get; set; }
    public string RAM { get; set; }
}

public interface IComputerBuilder
{
    void SetCPU();
    void SetGPU();
    void SetRAM();
    Computer GetResult();
}

public class GamingComputerBuilder : IComputerBuilder
{
    private Computer _computer = new Computer();
    public void SetCPU() => _computer.CPU = "High-end CPU";
    public void SetGPU() => _computer.GPU = "High-end GPU";
    public void SetRAM() => _computer.RAM = "16GB";
    public Computer GetResult() => _computer;
}

public class Director
{
    public void Construct(IComputerBuilder builder)
    {
        builder.SetCPU();
        builder.SetGPU();
        builder.SetRAM();
    }
}

// 사용 예시
class Program
{
    static void Main()
    {
        Director director = new Director();
        IComputerBuilder builder = new GamingComputerBuilder();

        director.Construct(builder);
        Computer gamingPC = builder.GetResult();
        Console.WriteLine($"{gamingPC.CPU}, {gamingPC.GPU}, {gamingPC.RAM}");
    }
}

1-3. Factory Method

개요

  • 의도: 객체 생성을 서브클래스(또는 별도 팩토리)에서 담당하도록 위임하여, ‘어떤’ 객체가 생성되는지에 대한 의존성을 줄임
  • 특징: 객체를 생성할 때 구체 클래스를 직접 지정하지 않고, 팩토리 메서드를 통해 인스턴스를 반환

C# 예시 (간단 버전)

public abstract class Creator
{
    // 팩토리 메서드
    public abstract IProduct FactoryMethod();

    public string SomeOperation()
    {
        IProduct product = FactoryMethod();
        return product.GetName();
    }
}

public interface IProduct { string GetName(); }

public class ConcreteProductA : IProduct { public string GetName() => "Product A"; }
public class ConcreteProductB : IProduct { public string GetName() => "Product B"; }

public class ConcreteCreatorA : Creator
{
    public override IProduct FactoryMethod() => new ConcreteProductA();
}

public class ConcreteCreatorB : Creator
{
    public override IProduct FactoryMethod() => new ConcreteProductB();
}

// 사용 예시
class Program
{
    static void Main()
    {
        Creator creatorA = new ConcreteCreatorA();
        Console.WriteLine(creatorA.SomeOperation());

        Creator creatorB = new ConcreteCreatorB();
        Console.WriteLine(creatorB.SomeOperation());
    }
}

1-4. Prototype

개요

  • 의도: 기존 객체를 복제(clone)해서 새로운 객체를 생성
  • 특징: 복제가 반복적으로 필요한 경우, new로 생성하는 비용을 줄이거나 특정 상태를 그대로 복제할 수 있음

C# 예시 (간단 버전)

public abstract class Shape
{
    public int X { get; set; }
    public int Y { get; set; }
    public abstract Shape Clone();
}

public class Circle : Shape
{
    public int Radius { get; set; }

    public Circle(int x, int y, int r)
    {
        X = x; Y = y; Radius = r;
    }

    // 프로토타입 복제 메서드
    public override Shape Clone()
    {
        return new Circle(X, Y, Radius);
    }
}

// 사용 예시
class Program
{
    static void Main()
    {
        Circle circle1 = new Circle(10, 20, 5);
        Circle circle2 = (Circle)circle1.Clone();

        Console.WriteLine($"{circle2.X}, {circle2.Y}, {circle2.Radius}"); // 10, 20, 5
    }
}

1-5. Singleton

개요

  • 의도: 클래스의 인스턴스가 오직 하나만 존재하도록 제한하고, 어디서든 전역 접근을 가능케 함
  • 특징: 주로 설정, 로거, 캐시 등 전역적으로 하나만 있으면 충분한 자원 관리용으로 자주 사용

C# 예시 (간단 버전)

public sealed class Logger
{
    private static readonly Logger _instance = new Logger();
    private Logger() { }

    public static Logger Instance => _instance;

    public void Log(string message) => Console.WriteLine($"[Log] {message}");
}

// 사용 예시
class Program
{
    static void Main()
    {
        Logger.Instance.Log("싱글턴 테스트");
    }
}

1-6. Object Pool (추가)

개요

  • 의도: 객체 생성 비용이 큰 경우, 미리 일정 수의 객체를 만들어 풀(Pool)에 저장해두고 필요 시 꺼내 쓰는 패턴
  • 특징: 사용 후 반환하면 재활용하므로, 빈번한 생성/소멸 비용을 줄일 수 있음

C# 예시 (간단 버전)

public class Connection
{
    public int Id { get; set; }
    public void Connect() => Console.WriteLine($"Connection {Id} 연결");
    public void Disconnect() => Console.WriteLine($"Connection {Id} 해제");
}

public class ConnectionPool
{
    private Queue<Connection> _available = new Queue<Connection>();

    public ConnectionPool(int size)
    {
        for (int i = 0; i < size; i++)
        {
            _available.Enqueue(new Connection { Id = i });
        }
    }

    public Connection Acquire()
    {
        if (_available.Count > 0)
            return _available.Dequeue();
        throw new Exception("풀에 더 이상 Connection이 없습니다.");
    }

    public void Release(Connection conn)
    {
        conn.Disconnect();
        _available.Enqueue(conn);
    }
}

// 사용 예시
class Program
{
    static void Main()
    {
        ConnectionPool pool = new ConnectionPool(2);
        var conn1 = pool.Acquire();
        conn1.Connect();

        var conn2 = pool.Acquire();
        conn2.Connect();

        pool.Release(conn1); // 다시 풀에 반환
        var conn3 = pool.Acquire(); // conn1 재활용
        conn3.Connect();
    }
}

2. 구조(Structural) 패턴

2-1. Adapter

개요

  • 의도: 호환되지 않는 인터페이스를 다른 인터페이스로 ‘변환’하여, 기존 코드를 수정하지 않고 재사용
  • 특징: 클래스나 객체의 인터페이스를 ‘중간에 맞춰주는 어댑터’를 통해 연결

C# 예시 (간단 버전)

// 기존에 사용하던 인터페이스
public interface ITarget
{
    void Request();
}

// 새로 도입된, 호환되지 않는 클래스
public class Adaptee
{
    public void SpecificRequest() => Console.WriteLine("Adaptee 실행");
}

// 어댑터
public class Adapter : ITarget
{
    private readonly Adaptee _adaptee = new Adaptee();
    public void Request() => _adaptee.SpecificRequest();
}

// 사용 예시
class Program
{
    static void Main()
    {
        ITarget target = new Adapter();
        target.Request();
    }
}

2-2. Bridge

개요

  • 의도: 구현부와 추상부를 분리하여, 둘이 독립적으로 확장 가능하게 하는 패턴
  • 특징: 계층(추상화)과 구현부를 별도의 클래스로 두고, 런타임에 상호 조합을 통해 기능 확장

C# 예시 (간단 버전)

// 구현부 인터페이스
public interface IRenderer
{
    void RenderCircle(float radius);
}

// 구체 구현
public class VectorRenderer : IRenderer
{
    public void RenderCircle(float radius) => Console.WriteLine($"벡터로 원 그리기: 반지름 {radius}");
}

public class RasterRenderer : IRenderer
{
    public void RenderCircle(float radius) => Console.WriteLine($"래스터로 원 그리기: 반지름 {radius}");
}

// 추상부
public abstract class Shape
{
    protected IRenderer renderer;
    protected Shape(IRenderer renderer) { this.renderer = renderer; }
    public abstract void Draw();
}

public class Circle : Shape
{
    private float _radius;
    public Circle(IRenderer renderer, float radius) : base(renderer)
    {
        _radius = radius;
    }
    public override void Draw() => renderer.RenderCircle(_radius);
}

// 사용 예시
class Program
{
    static void Main()
    {
        Shape circle1 = new Circle(new VectorRenderer(), 5);
        circle1.Draw();

        Shape circle2 = new Circle(new RasterRenderer(), 10);
        circle2.Draw();
    }
}

2-3. Composite

개요

  • 의도: 트리 구조로 객체를 구성하여, 단일 객체와 복합 객체를 동일하게 다루도록 함
  • 특징: ‘부분-전체(Part-Whole)’ 계층을 표현하고, 클라이언트 입장에서 일관된 인터페이스를 제공

C# 예시 (간단 버전)

public abstract class Component
{
    protected string name;
    public Component(string name) { this.name = name; }
    public abstract void Display(int depth);
}

public class Leaf : Component
{
    public Leaf(string name) : base(name) {}
    public override void Display(int depth) => 
        Console.WriteLine(new string('-', depth) + name);
}

public class CompositeNode : Component
{
    private List<Component> _children = new List<Component>();

    public CompositeNode(string name) : base(name) {}

    public void Add(Component c) => _children.Add(c);
    public void Remove(Component c) => _children.Remove(c);

    public override void Display(int depth)
    {
        Console.WriteLine(new string('-', depth) + name);
        foreach (var child in _children)
        {
            child.Display(depth + 2);
        }
    }
}

// 사용 예시
class Program
{
    static void Main()
    {
        CompositeNode root = new CompositeNode("Root");
        root.Add(new Leaf("Leaf A"));
        root.Add(new Leaf("Leaf B"));

        CompositeNode sub = new CompositeNode("Sub");
        sub.Add(new Leaf("Leaf X"));
        sub.Add(new Leaf("Leaf Y"));
        root.Add(sub);

        root.Display(1);
    }
}

2-4. Decorator

개요

  • 의도: 기존 객체에 동적으로 기능을 덧씌우는(확장) 패턴
  • 특징: ‘상속’ 대신 ‘포함(Composition)’을 통해 확장하여, 여러 데코레이터를 조합하여 사용할 수 있음

C# 예시 (간단 버전)

public interface ICoffee
{
    string GetDescription();
    int GetCost();
}

public class BasicCoffee : ICoffee
{
    public string GetDescription() => "기본 커피";
    public int GetCost() => 2000;
}

// 데코레이터 기본
public abstract class CoffeeDecorator : ICoffee
{
    protected ICoffee _coffee;
    public CoffeeDecorator(ICoffee coffee) => _coffee = coffee;
    public virtual string GetDescription() => _coffee.GetDescription();
    public virtual int GetCost() => _coffee.GetCost();
}

// 구체 데코레이터
public class MilkDecorator : CoffeeDecorator
{
    public MilkDecorator(ICoffee coffee) : base(coffee) {}
    public override string GetDescription() => base.GetDescription() + ", 우유";
    public override int GetCost() => base.GetCost() + 500;
}

public class SyrupDecorator : CoffeeDecorator
{
    public SyrupDecorator(ICoffee coffee) : base(coffee) {}
    public override string GetDescription() => base.GetDescription() + ", 시럽";
    public override int GetCost() => base.GetCost() + 300;
}

// 사용 예시
class Program
{
    static void Main()
    {
        ICoffee coffee = new BasicCoffee();
        coffee = new MilkDecorator(coffee);
        coffee = new SyrupDecorator(coffee);

        Console.WriteLine(coffee.GetDescription()); // "기본 커피, 우유, 시럽"
        Console.WriteLine(coffee.GetCost());        // 2800
    }
}

2-5. Facade

개요

  • 의도: 복잡한 서브시스템들을 단순화된 인터페이스(파사드)로 감싸서 쉽게 사용하도록 함
  • 특징: 여러 클래스로 구성된 복잡한 로직을 한 곳에서 간략히 제공

C# 예시 (간단 버전)

public class SubSystemA
{
    public void DoA() => Console.WriteLine("서브시스템 A 실행");
}
public class SubSystemB
{
    public void DoB() => Console.WriteLine("서브시스템 B 실행");
}
public class SubSystemC
{
    public void DoC() => Console.WriteLine("서브시스템 C 실행");
}

// 파사드
public class Facade
{
    private SubSystemA a = new SubSystemA();
    private SubSystemB b = new SubSystemB();
    private SubSystemC c = new SubSystemC();

    public void Operation()
    {
        a.DoA();
        b.DoB();
        c.DoC();
    }
}

// 사용 예시
class Program
{
    static void Main()
    {
        Facade facade = new Facade();
        facade.Operation();
    }
}

2-6. Flyweight

개요

  • 의도: 많은 수의 객체가 필요할 때, 공통 상태를 공유하여 메모리 사용량을 줄이는 패턴
  • 특징: 내부 상태(공유)와 외부 상태(독립)를 구분, 내부 상태를 재사용하여 자원 절약

C# 예시 (간단 버전)

public class Flyweight
{
    public string IntrinsicState { get; private set; }
    public Flyweight(string intrinsicState) => IntrinsicState = intrinsicState;
    public void Operation(string extrinsicState)
    {
        Console.WriteLine($"Intrinsic: {IntrinsicState}, Extrinsic: {extrinsicState}");
    }
}

public class FlyweightFactory
{
    private Dictionary<string, Flyweight> _flyweights = new Dictionary<string, Flyweight>();
    public Flyweight GetFlyweight(string key)
    {
        if (!_flyweights.ContainsKey(key))
        {
            _flyweights[key] = new Flyweight(key);
        }
        return _flyweights[key];
    }
}

// 사용 예시
class Program
{
    static void Main()
    {
        FlyweightFactory factory = new FlyweightFactory();

        Flyweight fw1 = factory.GetFlyweight("A");
        fw1.Operation("X");

        Flyweight fw2 = factory.GetFlyweight("A");
        fw2.Operation("Y");

        // fw1과 fw2는 IntrinsicState가 "A"로 동일 객체(혹은 공유)
    }
}

2-7. Proxy

개요

  • 의도: 실제 객체에 대한 대리인을 두어, 접근 제어나 지연 로딩, 로깅 등 추가 작업을 수행
  • 특징: 클라이언트는 프록시를 통해 실제 객체에 접근하며, 외부적으로는 동일한 인터페이스

C# 예시 (간단 버전)

public interface IService
{
    void Operation();
}

public class RealService : IService
{
    public void Operation() => Console.WriteLine("실제 서비스 동작");
}

public class ServiceProxy : IService
{
    private RealService _realService;
    public void Operation()
    {
        Console.WriteLine("프록시에서 접근 제어/로깅 가능");
        if (_realService == null) _realService = new RealService();
        _realService.Operation();
    }
}

// 사용 예시
class Program
{
    static void Main()
    {
        IService proxy = new ServiceProxy();
        proxy.Operation(); // 내부에서 실제 서비스를 생성 후 실행
    }
}

3. 행위(Behavioral) 패턴

3-1. Chain of Responsibility

개요

  • 의도: 요청을 보내면, 해당 요청을 처리할 수 있는 ‘체인(연쇄)’을 따라 순서대로 처리 기회를 부여
  • 특징: 각 객체는 요청을 처리하거나, 다음 객체로 넘길 수 있음 (분산된 책임)

C# 예시 (간단 버전)

public abstract class Handler
{
    protected Handler _next;
    public Handler SetNext(Handler next)
    {
        _next = next;
        return next;
    }
    public abstract void HandleRequest(int request);
}

public class ConcreteHandlerA : Handler
{
    public override void HandleRequest(int request)
    {
        if (request < 10)
            Console.WriteLine($"Handler A 처리: {request}");
        else
            _next?.HandleRequest(request);
    }
}

public class ConcreteHandlerB : Handler
{
    public override void HandleRequest(int request)
    {
        if (request < 20)
            Console.WriteLine($"Handler B 처리: {request}");
        else
            _next?.HandleRequest(request);
    }
}

// 사용 예시
class Program
{
    static void Main()
    {
        Handler h1 = new ConcreteHandlerA();
        Handler h2 = new ConcreteHandlerB();
        h1.SetNext(h2);

        h1.HandleRequest(5);  // A가 처리
        h1.HandleRequest(15); // B가 처리
        h1.HandleRequest(25); // 아무도 처리 안 함
    }
}

3-2. Command

개요

  • 의도: 요청을 객체(커맨드)로 캡슐화하여, 실행/취소(Undo)/재실행 등을 유연하게 관리
  • 특징: 수행할 동작(Receiver)과 실행 로직(Invoker)을 명령 객체로 분리, 큐나 스택에 보관 가능

C# 예시 (간단 버전)

public interface ICommand
{
    void Execute();
    void Undo();
}

// Receiver
public class Light
{
    public void On() => Console.WriteLine("전등 켜짐");
    public void Off() => Console.WriteLine("전등 꺼짐");
}

// Concrete Command
public class LightOnCommand : ICommand
{
    private Light _light;
    public LightOnCommand(Light light) => _light = light;
    public void Execute() => _light.On();
    public void Undo() => _light.Off();
}

// Invoker
public class RemoteControl
{
    private ICommand _command;
    public void SetCommand(ICommand command) => _command = command;
    public void PressButton() => _command?.Execute();
    public void PressUndo() => _command?.Undo();
}

// 사용 예시
class Program
{
    static void Main()
    {
        Light light = new Light();
        ICommand onCommand = new LightOnCommand(light);

        RemoteControl remote = new RemoteControl();
        remote.SetCommand(onCommand);
        remote.PressButton(); // 전등 켜짐
        remote.PressUndo();   // 전등 꺼짐
    }
}

3-3. Interpreter

개요

  • 의도: 간단한 언어(또는 문법)에서 문장을 해석(Interpreter)하기 위한 패턴
  • 특징: 문법 규칙을 클래스로 정의하고, 해당 규칙에 따라 해석 로직을 구성

C# 예시 (단순 버전)

public interface IExpression
{
    int Interpret(Dictionary<string, int> context);
}

public class NumberExpression : IExpression
{
    private string _name;
    public NumberExpression(string name) => _name = name;
    public int Interpret(Dictionary<string, int> context) 
        => context[_name];
}

public class PlusExpression : IExpression
{
    private IExpression _left, _right;
    public PlusExpression(IExpression left, IExpression right)
    {
        _left = left; _right = right;
    }
    public int Interpret(Dictionary<string, int> context)
        => _left.Interpret(context) + _right.Interpret(context);
}

// 사용 예시
class Program
{
    static void Main()
    {
        // 표현식: x + y
        IExpression expression = new PlusExpression(
            new NumberExpression("x"),
            new NumberExpression("y")
        );

        Dictionary<string, int> context = new Dictionary<string, int>
        {
            { "x", 10 },
            { "y", 5 }
        };

        Console.WriteLine(expression.Interpret(context)); // 15
    }
}

3-4. Iterator

개요

  • 의도: 내부 표현을 노출하지 않고 객체 묶음(컬렉션)을 순회(Iterator)하는 방법을 제공
  • 특징: C#의 IEnumerator, IEnumerable이 대표적인 예시

C# 예시 (간단 버전)

public class CustomCollection : IEnumerable<int>
{
    private int[] _items;
    public CustomCollection(int[] items) => _items = items;

    public IEnumerator<int> GetEnumerator()
    {
        for(int i=0; i<_items.Length; i++)
        {
            yield return _items[i];
        }
    }

    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

// 사용 예시
class Program
{
    static void Main()
    {
        var collection = new CustomCollection(new[] {1, 2, 3});
        foreach(var item in collection)
        {
            Console.WriteLine(item);
        }
    }
}

3-5. Mediator

개요

  • 의도: 객체들 간 복잡한 상호작용을 ‘중재자’ 객체 하나가 맡아, 결합도를 낮춤
  • 특징: 각 객체는 중재자만 참조하여, 상호 직접 연결을 최소화

C# 예시 (간단 버전)

public abstract class Mediator
{
    public abstract void SendMessage(string message, Colleague colleague);
}

public abstract class Colleague
{
    protected Mediator _mediator;
    public Colleague(Mediator mediator) => _mediator = mediator;
}

public class ConcreteColleagueA : Colleague
{
    public ConcreteColleagueA(Mediator mediator) : base(mediator) {}
    public void Notify(string message) => Console.WriteLine($"A가 받은 메시지: {message}");
    public void Send(string message) => _mediator.SendMessage(message, this);
}

public class ConcreteColleagueB : Colleague
{
    public ConcreteColleagueB(Mediator mediator) : base(mediator) {}
    public void Notify(string message) => Console.WriteLine($"B가 받은 메시지: {message}");
    public void Send(string message) => _mediator.SendMessage(message, this);
}

// 구체 Mediator
public class ConcreteMediator : Mediator
{
    public ConcreteColleagueA ColleagueA { get; set; }
    public ConcreteColleagueB ColleagueB { get; set; }

    public override void SendMessage(string message, Colleague colleague)
    {
        if(colleague == ColleagueA)
            ColleagueB.Notify(message);
        else
            ColleagueA.Notify(message);
    }
}

// 사용 예시
class Program
{
    static void Main()
    {
        ConcreteMediator mediator = new ConcreteMediator();
        ConcreteColleagueA a = new ConcreteColleagueA(mediator);
        ConcreteColleagueB b = new ConcreteColleagueB(mediator);

        mediator.ColleagueA = a;
        mediator.ColleagueB = b;

        a.Send("안녕 B?");
        b.Send("안녕 A!");
    }
}

3-6. Memento

개요

  • 의도: 객체의 내부 상태를 캡슐화하여 저장하고, 나중에 해당 상태로 복원할 수 있게 함
  • 특징: 상태 히스토리를 관리하여 undo/redo 기능 등을 구현

C# 예시 (간단 버전)

// Originator
public class Editor
{
    public string Content { get; set; }

    public Memento Save() => new Memento(Content);
    public void Restore(Memento memento) => Content = memento.State;
}

// Memento
public class Memento
{
    public string State { get; }
    public Memento(string state) => State = state;
}

// Caretaker
public class History
{
    private Stack<Memento> _history = new Stack<Memento>();
    public void Push(Memento m) => _history.Push(m);
    public Memento Pop() => _history.Pop();
}

// 사용 예시
class Program
{
    static void Main()
    {
        Editor editor = new Editor();
        History history = new History();

        editor.Content = "버전 1";
        history.Push(editor.Save());

        editor.Content = "버전 2";
        history.Push(editor.Save());

        // 복원
        editor.Restore(history.Pop());
        Console.WriteLine(editor.Content); // "버전 2"
        editor.Restore(history.Pop());
        Console.WriteLine(editor.Content); // "버전 1"
    }
}

3-7. Null Object (추가)

개요

  • 의도: ‘Null’ 참조를 반환하는 대신, 아무 동작도 하지 않는 ‘Null 객체’를 사용하여 조건문을 줄이고 코드 단순화
  • 특징: if (obj != null) 같은 방어코드 없이 호출 가능. Null 대신 ‘빈(Empty)’ 동작을 제공

C# 예시 (간단 버전)

public interface ILogger
{
    void Log(string message);
}

public class ConsoleLogger : ILogger
{
    public void Log(string message) => Console.WriteLine(message);
}

public class NullLogger : ILogger
{
    public void Log(string message) { /* 아무 것도 안 함 */ }
}

class Program
{
    static void Main()
    {
        ILogger logger = new NullLogger(); // 혹은 new ConsoleLogger()
        // logger가 null이 아님 -> 바로 메서드 호출 가능
        logger.Log("이 메시지는 아무 일도 일어나지 않습니다.");
    }
}

3-8. Observer

개요

  • 의도: 어떤 객체(Subject)의 상태가 바뀔 때, 이를 의존하고 있는 객체(Observers)들에게 자동으로 알림
  • 특징: 여러 옵저버가 동시에 Subject를 구독하고, 변경 시 일괄 통지

C# 예시 (간단 버전)

public interface IObserver
{
    void Update(string data);
}

public interface ISubject
{
    void Attach(IObserver observer);
    void Detach(IObserver observer);
    void Notify();
}

public class WeatherStation : ISubject
{
    private List<IObserver> _observers = new List<IObserver>();
    private string _weather;
    public string Weather
    {
        get => _weather;
        set { _weather = value; Notify(); }
    }

    public void Attach(IObserver observer) => _observers.Add(observer);
    public void Detach(IObserver observer) => _observers.Remove(observer);

    public void Notify()
    {
        foreach (var obs in _observers)
        {
            obs.Update(_weather);
        }
    }
}

public class DisplayDevice : IObserver
{
    private string _name;
    public DisplayDevice(string name) => _name = name;
    public void Update(string data) => Console.WriteLine($"{_name} 디스플레이: {data}");
}

// 사용 예시
class Program
{
    static void Main()
    {
        WeatherStation station = new WeatherStation();
        DisplayDevice phone = new DisplayDevice("폰");
        DisplayDevice tv = new DisplayDevice("TV");

        station.Attach(phone);
        station.Attach(tv);

        station.Weather = "맑음";
        station.Weather = "비";
    }
}

3-9. State

개요

  • 의도: 객체의 내부 상태에 따라 행동이 달라지도록, 상태를 객체로 분리하여 캡슐화
  • 특징: 상태 전환 로직을 각 상태 클래스 안에 두어, 조건문 없이도 상태 전이가 가능

C# 예시 (간단 버전)

public interface IState
{
    void Handle(Context context);
}

public class Context
{
    public IState State { get; set; }
    public Context(IState state) => State = state;
    public void Request() => State.Handle(this);
}

public class ConcreteStateA : IState
{
    public void Handle(Context context)
    {
        Console.WriteLine("State A 처리");
        context.State = new ConcreteStateB();
    }
}

public class ConcreteStateB : IState
{
    public void Handle(Context context)
    {
        Console.WriteLine("State B 처리");
        context.State = new ConcreteStateA();
    }
}

// 사용 예시
class Program
{
    static void Main()
    {
        Context context = new Context(new ConcreteStateA());
        context.Request(); // A -> B
        context.Request(); // B -> A
        context.Request(); // A -> B
    }
}

3-10. Strategy

개요

  • 의도: 알고리즘 군(여러 로직)을 정의하고, 런타임에 선택적으로 바꿔 쓸 수 있게 캡슐화
  • 특징: 조건문 없이도 적절한 알고리즘을 교체 가능. 예: 할인 정책, 정렬 방식 등

C# 예시 (간단 버전)

public interface IDiscountStrategy
{
    decimal GetDiscountedPrice(decimal originalPrice);
}

public class FixedDiscount : IDiscountStrategy
{
    private decimal _discount;
    public FixedDiscount(decimal discount) => _discount = discount;
    public decimal GetDiscountedPrice(decimal originalPrice) => originalPrice - _discount;
}

public class RateDiscount : IDiscountStrategy
{
    private decimal _rate;
    public RateDiscount(decimal rate) => _rate = rate; // 0.2 = 20% 할인
    public decimal GetDiscountedPrice(decimal originalPrice) => originalPrice * (1 - _rate);
}

public class ShoppingCart
{
    private IDiscountStrategy _strategy;
    public ShoppingCart(IDiscountStrategy strategy) => _strategy = strategy;
    public decimal CalculatePrice(decimal price) => _strategy.GetDiscountedPrice(price);
}

// 사용 예시
class Program
{
    static void Main()
    {
        var cart1 = new ShoppingCart(new FixedDiscount(500));
        Console.WriteLine(cart1.CalculatePrice(2000)); // 1500

        var cart2 = new ShoppingCart(new RateDiscount(0.2m));
        Console.WriteLine(cart2.CalculatePrice(2000)); // 1600
    }
}

3-11. Template Method

개요

  • 의도: 알고리즘의 골격(템플릿)을 정의하고, 세부 단계 구현은 서브클래스가 담당
  • 특징: 상위 클래스에서 전체 흐름을 정의, 하위 클래스에서 특정 단계만 오버라이드

C# 예시 (간단 버전)

public abstract class Game
{
    public void Play()
    {
        Initialize();
        StartPlay();
        EndPlay();
    }
    protected abstract void Initialize();
    protected abstract void StartPlay();
    protected abstract void EndPlay();
}

public class Soccer : Game
{
    protected override void Initialize() => Console.WriteLine("축구 게임 준비");
    protected override void StartPlay() => Console.WriteLine("축구 시작");
    protected override void EndPlay() => Console.WriteLine("축구 종료");
}

// 사용 예시
class Program
{
    static void Main()
    {
        Game game = new Soccer();
        game.Play();
    }
}

3-12. Visitor

개요

  • 의도: 객체 구조에 새 연산을 추가할 때, 원래 구조를 변경하지 않고도 확장 가능
  • 특징: 방문자(Visitor)가 요소들의 ‘클래스 종류에 따라’ 다른 동작을 수행

C# 예시 (간단 버전)

public interface IVisitor
{
    void Visit(ElementA element);
    void Visit(ElementB element);
}

public abstract class Element
{
    public abstract void Accept(IVisitor visitor);
}

public class ElementA : Element
{
    public override void Accept(IVisitor visitor) => visitor.Visit(this);
    public string AFeature() => "A 요소 기능";
}

public class ElementB : Element
{
    public override void Accept(IVisitor visitor) => visitor.Visit(this);
    public string BFeature() => "B 요소 기능";
}

public class ConcreteVisitor : IVisitor
{
    public void Visit(ElementA element)
    {
        Console.WriteLine($"방문자, {element.AFeature()} 처리");
    }

    public void Visit(ElementB element)
    {
        Console.WriteLine($"방문자, {element.BFeature()} 처리");
    }
}

// 사용 예시
class Program
{
    static void Main()
    {
        List<Element> elements = new List<Element> { new ElementA(), new ElementB() };
        IVisitor visitor = new ConcreteVisitor();

        foreach(var e in elements)
        {
            e.Accept(visitor);
        }
    }
}

4. 추가적인 패턴

위에서 GoF가 정의한 23개 패턴과 함께, 현업에서도 자주 언급되는 패턴 5가지 예시를 살펴봅니다.

4-1. Model-View-Controller (MVC)

개요

  • 의도: UI, 데이터, 로직을 분리하여 유지보수와 확장성을 높이는 아키텍처 패턴
  • 특징:
    • Model: 비즈니스 로직, 데이터 관리
    • View: 사용자 인터페이스
    • Controller: 입력 제어 및 Model-View 연결

C# 예시 (간단 스케치: ASP.NET Core MVC 형태 X, 개념만)

// Model
public class Product
{
    public string Name { get; set; }
    public decimal Price { get; set; }
}

// View (간단하게 콘솔 출력)
public class ProductView
{
    public void ShowProduct(string name, decimal price)
    {
        Console.WriteLine($"상품명: {name}, 가격: {price}");
    }
}

// Controller
public class ProductController
{
    private Product _model;
    private ProductView _view;

    public ProductController(Product model, ProductView view)
    {
        _model = model;
        _view = view;
    }

    public void UpdateView()
    {
        _view.ShowProduct(_model.Name, _model.Price);
    }

    public void SetProductName(string name) => _model.Name = name;
    public void SetProductPrice(decimal price) => _model.Price = price;
}

// 사용 예시
class Program
{
    static void Main()
    {
        Product model = new Product { Name = "커피", Price = 3000 };
        ProductView view = new ProductView();
        ProductController controller = new ProductController(model, view);

        controller.UpdateView();
        controller.SetProductName("라떼");
        controller.SetProductPrice(4000);
        controller.UpdateView();
    }
}

4-2. Repository

개요

  • 의도: 데이터 접근 로직(ORM, DB 쿼리 등)을 캡슐화하여, 도메인 로직과 분리
  • 특징: ‘가짜(InMemory) 구현’과 ‘실 DB 구현’을 쉽게 교체하고, 비즈니스 레이어가 DB와 직접 상호 작용하지 않도록 정리

C# 예시 (간단 버전)

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public interface IProductRepository
{
    Product GetById(int id);
    void Add(Product product);
    // etc...
}

// InMemory Repository 예시
public class InMemoryProductRepository : IProductRepository
{
    private List<Product> _products = new List<Product>();
    public Product GetById(int id) => _products.FirstOrDefault(p => p.Id == id);
    public void Add(Product product) => _products.Add(product);
}

// 사용 예시
class Program
{
    static void Main()
    {
        IProductRepository repo = new InMemoryProductRepository();
        repo.Add(new Product { Id = 1, Name = "커피" });
        Product p = repo.GetById(1);
        Console.WriteLine(p?.Name); // "커피"
    }
}

4-3. Dependency Injection (DI)

개요

  • 의도: 객체 간 의존성을 외부에서 주입(Injection) 받아, 클래스 간 결합도를 낮춤
  • 특징: IoC(Inversion of Control) 컨테이너를 이용하거나, 생성자/속성/메서드 인젝션 방식을 활용

C# 예시 (간단 버전)

public interface IMessageService
{
    void SendMessage(string message);
}

public class EmailService : IMessageService
{
    public void SendMessage(string message) => Console.WriteLine($"이메일 전송: {message}");
}

public class SmsService : IMessageService
{
    public void SendMessage(string message) => Console.WriteLine($"SMS 전송: {message}");
}

public class Notifier
{
    private readonly IMessageService _messageService;
    public Notifier(IMessageService messageService) // 생성자 주입
    {
        _messageService = messageService;
    }
    
    public void Notify(string message)
    {
        _messageService.SendMessage(message);
    }
}

// 사용 예시
class Program
{
    static void Main()
    {
        // 외부에서 어떤 구현을 넣어줄지 결정
        IMessageService emailService = new EmailService();
        Notifier notifier = new Notifier(emailService);
        notifier.Notify("안녕하세요!");

        // 필요에 따라 다른 구현체 주입
        IMessageService smsService = new SmsService();
        Notifier notifier2 = new Notifier(smsService);
        notifier2.Notify("SMS 발송");
    }
}

마무리

위에서 살펴본 28개 패턴(GoF 23 + 추가 5)은 대표적이고도 실무에서 유용하게 쓰이는 사례들입니다.

  • 특정 상황에서 어떤 패턴을 적용해야 할지 판단 기준은 **의도(Intent)**와 **특징(Structure)**이 가장 중요합니다.
  • 코드를 작성하다 보면 자연스럽게 “이 문제를 해결하려면 어떤 구조가 필요하지?” 하고 고민하게 되고, 그 해답의 일부가 바로 디자인 패턴입니다.

정리

  1. 생성 패턴(Creational): 객체 생성 로직을 체계적으로 캡슐화
  2. 구조 패턴(Structural): 클래스나 객체를 조합해 더 큰 구조를 만들 때 유용
  3. 행위 패턴(Behavioral): 객체 간의 상호작용(로직 흐름)에 초점
  4. 추가 패턴: MVC, Repository, DI 등 현대 개발에서 자주 언급

이 글을 통해 디자인 패턴의 전체적인 숲을 조망해 보셨길 바랍니다. 실제로는 각 패턴을 더 깊이 파고들고, 자신의 프로젝트에 맞춰 재해석/응용하는 과정이 중요합니다. 필요에 맞게 유연하게 적용하면서 유지보수성 높은 코드를 작성해보세요!

감사합니다.

댓글

이 블로그의 인기 게시물

실버테크(Silver-Tech)

고령화 시대의 새로운 혁신, 실버테크(Silver Tech) 1. 실버테크란 무엇인가? 현대 사회는 빠른 속도로 고령화되고 있으며, 이에 따라 노년층을 위한 기술과 서비스가 더욱 중요해지고 있습니다. **실버테크(Silver Tech)**는 노년층의 삶의 질을 향상시키기 위해 개발된 기술과 서비스를 의미합니다. 건강 관리, 안전, 생활 편의성, 사회적 연결 등을 지원하는 다양한 혁신적인 기술이 포함됩니다. 2. 실버테크의 주요 분야 1) 헬스케어 및 원격 의료 스마트워치나 피트니스 트래커를 활용한  건강 모니터링 온라인 진료를 통해 병원 방문 없이 상담이 가능한  원격 의료 서비스 건강 상태를 분석하고 관리해주는  AI 기반 건강 관리 시스템 2) 스마트홈 및 생활 보조 기술 음성 인식으로 조작이 가능한  스마트 가전 낙상 감지 및 응급 호출 기능이 포함된  스마트 센서 노년층을 위한  자동화된 조명, 난방 시스템 3) 커뮤니케이션 및 소셜 기술 사용하기 쉬운 UI를 적용한  실버폰 및 태블릿 가족 및 친구와 소통을 쉽게 해주는  화상 통화 및 메시징 앱 노인 대상의  디지털 리터러시 교육 프로그램 4) 여가 및 엔터테인먼트 노년층을 위한  VR(가상현실) 체험 프로그램 온라인으로 제공되는  문화, 교육 콘텐츠 손쉽게 즐길 수 있는  전용 게임 및 앱 5) 이동성 및 모빌리티 솔루션 편리한 이동을 돕는  전동 휠체어 및 스쿠터 실시간 위치 추적이 가능한  스마트 네비게이션 시스템 고령자를 위한  자율주행 차량 및 호출 서비스 3. 실버테크가 가져오는 변화 - 삶의 질 향상 기술의 발전으로 인해 노년층이 더욱 독립적으로 생활할 수 있도록 도와주며, 건강하고 편리한 삶을 지원합니다. - 사회적 고립 해소 화상 통화, 소셜 네트워크, 온라인 커뮤니티 등을 통해 사회적 관계를 유지할 수 있도록 돕습니다. - 경제적 기회 창출 실버테크 산...

로또 번호 생성기

키움증권 Open API를 활용하여 갭 매매 전략 구현- C#

  1. 갭 매매 전략이란? 갭 매매란 주가가 전일 종가 대비 큰 폭으로 상승(갭업)하거나 하락(갭다운)하여 개장할 때, 발생한 갭의 움직임을 활용하여 수익을 내는 전략입니다. 이번에 소개할 전략은 다음 조건에 해당하는 종목을 대상으로 합니다. 조건검색 : 전일 대비 오늘 시가가 5% 이상 갭업으로 시작한 종목 전략 실행 : 개장 후 30분 이내에 주가가 전일 종가 수준까지 회귀하면 매수 진입 후, 갭이 완전히 메워질 때 매도합니다. 손절매 : 진입 가격에서 갭 상승분의 50% 하락 시 손절 익절매 : 갭 상승분의 50% 이상 추가 상승 시 익절합니다. 2. 구현 환경 및 준비사항 키움증권 API+ 설치 (영웅문 API) Visual Studio Community Edition (C#) 키움증권 계좌 (모의투자 계좌 추천) 3. C# 코드 전체 구현 (주석 포함) 다음 코드에는 갭 매매 전략, 주문 실행, 체결 확인 및 정정 주문까지 포함되어 있습니다. using System; using System.Collections.Generic; using AxKHOpenAPILib; public class GapTrading { private AxKHOpenAPI axKHOpenAPI; private string accountNumber; // 매수 후 체결된 정보를 저장할 Dictionary (종목코드, (체결가격, 체결수량)) private Dictionary< string , ( int price, int qty)> positions = new Dictionary< string , ( int , int )>(); public GapTradingStrategy (AxKHOpenAPI api, string accountNo) { axKHOpenAPI = api; accountNumber = accountNo; ...

Log4Net vs. Serilog 비교

🔍 Log4Net vs. Serilog 간단 비교 기준Log4NetSerilog 성능 중간 (충분하지만, 최신 라이브러리보단 느림) 빠르고 효율적 설정 방식 XML 기반 (전통적) JSON 기반 (모던함) 구조적 로깅 미지원 (기본 텍스트 로그) 강력한 구조적 로깅 지원 ASP.NET Core 통합 가능하지만 설정이 좀 복잡 간단하고 직관적 Sink(대상) 다양성 적당한 편 매우 다양하고 확장성 높음 생태계 & 유지보수 전통적, 유지보수 상태는 다소 정체 활발한 개발과 업데이트 🎯 어떤 프레임워크가 좋을까? Serilog를 추천하는 이유: 최신 기술 : ASP.NET Core와 완벽히 통합, 구조적 로깅이 뛰어나 JSON 로그 처리, 분석에 최적화됨. 높은 생산성 : 설정과 유지보수가 쉬움. 코드 기반 및 JSON 설정으로 직관적이고 빠른 개발이 가능. 확장성 : 파일, 콘솔, DB, Elasticsearch, Seq 등 다양한 Sink를 제공해 향후 확장성이 좋음. 🚀 결론 요약 Log4Net을 추천할 때Serilog를 추천할 때 기존 시스템과 호환이 필수 최신 ASP.NET Core 프로젝트 XML 설정 선호 JSON, 코드 기반 설정 선호 구조적 로깅 필요 없음 구조적 로깅과 분석 필요

C#으로 아두이노와의 시리얼 통신

  1. 들어가며 최근 IoT와 임베디드 시스템 개발이 증가하면서 PC와 아두이노 간의 통신을 효율적으로 처리하는 방법이 중요해졌습니다. 그중에서도 가장 널리 쓰이는 방법 중 하나가 바로  시리얼(Serial) 통신 입니다. 이번 글에서는 C#을 이용해 아두이노와의 시리얼 통신을 완벽히 이해하고 구현하는 방법을 자세히 소개하겠습니다. 2. 시리얼 통신이란? 시리얼 통신은 데이터를 한 번에 한 비트씩 순차적으로 전송하는 방식입니다. USB 포트를 통해 아두이노와 PC 간 데이터를 주고받을 때 주로 사용됩니다. 3. 준비물 아두이노(UNO, MEGA 등) PC 및 Visual Studio USB 케이블 4. 아두이노에서의 시리얼 통신 설정 아두이노 IDE에서 다음과 같은 코드를 작성하여 아두이노 보드에 업로드합니다. void setup () { Serial . begin ( 9600 ); // Baud rate 설정 } void loop () { if ( Serial . available ()) { char c = Serial . read (); // PC에서 받은 데이터 읽기 Serial . print ( "Received: " ); Serial . println (c); // 읽은 데이터를 다시 PC로 전송 } } 5. C#에서의 시리얼 통신 설정 C#에서는 System.IO.Ports 네임스페이스의 SerialPort 클래스를 사용합니다. 예제 코드 using System; using System.IO.Ports; class Program { static SerialPort port; static void Main () { port = new SerialPort( "COM3" , 9600 ); // 포트번호와 Baud rate 설정 port.Open(); port.DataReceived +...