재우니 개발자 블로그

 

 

 

https://x.com/alexxubyte/status/1915433600096624731?s=43

 

 

 

 

 

안녕하세요! 이미지에 보이는 9가지 객체지향 디자인 패턴을 C#으로 실생활 예시와 함께 설명해 드리겠습니다. 

 

1. 팩토리 패턴 (Factory Pattern)

실생활 시나리오: 피자 주문 애플리케이션

// 인터페이스 정의
public interface IPizza
{
    void Prepare();
    void Bake();
    void Cut();
    void Box();
}

// 구체적인 피자 클래스들
public class CheesePizza : IPizza
{
    public void Prepare() { Console.WriteLine("치즈 피자 준비 중..."); }
    public void Bake() { Console.WriteLine("치즈 피자 굽는 중..."); }
    public void Cut() { Console.WriteLine("치즈 피자 자르는 중..."); }
    public void Box() { Console.WriteLine("치즈 피자 포장 중..."); }
}

public class PepperoniPizza : IPizza
{
    public void Prepare() { Console.WriteLine("페퍼로니 피자 준비 중..."); }
    public void Bake() { Console.WriteLine("페퍼로니 피자 굽는 중..."); }
    public void Cut() { Console.WriteLine("페퍼로니 피자 자르는 중..."); }
    public void Box() { Console.WriteLine("페퍼로니 피자 포장 중..."); }
}

// 피자 팩토리
public class PizzaFactory
{
    public IPizza CreatePizza(string type)
    {
        IPizza pizza = null;
        
        if (type.Equals("cheese"))
        {
            pizza = new CheesePizza();
        }
        else if (type.Equals("pepperoni"))
        {
            pizza = new PepperoniPizza();
        }
        
        return pizza;
    }
}

// 클라이언트 코드
public class PizzaStore
{
    public static void Main()
    {
        PizzaFactory factory = new PizzaFactory();
        
        // 치즈 피자 주문
        IPizza cheesePizza = factory.CreatePizza("cheese");
        cheesePizza.Prepare();
        cheesePizza.Bake();
        cheesePizza.Cut();
        cheesePizza.Box();
    }
}

 

설명: 팩토리 패턴은 객체 생성 로직을 캡슐화하여 클라이언트 코드와 분리합니다. 클라이언트는 구체적인 클래스를 알 필요 없이 팩토리를 통해 객체를 생성할 수 있습니다.

 

 

2. 싱글톤 패턴 (Singleton Pattern)

실생활 시나리오: 데이터베이스 연결 관리자

public class DatabaseConnection
{
    // 단일 인스턴스 저장
    private static DatabaseConnection instance;
    
    // 스레드 안전을 위한 락 객체
    private static readonly object lockObject = new object();
    
    // 연결 정보
    private string connectionString;
    
    // 생성자는 private으로 외부에서 접근 불가
    private DatabaseConnection()
    {
        connectionString = "Server=myServerAddress;Database=myDataBase;User Id=myUsername;Password=myPassword;";
        Console.WriteLine("데이터베이스 연결 설정 완료");
    }
    
    // 인스턴스 접근 메서드 (스레드 안전한 구현)
    public static DatabaseConnection GetInstance()
    {
        // 더블 체크 락킹
        if (instance == null)
        {
            lock (lockObject)
            {
                if (instance == null)
                {
                    instance = new DatabaseConnection();
                }
            }
        }
        return instance;
    }
    
    public void Connect()
    {
        Console.WriteLine("데이터베이스에 연결 중...");
        // 실제 연결 코드
    }
    
    public void Disconnect()
    {
        Console.WriteLine("데이터베이스 연결 해제 중...");
        // 실제 연결 해제 코드
    }
}

// 사용 예시
public class Program
{
    public static void Main()
    {
        // 어디서든 같은 인스턴스 사용
        DatabaseConnection connection1 = DatabaseConnection.GetInstance();
        connection1.Connect();
        
        // 두 번째 호출에도 같은 인스턴스 반환
        DatabaseConnection connection2 = DatabaseConnection.GetInstance();
        
        // 두 변수가 같은 인스턴스인지 확인
        Console.WriteLine($"두 연결이 같은 인스턴스인가: {connection1 == connection2}");
        
        connection2.Disconnect();
    }
}

설명: 싱글톤 패턴은 클래스의 인스턴스가 오직 하나만 생성되도록 보장합니다. 데이터베이스 연결처럼 리소스가 많이 드는 작업은 여러 인스턴스를 만들면 비효율적이므로 싱글톤으로 관리합니다.

3. 빌더 패턴 (Builder Pattern)

실생활 시나리오: 주문 시스템

