디자인 패턴은 특정 맥락의 문제를 체계적으로 해결하기 위한 ‘재사용 가능한 솔루션’입니다.
- 목표: 코드의 재사용성, 유지보수성, 확장성을 높이는 것
- 종류: 크게 생성(Creational), 구조(Structural), 행위(Behavioral)로 나누며, 23개 GoF 패턴 이외에도 실무 환경에서 자주 쓰이는 여러 패턴이 있습니다.
본 글에서는 다음과 같이 28개 패턴을 소개합니다.
- 생성(Creational) 패턴
- Abstract Factory
- Builder
- Factory Method
- Prototype
- Singleton
- Object Pool (추가)
- 구조(Structural) 패턴
7. Adapter
8. Bridge
9. Composite
10. Decorator
11. Facade
12. Flyweight
13. Proxy - 행위(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 - 추가적인 패턴
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)**이 가장 중요합니다.
- 코드를 작성하다 보면 자연스럽게 “이 문제를 해결하려면 어떤 구조가 필요하지?” 하고 고민하게 되고, 그 해답의 일부가 바로 디자인 패턴입니다.
정리
- 생성 패턴(Creational): 객체 생성 로직을 체계적으로 캡슐화
- 구조 패턴(Structural): 클래스나 객체를 조합해 더 큰 구조를 만들 때 유용
- 행위 패턴(Behavioral): 객체 간의 상호작용(로직 흐름)에 초점
- 추가 패턴: MVC, Repository, DI 등 현대 개발에서 자주 언급
이 글을 통해 디자인 패턴의 전체적인 숲을 조망해 보셨길 바랍니다. 실제로는 각 패턴을 더 깊이 파고들고, 자신의 프로젝트에 맞춰 재해석/응용하는 과정이 중요합니다. 필요에 맞게 유연하게 적용하면서 유지보수성 높은 코드를 작성해보세요!
감사합니다.
.png)
댓글
댓글 쓰기