C# 11편: 디자인 패턴으로 코드의 재사용성 및 유지보수성 향상시키기
1. 디자인 패턴이란?
**디자인 패턴(Design Pattern)**은 자주 발생하는 문제를 해결하기 위한 객체지향 설계 원칙으로, 코드의 구조를 표준화하여 재사용성과 유지보수성을 높이는 데 도움을 줍니다. 특히 C#에서는 다양한 디자인 패턴을 활용하여 효율적이고 확장 가능한 코드를 작성할 수 있습니다.
디자인 패턴은 크게 세 가지로 분류됩니다:
- 생성 패턴: 객체 생성 방식을 관리하는 패턴
- 구조 패턴: 클래스 및 객체를 조합해 더 큰 구조를 만드는 패턴
- 행위 패턴: 객체 간 상호작용을 정의하고 관리하는 패턴
2. 생성 패턴 (Creational Patterns)
생성 패턴은 객체 생성 방식을 제어하여 필요할 때 유연하게 객체를 생성하고, 코드의 결합도를 낮추는 패턴입니다.
2.1 싱글톤 패턴 (Singleton Pattern)
싱글톤 패턴은 특정 클래스의 인스턴스를 하나만 생성하고, 어디에서든 이 인스턴스에 접근할 수 있도록 보장하는 패턴입니다. 주로 공유 자원이나 전역 상태를 관리할 때 유용합니다.
class Singleton
{
private static Singleton _instance;
private static readonly object _lock = new object();
private Singleton() { }
public static Singleton Instance
{
get
{
lock (_lock)
{
if (_instance == null)
_instance = new Singleton();
return _instance;
}
}
}
public void ShowMessage() => Console.WriteLine("싱글톤 인스턴스입니다!");
}
사용 예시
class Program
{
static void Main(string[] args)
{
Singleton instance1 = Singleton.Instance;
Singleton instance2 = Singleton.Instance;
instance1.ShowMessage();
Console.WriteLine(instance1 == instance2); // True (동일한 인스턴스)
}
}
2.2 팩토리 패턴 (Factory Pattern)
팩토리 패턴은 객체 생성 로직을 팩토리(공장) 메서드에 캡슐화하여, 객체 생성 방식을 변경할 수 있는 패턴입니다. 이 패턴을 사용하면 객체 생성 로직을 한 곳에서 관리하여 유연성을 높일 수 있습니다.
interface IProduct
{
void Use();
}
class ProductA : IProduct
{
public void Use() => Console.WriteLine("ProductA 사용");
}
class ProductB : IProduct
{
public void Use() => Console.WriteLine("ProductB 사용");
}
class ProductFactory
{
public static IProduct CreateProduct(string type)
{
return type switch
{
"A" => new ProductA(),
"B" => new ProductB(),
_ => throw new ArgumentException("잘못된 제품 타입")
};
}
}
사용 예시
class Program
{
static void Main(string[] args)
{
IProduct product = ProductFactory.CreateProduct("A");
product.Use(); // ProductA 사용
}
}
3. 구조 패턴 (Structural Patterns)
구조 패턴은 객체나 클래스를 조합해 더 큰 구조를 만들고, 객체 간의 관계를 쉽게 관리할 수 있도록 하는 패턴입니다.
3.1 어댑터 패턴 (Adapter Pattern)
어댑터 패턴은 호환되지 않는 인터페이스를 연결해주는 패턴으로, 서로 다른 인터페이스를 가진 클래스들이 상호작용할 수 있게 도와줍니다.
interface ITarget
{
void Request();
}
class Adaptee
{
public void SpecificRequest() => Console.WriteLine("호환되지 않는 요청");
}
class Adapter : ITarget
{
private Adaptee _adaptee;
public Adapter(Adaptee adaptee) => _adaptee = adaptee;
public void Request() => _adaptee.SpecificRequest();
}
사용 예시
class Program
{
static void Main(string[] args)
{
Adaptee adaptee = new Adaptee();
ITarget target = new Adapter(adaptee);
target.Request(); // 호환되지 않는 요청
}
}
3.2 데코레이터 패턴 (Decorator Pattern)
데코레이터 패턴은 객체를 감싸는 방식으로 기능을 추가할 때 사용합니다. 기존 클래스의 기능을 변경하지 않고 새로운 기능을 추가할 수 있는 장점이 있습니다.
interface IComponent
{
void Operation();
}
class ConcreteComponent : IComponent
{
public void Operation() => Console.WriteLine("기본 동작");
}
class Decorator : IComponent
{
private IComponent _component;
public Decorator(IComponent component) => _component = component;
public virtual void Operation() => _component.Operation();
}
class ConcreteDecorator : Decorator
{
public ConcreteDecorator(IComponent component) : base(component) { }
public override void Operation()
{
base.Operation();
Console.WriteLine("추가 기능 동작");
}
}
사용 예시
class Program
{
static void Main(string[] args)
{
IComponent component = new ConcreteComponent();
IComponent decoratedComponent = new ConcreteDecorator(component);
decoratedComponent.Operation(); // 기본 동작, 추가 기능 동작
}
}
4. 행위 패턴 (Behavioral Patterns)
행위 패턴은 객체나 클래스 간 상호작용을 정의하고 관리하는 패턴입니다.
4.1 옵저버 패턴 (Observer Pattern)
옵저버 패턴은 상태 변화를 감지하여 자동으로 알림을 받는 패턴으로, 주로 이벤트 기반 시스템에서 자주 사용됩니다.
interface IObserver
{
void Update(string message);
}
class Subject
{
private List<IObserver> _observers = new List<IObserver>();
public void Attach(IObserver observer) => _observers.Add(observer);
public void Detach(IObserver observer) => _observers.Remove(observer);
public void Notify(string message)
{
foreach (var observer in _observers)
{
observer.Update(message);
}
}
}
class ConcreteObserver : IObserver
{
private string _name;
public ConcreteObserver(string name) => _name = name;
public void Update(string message) => Console.WriteLine($"{_name}이(가) 메시지를 받았습니다: {message}");
}
사용 예시
class Program
{
static void Main(string[] args)
{
Subject subject = new Subject();
IObserver observer1 = new ConcreteObserver("Observer1");
IObserver observer2 = new ConcreteObserver("Observer2");
subject.Attach(observer1);
subject.Attach(observer2);
subject.Notify("이벤트 발생!");
}
}
출력
Observer1이(가) 메시지를 받았습니다: 이벤트 발생!
Observer2이(가) 메시지를 받았습니다: 이벤트 발생!
4.2 전략 패턴 (Strategy Pattern)
전략 패턴은 동일한 작업을 여러 방식으로 처리할 수 있도록 알고리즘을 선택할 수 있게 해줍니다.
interface IStrategy
{
void Execute();
}
class ConcreteStrategyA : IStrategy
{
public void Execute() => Console.WriteLine("전략 A 실행");
}
class ConcreteStrategyB : IStrategy
{
public void Execute() => Console.WriteLine("전략 B 실행");
}
class Context
{
private IStrategy _strategy;
public void SetStrategy(IStrategy strategy) => _strategy = strategy;
public void ExecuteStrategy() => _strategy.Execute();
}
사용 예시
class Program
{
static void Main(string[] args)
{
Context context = new Context();
context.SetStrategy(new ConcreteStrategyA());
context.ExecuteStrategy(); // 전략 A 실행
context.SetStrategy(new ConcreteStrategyB());
context.ExecuteStrategy(); // 전략 B 실행
}
}
5. 요약
- 싱글톤 패턴: 단일 인스턴스를 유지하여 전역에서 사용
- 팩토리 패턴: 객체 생성 로직을 팩토리 메서드로 분리하여 유연하게 생성
- 어댑터 패턴: 호환되지 않는 인터페이스를 연결
- 데코레이터 패턴: 기존 객체에 새로운 기능을 추가
- 옵저버 패턴: 상태 변화를 자동으로 감지하여 알림
- 전략 패턴: 여러 알고리즘을 유연하게 선택하여 사용
디자인 패턴을 사용하면 코드의 재사용성과 유연성이 크게 향상됩니다. 각 패턴을 실습해보며 다양한 상황에서 어떻게 적용할 수 있는지 익혀보세요!