// 제품 클래스 (결과물)
public class Pizza
{
    public string Dough { get; set; }
    public string Sauce { get; set; }
    public List<string> Toppings { get; set; } = new List<string>();
    
    public void Display()
    {
        Console.WriteLine($"도우: {Dough}");
        Console.WriteLine($"소스: {Sauce}");
        Console.WriteLine("토핑:");
        foreach (var topping in Toppings)
        {
            Console.WriteLine($"- {topping}");
        }
    }
}

// 빌더 인터페이스
public interface IPizzaBuilder
{
    void SetDough(string dough);
    void SetSauce(string sauce);
    void AddTopping(string topping);
    Pizza Build();
}

// 구체적인 빌더
public class MargheritaPizzaBuilder : IPizzaBuilder
{
    private Pizza pizza = new Pizza();
    
    public void SetDough(string dough)
    {
        pizza.Dough = dough;
    }
    
    public void SetSauce(string sauce)
    {
        pizza.Sauce = sauce;
    }
    
    public void AddTopping(string topping)
    {
        pizza.Toppings.Add(topping);
    }
    
    public Pizza Build()
    {
        return pizza;
    }
}

// 디렉터 클래스 (선택적)
public class PizzaDirector
{
    private IPizzaBuilder builder;
    
    public PizzaDirector(IPizzaBuilder builder)
    {
        this.builder = builder;
    }
    
    public void MakeMargherita()
    {
        builder.SetDough("씬 크러스트");
        builder.SetSauce("토마토");
        builder.AddTopping("모짜렐라 치즈");
        builder.AddTopping("바질");
    }
    
    public void MakePepperoni()
    {
        builder.SetDough("두꺼운 크러스트");
        builder.SetSauce("토마토");
        builder.AddTopping("모짜렐라 치즈");
        builder.AddTopping("페퍼로니");
    }
}

// 사용 예시
public class Program
{
    public static void Main()
    {
        // 빌더 생성
        IPizzaBuilder builder = new MargheritaPizzaBuilder();
        
        // 디렉터 사용
        PizzaDirector director = new PizzaDirector(builder);
        director.MakeMargherita();
        
        // 피자 완성
        Pizza pizza = builder.Build();
        pizza.Display();
        
        // 직접 빌더 사용
        builder = new MargheritaPizzaBuilder();
        builder.SetDough("일반 도우");
        builder.SetSauce("바베큐");
        builder.AddTopping("치즈");
        builder.AddTopping("올리브");
        builder.AddTopping("버섯");
        
        Pizza customPizza = builder.Build();
        customPizza.Display();
    }
}

설명: 빌더 패턴은 복잡한 객체의 생성 과정을 단계별로 나누어 처리합니다. 위 예시에서는 피자를 만드는 과정을 도우 선택, 소스 선택, 토핑 추가 등의 단계로 나누어 처리합니다.

4. 어댑터 패턴 (Adapter Pattern)

실생활 시나리오: 오래된 결제 시스템을 새로운 시스템에 통합

// 새 결제 시스템이 요구하는 인터페이스
public interface INewPaymentSystem
{
    void ProcessPayment(decimal amount);
    bool VerifyPayment(string paymentId);
}

// 기존 레거시 결제 시스템
public class LegacyPaymentSystem
{
    public void MakePayment(double amount, string currency)
    {
        Console.WriteLine($"레거시 시스템: {amount} {currency} 결제 처리 중");
    }
    
    public int CheckStatus(string reference)
    {
        Console.WriteLine($"레거시 시스템: {reference} 결제 상태 확인 중");
        return 1; // 1: 성공, 0: 실패
    }
}

// 어댑터 클래스
public class PaymentSystemAdapter : INewPaymentSystem
{
    private LegacyPaymentSystem legacySystem;
    
    public PaymentSystemAdapter(LegacyPaymentSystem legacySystem)
    {
        this.legacySystem = legacySystem;
    }
    
    public void ProcessPayment(decimal amount)
    {
        // 새 인터페이스를 레거시 시스템에 맞게 변환
        legacySystem.MakePayment((double)amount, "KRW");
    }
    
    public bool VerifyPayment(string paymentId)
    {
        // 레거시 시스템의 응답을 새 인터페이스 형식으로 변환
        int status = legacySystem.CheckStatus(paymentId);
        return status == 1;
    }
}

// 클라이언트 코드
public class PaymentProcessor
{
    private INewPaymentSystem paymentSystem;
    
    public PaymentProcessor(INewPaymentSystem paymentSystem)
    {
        this.paymentSystem = paymentSystem;
    }
    
    public void MakeOrder(decimal amount, string orderId)
    {
        Console.WriteLine($"주문 ID: {orderId}, 금액: {amount}원 결제 시작");
        paymentSystem.ProcessPayment(amount);
        
        if (paymentSystem.VerifyPayment(orderId))
        {
            Console.WriteLine("결제 성공!");
        }
        else
        {
            Console.WriteLine("결제 실패!");
        }
    }
}

// 사용 예시
public class Program
{
    public static void Main()
    {
        // 레거시 시스템
        LegacyPaymentSystem legacySystem = new LegacyPaymentSystem();
        
        // 어댑터를 통해 새 인터페이스로 변환
        INewPaymentSystem adapter = new PaymentSystemAdapter(legacySystem);
        
        // 클라이언트에서 어댑터를 통해 레거시 시스템 사용
        PaymentProcessor processor = new PaymentProcessor(adapter);
        processor.MakeOrder(50000, "ORDER123");
    }
}

 

설명: 어댑터 패턴은 호환되지 않는 인터페이스를 함께 작동할 수 있게 해줍니다. 위 예시에서는 새로운 결제 시스템 인터페이스를 통해 레거시 결제 시스템을 사용할 수 있도록 어댑터를 만들었습니다.

 

 

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

실생활 시나리오: 카페 음료 주문 시스템

// 기본 인터페이스
public interface IBeverage
{
    string GetDescription();
    decimal GetCost();
}

// 구체적인 컴포넌트
public class Espresso : IBeverage
{
    public string GetDescription()
    {
        return "에스프레소";
    }
    
    public decimal GetCost()
    {
        return 2000m;
    }
}

public class Americano : IBeverage
{
    public string GetDescription()
    {
        return "아메리카노";
    }
    
    public decimal GetCost()
    {
        return 3000m;
    }
}

// 데코레이터 추상 클래스
public abstract class BeverageDecorator : IBeverage
{
    protected IBeverage beverage;
    
    public BeverageDecorator(IBeverage beverage)
    {
        this.beverage = beverage;
    }
    
    // 기본 구현은 위임
    public virtual string GetDescription()
    {
        return beverage.GetDescription();
    }
    
    public virtual decimal GetCost()
    {
        return beverage.GetCost();
    }
}

// 구체적인 데코레이터들
public class MilkDecorator : BeverageDecorator
{
    public MilkDecorator(IBeverage beverage) : base(beverage) { }
    
    public override string GetDescription()
    {
        return $"{beverage.GetDescription()}, 우유 추가";
    }
    
    public override decimal GetCost()
    {
        return beverage.GetCost() + 500m;
    }
}

public class WhippedCreamDecorator : BeverageDecorator
{
    public WhippedCreamDecorator(IBeverage beverage) : base(beverage) { }
    
    public override string GetDescription()
    {
        return $"{beverage.GetDescription()}, 휘핑크림 추가";
    }
    
    public override decimal GetCost()
    {
        return beverage.GetCost() + 700m;
    }
}

public class CaramelDecorator : BeverageDecorator
{
    public CaramelDecorator(IBeverage beverage) : base(beverage) { }
    
    public override string GetDescription()
    {
        return $"{beverage.GetDescription()}, 카라멜 시럽 추가";
    }
    
    public override decimal GetCost()
    {
        return beverage.GetCost() + 600m;
    }
}

// 사용 예시
public class Program
{
    public static void Main()
    {
        // 기본 에스프레소
        IBeverage espresso = new Espresso();
        Console.WriteLine($"{espresso.GetDescription()} - {espresso.GetCost():C}");
        
        // 아메리카노에 우유 추가
        IBeverage latteMacchiato = new MilkDecorator(new Americano());
        Console.WriteLine($"{latteMacchiato.GetDescription()} - {latteMacchiato.GetCost():C}");
        
        // 에스프레소에 우유, 휘핑크림, 카라멜 시럽 추가 (카푸치노)
        IBeverage fancyCoffee = new CaramelDecorator(
                                   new WhippedCreamDecorator(
                                       new MilkDecorator(
                                           new Espresso())));
        
        Console.WriteLine($"{fancyCoffee.GetDescription()} - {fancyCoffee.GetCost():C}");
    }
}

 

설명: 데코레이터 패턴은 객체에 동적으로 새로운 책임을 추가할 수 있게 합니다. 위 예시에서는 기본 음료에 우유, 휘핑크림, 카라멜 등의 부가 요소를 조합하여 새로운 음료를 만들 수 있습니다.

 

 

6. 프록시 패턴 (Proxy Pattern)

실생활 시나리오: 고해상도 이미지 로딩 지연

// 인터페이스
public interface IImage
{
    void Display();
}

// 실제 이미지 - 리소스가 많이 필요한 객체
public class RealImage : IImage
{
    private string filename;
    
    public RealImage(string filename)
    {
        this.filename = filename;
        LoadFromDisk();
    }
    
    private void LoadFromDisk()
    {
        Console.WriteLine($"디스크에서 이미지 로딩 중: {filename}");
        // 큰 이미지 파일 로딩을 시뮬레이션
        Thread.Sleep(1000);
    }
    
    public void Display()
    {
        Console.WriteLine($"이미지 표시 중: {filename}");
    }
}

// 프록시 이미지
public class ProxyImage : IImage
{
    private RealImage realImage;
    private string filename;
    
    public ProxyImage(string filename)
    {
        this.filename = filename;
    }
    
    // 실제로 표시할 때만 RealImage 생성
    public void Display()
    {
        if (realImage == null)
        {
            realImage = new RealImage(filename);
        }
        realImage.Display();
    }
}

// 사용 예시
public class Program
{
    public static void Main()
    {
        // 10개의 이미지를 갤러리에 추가
        List<IImage> gallery = new List<IImage>();
        
        for (int i = 1; i <= 10; i++)
        {
            gallery.Add(new ProxyImage($"image_{i}.jpg"));
        }
        
        Console.WriteLine("갤러리 로딩 완료 (이미지는 아직 로딩되지 않음)");
        
        // 사용자가 3번째 이미지를 클릭
        Console.WriteLine("\n사용자가 3번째 이미지 클릭");
        gallery[2].Display();
        
        // 다시 같은 이미지 클릭 (이미 로딩됨)
        Console.WriteLine("\n사용자가 3번째 이미지 다시 클릭");
        gallery[2].Display();
        
        // 다른 이미지 클릭
        Console.WriteLine("\n사용자가 5번째 이미지 클릭");
        gallery[4].Display();
    }
}

 

설명: 프록시 패턴은 다른 객체에 대한 접근을 제어하는 대리자 객체를 제공합니다. 위 예시에서는 무거운 이미지 객체를 실제로 필요할 때만 로딩하도록 제어합니다.

 

 

7. 컴포지트 패턴 (Composite Pattern)

실생활 시나리오: 파일 시스템 구조

// 컴포넌트 인터페이스
public interface IFileSystemComponent
{
    string Name { get; }
    void Display(int depth = 0);
    long GetSize();
}

// 리프 노드 - 파일
public class File : IFileSystemComponent
{
    public string Name { get; private set; }
    private long size;
    
    public File(string name, long size)
    {
        Name = name;
        this.size = size;
    }
    
    public void Display(int depth = 0)
    {
        Console.WriteLine($"{new string(' ', depth * 2)}파일: {Name} ({FormatSize(size)})");
    }
    
    public long GetSize()
    {
        return size;
    }
    
    private string FormatSize(long bytes)
    {
        if (bytes < 1024) return $"{bytes} B";
        if (bytes < 1024 * 1024) return $"{bytes / 1024.0:F1} KB";
        return $"{bytes / (1024.0 * 1024.0):F1} MB";
    }
}

// 복합체 - 폴더
public class Directory : IFileSystemComponent
{
    public string Name { get; private set; }
    private List<IFileSystemComponent> children = new List<IFileSystemComponent>();
    
    public Directory(string name)
    {
        Name = name;
    }
    
    public void Add(IFileSystemComponent component)
    {
        children.Add(component);
    }
    
    public void Remove(IFileSystemComponent component)
    {
        children.Remove(component);
    }
    
    public void Display(int depth = 0)
    {
        Console.WriteLine($"{new string(' ', depth * 2)}폴더: {Name} ({FormatSize(GetSize())})");
        
        // 자식 표시
        foreach (var component in children)
        {
            component.Display(depth + 1);
        }
    }
    
    public long GetSize()
    {
        // 모든 자식의 크기 합산
        long totalSize = 0;
        foreach (var component in children)
        {
            totalSize += component.GetSize();
        }
        return totalSize;
    }
    
    private string FormatSize(long bytes)
    {
        if (bytes < 1024) return $"{bytes} B";
        if (bytes < 1024 * 1024) return $"{bytes / 1024.0:F1} KB";
        return $"{bytes / (1024.0 * 1024.0):F1} MB";
    }
}

// 사용 예시
public class Program
{
    public static void Main()
    {
        // 루트 폴더 생성
        Directory root = new Directory("C:");
        
        // 문서 폴더 생성
        Directory docs = new Directory("Documents");
        docs.Add(new File("resume.docx", 38400));
        docs.Add(new File("report.pdf", 775000));
        
        // 이미지 폴더 생성
        Directory pictures = new Directory("Pictures");
        pictures.Add(new File("vacation.jpg", 1560000));
        pictures.Add(new File("family.png", 2800000));
        
        // 음악 폴더와 하위 폴더
        Directory music = new Directory("Music");
        Directory kpop = new Directory("K-Pop");
        kpop.Add(new File("song1.mp3", 4500000));
        kpop.Add(new File("song2.mp3", 5200000));
        music.Add(kpop);
        music.Add(new File("classical.mp3", 6100000));
        
        // 루트에 폴더 추가
        root.Add(docs);
        root.Add(pictures);
        root.Add(music);
        
        // 파일 시스템 표시
        root.Display();
        
        // 특정 폴더 크기 확인
        Console.WriteLine($"\n음악 폴더 크기: {music.GetSize() / (1024.0 * 1024.0):F2} MB");
    }
}

 

설명: 컴포지트 패턴은 객체들을 트리 구조로 구성하여 개별 객체와 객체 그룹을 같은 방식으로 다룰 수 있게 합니다. 위 예시에서는 파일과 폴더를 동일한 인터페이스로 처리합니다.

 

 

8. 전략 패턴 (Strategy Pattern)

실생활 시나리오: 온라인 쇼핑몰의 결제 시스템

// 전략 인터페이스
public interface IPaymentStrategy
{
    bool Pay(decimal amount);
    string GetName();
}

// 구체적인 전략 - 신용카드
public class CreditCardPayment : IPaymentStrategy
{
    private string cardNumber;
    private string cvv;
    private string expiryDate;
    private string name;
    
    public CreditCardPayment(string cardNumber, string cvv, string expiryDate, string name)
    {
        this.cardNumber = cardNumber;
        this.cvv = cvv;
        this.expiryDate = expiryDate;
        this.name = name;
    }
    
    public bool Pay(decimal amount)
    {
        // 실제로는 카드사에 결제 요청
        Console.WriteLine($"신용카드로 {amount:C} 결제 처리 중...");
        Console.WriteLine($"카드 번호: {MaskCardNumber(cardNumber)}");
        return true;
    }
    
    public string GetName()
    {
        return "신용카드";
    }
    
    private string MaskCardNumber(string number)
    {
        // 카드 번호 보안 처리
        if (number.Length < 4) return number;
        return new string('*', number.Length - 4) + number.Substring(number.Length - 4);
    }
}

// 구체적인 전략 - 페이팔
public class PaypalPayment : IPaymentStrategy
{
    private string email;
    private string password;
    
    public PaypalPayment(string email, string password)
    {
        this.email = email;
        this.password = password;
    }
    
    public bool Pay(decimal amount)
    {
        // 페이팔 API 호출
        Console.WriteLine($"페이팔로 {amount:C} 결제 처리 중...");
        Console.WriteLine($"이메일: {email}");
        return true;
    }
    
    public string GetName()
    {
        return "페이팔";
    }
}

// 상황 클래스
public class ShoppingCart
{
    private List<string> items = new List<string>();
    private Dictionary<string, decimal> prices = new Dictionary<string, decimal>();
    
    public void AddItem(string item, decimal price)
    {
        items.Add(item);
        prices[item] = price;
        Console.WriteLine($"장바구니에 {item} 추가됨 - {price:C}");
    }
    
    public decimal CalculateTotal()
    {
        decimal sum = 0;
        foreach (var item in items)
        {
            sum += prices[item];
        }
        return sum;
    }
    
    public void Checkout(IPaymentStrategy paymentMethod)
    {
        decimal amount = CalculateTotal();
        Console.WriteLine($"\n총 결제 금액: {amount:C}");
        Console.WriteLine($"결제 방법: {paymentMethod.GetName()}");
        
        bool success = paymentMethod.Pay(amount);
        
        if (success)
        {
            Console.WriteLine("결제 성공! 주문이 처리되었습니다.");
            items.Clear();
        }
        else
        {
            Console.WriteLine("결제 실패! 다시 시도해주세요.");
        }
    }
}

// 사용 예시
public class Program
{
    public static void Main()
    {
        // 장바구니 생성
        ShoppingCart cart = new ShoppingCart();
        
        // 상품 추가
        cart.AddItem("노트북", 1200000);
        cart.AddItem("마우스", 35000);
        cart.AddItem("키보드", 89000);
        
        // 신용카드로 결제
        IPaymentStrategy creditCard = new CreditCardPayment("1234-5678-9012-3456", "123", "12/25", "홍길동");
        cart.Checkout(creditCard);
        
        // 새 장바구니 생성
        cart = new ShoppingCart();
        cart.AddItem("책", 25000);
        cart.AddItem("커피머신", 150000);
        
        // 페이팔로 결제
        IPaymentStrategy paypal = new PaypalPayment("hong@example.com", "password");
        cart.Checkout(paypal);
    }
}

 

 

설명: 전략 패턴은 알고리즘 군을 정의하고 각각 캡슐화하여 교체 가능하게 만듭니다. 위 예시에서는 다양한 결제 방법을 전략으로 구현하여 쉽게 교체할 수 있습니다.

 

 

9. 옵저버 패턴 (Observer Pattern)

 

실생활 시나리오: 주식 시장 모니터링 앱

 

// 구체적인 주제 - 주식 시장
public class StockMarket : IStockMarket
{
    private List<IInvestor> investors = new List<IInvestor>();
    private Dictionary<string, decimal> stocks = new Dictionary<string, decimal>();
    
    public void RegisterObserver(IInvestor investor)
    {
        investors.Add(investor);
    }
    
    public void RemoveObserver(IInvestor investor)
    {
        investors.Remove(investor);
    }
    
    public void NotifyObservers()
    {
        foreach (var stock in stocks)
        {
            foreach (var investor in investors)
            {
                investor.Update(stock.Key, stock.Value);
            }
        }
    }
    
    public void SetStockPrice(string symbol, decimal price)
    {
        Console.WriteLine($"\n주식 시장: {symbol} 가격 변동 -> {price:C}");
        
        // 기존 가격이 있는지 확인
        if (stocks.ContainsKey(symbol))
        {
            decimal oldPrice = stocks[symbol];
            stocks[symbol] = price;
            
            // 가격이 변경된 경우에만 알림
            if (oldPrice != price)
            {
                NotifyObservers();
            }
        }
        else
        {
            // 새 주식 추가
            stocks[symbol] = price;
            NotifyObservers();
        }
    }
}

// 구체적인 옵저버 - 투자자
public class Investor : IInvestor
{
    private string name;
    private Dictionary<string, decimal> portfolio = new Dictionary<string, decimal>();
    
    public Investor(string name)
    {
        this.name = name;
    }
    
    public void AddStock(string symbol, decimal buyPrice)
    {
        portfolio[symbol] = buyPrice;
        Console.WriteLine($"{name}이(가) {symbol} 주식을 {buyPrice:C}에 구매했습니다.");
    }
    
    public void Update(string stockSymbol, decimal currentPrice)
    {
        // 보유 중인 주식인 경우에만 반응
        if (portfolio.ContainsKey(stockSymbol))
        {
            decimal buyPrice = portfolio[stockSymbol];
            decimal profitLoss = currentPrice - buyPrice;
            
            Console.WriteLine($"{name} 알림: {stockSymbol} 현재가 {currentPrice:C}, " +
                             $"매수가 {buyPrice:C}, 손익 {profitLoss:C} " +
                             $"({profitLoss / buyPrice:P2})");
            
            // 투자자의 전략적 결정
            if (profitLoss > 0 && profitLoss / buyPrice > 0.1m)
            {
                Console.WriteLine($"{name}의 결정: {stockSymbol} 주식이 10% 이상 상승했습니다. 일부 매도를 고려하세요!");
            }
            else if (profitLoss < 0 && Math.Abs(profitLoss / buyPrice) > 0.05m)
            {
                Console.WriteLine($"{name}의 결정: {stockSymbol} 주식이 5% 이상 하락했습니다. 추가 매수를 고려하세요!");
            }
        }
    }
}

// 사용 예시
public class Program
{
    public static void Main()
    {
        // 주식 시장 생성
        StockMarket market = new StockMarket();
        
        // 투자자 생성
        Investor investor1 = new Investor("홍길동");
        Investor investor2 = new Investor("김철수");
        Investor investor3 = new Investor("이영희");
        
        // 투자자들을 옵저버로 등록
        market.RegisterObserver(investor1);
        market.RegisterObserver(investor2);
        market.RegisterObserver(investor3);
        
        // 투자자들의 포트폴리오 설정
        investor1.AddStock("SAMSUNG", 70000);
        investor1.AddStock("NAVER", 350000);
        
        investor2.AddStock("SAMSUNG", 65000);
        investor2.AddStock("KAKAO", 120000);
        
        investor3.AddStock("NAVER", 340000);
        investor3.AddStock("KAKAO", 125000);
        
        Console.WriteLine("\n=== 주식 시장 모니터링 시작 ===");
        
        // 주식 가격 변동 시뮬레이션
        market.SetStockPrice("SAMSUNG", 75000);
        market.SetStockPrice("NAVER", 330000);
        market.SetStockPrice("KAKAO", 135000);
        
        // 이영희 투자자 제거
        market.RemoveObserver(investor3);
        Console.WriteLine("\n이영희님이 알림 서비스를 해지했습니다.");
        
        // 다시 가격 변동
        market.SetStockPrice("SAMSUNG", 78000);
        market.SetStockPrice("KAKAO", 115000);
    }
}

 

설명: 옵저버 패턴은 객체 간의 일대다 종속성을 정의하여 한 객체의 상태가 변경되면 모든 종속 객체에게 자동으로 알림이 갑니다. 위 예시에서는 주식 시장(Subject)이 가격 변동 시 모든 투자자(Observer)에게 알림을 보내는 방식으로 구현했습니다.

 

 

10. 커맨드 패턴 (Command Pattern)

실생활 시나리오: 스마트홈 리모컨 시스템

// 커맨드 인터페이스
public interface ICommand
{
    void Execute();
    void Undo();
}

// 수신자(Receiver) 클래스들
public class Light
{
    private string location;
    
    public Light(string location)
    {
        this.location = location;
    }
    
    public void TurnOn()
    {
        Console.WriteLine($"{location} 조명이 켜졌습니다.");
    }
    
    public void TurnOff()
    {
        Console.WriteLine($"{location} 조명이 꺼졌습니다.");
    }
}

public class Television
{
    private int volume = 10;
    private int channel = 1;
    
    public void TurnOn()
    {
        Console.WriteLine("TV가 켜졌습니다.");
        Console.WriteLine($"볼륨: {volume}, 채널: {channel}");
    }
    
    public void TurnOff()
    {
        Console.WriteLine("TV가 꺼졌습니다.");
    }
    
    public void SetVolume(int volume)
    {
        this.volume = volume;
        Console.WriteLine($"TV 볼륨이 {volume}으로 설정되었습니다.");
    }
    
    public void SetChannel(int channel)
    {
        this.channel = channel;
        Console.WriteLine($"TV 채널이 {channel}번으로 변경되었습니다.");
    }
}

public class Thermostat
{
    private int temperature = 22;
    
    public void SetTemperature(int temperature)
    {
        this.temperature = temperature;
        Console.WriteLine($"온도가 {temperature}°C로 설정되었습니다.");
    }
    
    public int GetTemperature()
    {
        return temperature;
    }
}

// 구체적인 커맨드 클래스들
public class LightOnCommand : ICommand
{
    private Light light;
    
    public LightOnCommand(Light light)
    {
        this.light = light;
    }
    
    public void Execute()
    {
        light.TurnOn();
    }
    
    public void Undo()
    {
        light.TurnOff();
    }
}

public class LightOffCommand : ICommand
{
    private Light light;
    
    public LightOffCommand(Light light)
    {
        this.light = light;
    }
    
    public void Execute()
    {
        light.TurnOff();
    }
    
    public void Undo()
    {
        light.TurnOn();
    }
}

public class TVOnCommand : ICommand
{
    private Television tv;
    
    public TVOnCommand(Television tv)
    {
        this.tv = tv;
    }
    
    public void Execute()
    {
        tv.TurnOn();
    }
    
    public void Undo()
    {
        tv.TurnOff();
    }
}

public class TVOffCommand : ICommand
{
    private Television tv;
    
    public TVOffCommand(Television tv)
    {
        this.tv = tv;
    }
    
    public void Execute()
    {
        tv.TurnOff();
    }
    
    public void Undo()
    {
        tv.TurnOn();
    }
}

public class ThermostatSetCommand : ICommand
{
    private Thermostat thermostat;
    private int temperature;
    private int previousTemperature;
    
    public ThermostatSetCommand(Thermostat thermostat, int temperature)
    {
        this.thermostat = thermostat;
        this.temperature = temperature;
    }
    
    public void Execute()
    {
        previousTemperature = thermostat.GetTemperature();
        thermostat.SetTemperature(temperature);
    }
    
    public void Undo()
    {
        thermostat.SetTemperature(previousTemperature);
    }
}

// 호출자(Invoker) 클래스
public class RemoteControl
{
    private ICommand[] onCommands;
    private ICommand[] offCommands;
    private Stack<ICommand> commandHistory = new Stack<ICommand>();
    
    public RemoteControl(int slots)
    {
        onCommands = new ICommand[slots];
        offCommands = new ICommand[slots];
        
        // 비어있는 커맨드로 초기화
        ICommand noCommand = new NoCommand();
        for (int i = 0; i < slots; i++)
        {
            onCommands[i] = noCommand;
            offCommands[i] = noCommand;
        }
    }
    
    public void SetCommand(int slot, ICommand onCommand, ICommand offCommand)
    {
        onCommands[slot] = onCommand;
        offCommands[slot] = offCommand;
    }
    
    public void PressOnButton(int slot)
    {
        onCommands[slot].Execute();
        commandHistory.Push(onCommands[slot]);
    }
    
    public void PressOffButton(int slot)
    {
        offCommands[slot].Execute();
        commandHistory.Push(offCommands[slot]);
    }
    
    public void PressUndoButton()
    {
        if (commandHistory.Count > 0)
        {
            ICommand command = commandHistory.Pop();
            command.Undo();
        }
        else
        {
            Console.WriteLine("실행 취소할 명령이 없습니다.");
        }
    }
}

// 빈 커맨드 (Null Object 패턴)
public class NoCommand : ICommand
{
    public void Execute() { }
    public void Undo() { }
}

// 사용 예시
public class Program
{
    public static void Main()
    {
        // 리모컨 초기화 (3개 슬롯)
        RemoteControl remote = new RemoteControl(3);
        
        // 디바이스 생성
        Light livingRoomLight = new Light("거실");
        Television tv = new Television();
        Thermostat thermostat = new Thermostat();
        
        // 커맨드 생성
        LightOnCommand livingRoomLightOn = new LightOnCommand(livingRoomLight);
        LightOffCommand livingRoomLightOff = new LightOffCommand(livingRoomLight);
        
        TVOnCommand tvOn = new TVOnCommand(tv);
        TVOffCommand tvOff = new TVOffCommand(tv);
        
        ThermostatSetCommand thermostatHigh = new ThermostatSetCommand(thermostat, 25);
        ThermostatSetCommand thermostatLow = new ThermostatSetCommand(thermostat, 18);
        
        // 리모컨에 커맨드 설정
        remote.SetCommand(0, livingRoomLightOn, livingRoomLightOff);
        remote.SetCommand(1, tvOn, tvOff);
        remote.SetCommand(2, thermostatHigh, thermostatLow);
        
        // 리모컨 사용
        Console.WriteLine("=== 스마트홈 리모컨 테스트 ===\n");
        
        Console.WriteLine("거실 조명 켜기 버튼 누름:");
        remote.PressOnButton(0);
        
        Console.WriteLine("\nTV 켜기 버튼 누름:");
        remote.PressOnButton(1);
        
        Console.WriteLine("\n온도 높이기 버튼 누름:");
        remote.PressOnButton(2);
        
        Console.WriteLine("\n실행 취소 버튼 누름:");
        remote.PressUndoButton();
        
        Console.WriteLine("\nTV 끄기 버튼 누름:");
        remote.PressOffButton(1);
        
        Console.WriteLine("\n거실 조명 끄기 버튼 누름:");
        remote.PressOffButton(0);
    }
}

 

설명: 커맨드 패턴은 요청을 객체로 캡슐화하여 다양한 요청을 매개변수화하고, 요청을 큐에 저장하거나 로그에 기록하고, 취소 가능한 작업을 지원합니다. 위 예시에서는 스마트홈 리모컨이 다양한 가전제품을 제어하는 명령을 실행하고 되돌릴 수 있게 구현했습니다.

 

정리

이미지에 보이는 9가지 디자인 패턴은 객체지향 프로그래밍에서 자주 사용되는 중요한 패턴들입니다:

  1. 생성 패턴(Creational Patterns)
    • 팩토리 패턴: 객체 생성 과정을 캡슐화
    • 싱글톤 패턴: 클래스의 인스턴스를 하나만 생성
    • 빌더 패턴: 복잡한 객체의 생성 과정을 단계별로 처리
  2. 구조 패턴(Structural Patterns)
    • 어댑터 패턴: 호환되지 않는 인터페이스 연결
    • 데코레이터 패턴: 객체에 동적으로 기능 추가
    • 프록시 패턴: 다른 객체에 대한 접근 제어
    • 컴포지트 패턴: 객체들을 트리 구조로 구성하여 동일하게 취급
  3. 행동 패턴(Behavioral Patterns)
    • 전략 패턴: 알고리즘을 캡슐화하여 교체 가능하게 함
    • 옵저버 패턴: 객체 상태 변화를 다른 객체에 자동 알림
    • 커맨드 패턴: 요청을 객체로 캡슐화하여 매개변수화 및 취소 가능하게 함

 

 

이러한 디자인 패턴들은 코드의 재사용성, 유지보수성, 확장성을 높이는 데 큰 도움이 됩니다. 모든 패턴을 다 외우려고 하기보다는 문제 상황에 맞는 패턴을 찾아 적용하는 연습을 하는 것이 효과적입니다.

 

 

https://x.com/alexxubyte/status/1915433600096624731/photo/1

 

X의 Alex Xu님(@alexxubyte)

9 OOP Design Patterns You Must Know

x.com