솔리드 디자인 원칙: 만지면 깨지는 코드 작성 중지

작성자

카테고리:

← 피드로
DEV Community · Madhukar Vissapragada · 2026-07-22 개발(SW)

What is SOLID?

SOLID is a set of software design guidelines — not hard rules, but principles that guide how we organise our code.

The goal is simple: as your codebase grows and your team scales, things should get easier to change, not harder. SOLID is what makes that possible.

Five principles. One goal. Let’s walk through each one with a real, working example.

The Example

We’re building an order processing system for a ticket marketplace. When a customer buys a ticket, the system needs to:

  1. Process the payment
  2. Save the order to the database
  3. Send a confirmation to the buyer
  4. Generate an invoice

There are three order types — Standard, VIP (with SMS notification), and Resale (with seller notification).

We’ll start with the naive implementation and fix it one principle at a time.

The Naive Version

One class. Everything inside it. No abstractions, no separation.

import enum
import uuid
from dataclasses import dataclass

class OrderType(enum.Enum):
    STANDARD = 1
    VIP = 2
    RESALE = 3

@dataclass
class Order:
    event_name: str
    start_seat: str
    end_seat: str
    order_type: int
    customer_id: uuid.UUID
    seller_id: uuid.UUID
    seller_email: str
    order_id: uuid.UUID = uuid.uuid4()


class OrderProcessingSystem:
    def save_order(self, order: Order):
        print(f"Saving order {order.order_id} to database")

    def process_payment(self, order: Order):
        print(f"Processing payment for order {order.order_id}")

    def send_confirmation(self, order: Order):
        print(f"Sending confirmation for order {order.order_id}")

    def generate_invoice(self, order: Order):
        print(f"Generating invoice for order {order.order_id}")

    def process(self, order: Order):
        self.process_payment(order)
        self.save_order(order)
        self.send_confirmation(order)
        self.generate_invoice(order)

Enter fullscreen mode Exit fullscreen mode

This works. But as requirements grow, this class becomes a nightmare to maintain. Let’s fix it, one principle at a time.

S — Single Responsibility Principle

A class, function, or method should have one and only one reason to change.

OrderProcessingSystem has four responsibilities — payment, persistence, confirmation, and invoice generation. That’s four reasons to change. If the invoice format changes, you touch this class. If the payment gateway changes, you touch this class. If the email template changes, you touch this class.

The fix — split into focused classes:

import enum
import uuid
from dataclasses import dataclass

class OrderType(enum.Enum):
    STANDARD = 1
    VIP = 2
    RESALE = 3

@dataclass
class Order:
    event_name: str
    start_seat: str
    end_seat: str
    order_type: int
    customer_id: uuid.UUID
    seller_id: uuid.UUID
    seller_email: str
    order_id: uuid.UUID = uuid.uuid4()


class InventoryManager:
    def save_order(self, order: Order):
        print(f"Save order {order.order_id} for customer {order.customer_id}")


class PaymentManager:
    def process_payment(self, order: Order):
        print(f"Process payment for order {order.order_id}")


class InvoiceGenerator:
    def generate_invoice(self, order: Order):
        print(f"Generate invoice for order {order.order_id}")


class TicketConfirmation:
    def send_confirmation(self, order: Order):
        print(f"Send confirmation for order {order.order_id} to customer {order.customer_id}")


class OrderProcessor:
    def process(self, order: Order):
        PaymentManager().process_payment(order)
        InventoryManager().save_order(order)
        TicketConfirmation().send_confirmation(order)
        InvoiceGenerator().generate_invoice(order)

Enter fullscreen mode Exit fullscreen mode

Now each class has one responsibility and one reason to change. OrderProcessor is a clean orchestrator — it coordinates the flow but holds no logic itself.

O — Open/Closed Principle

A class should be open for extension but closed for modification.

Our three order types have different flows:

  • Standard — payment, save, confirmation, invoice
  • VIP — all of the above + SMS notification
  • Resale — all of the above + notify the seller

The naive fix is if/else blocks inside OrderProcessor.process():

def process(self, order: Order):
    if order.order_type == 1:
        # standard flow
    elif order.order_type == 2:
        # VIP flow
    elif order.order_type == 3:
        # resale flow

Enter fullscreen mode Exit fullscreen mode

Every new order type means opening OrderProcessor and adding another elif. That’s an OCP violation — you’re modifying existing, working code to add new behaviour.

The fix — an abstract base class and a type map:

from abc import ABC, abstractmethod

class OrderCategorySchema(ABC):
    @abstractmethod
    def payment_processor(self, order: Order): pass

    @abstractmethod
    def save_order(self, order: Order): pass

    @abstractmethod
    def send_confirmation(self, order: Order): pass

    @abstractmethod
    def generate_invoice(self, order: Order): pass

    def process(self, order: Order):
        self.payment_processor(order)
        self.save_order(order)
        self.send_confirmation(order)
        self.generate_invoice(order)


class StandardOrder(OrderCategorySchema):
    def payment_processor(self, order): PaymentManager().process_payment(order)
    def save_order(self, order): InventoryManager().save_order(order)
    def send_confirmation(self, order): TicketConfirmation().send_confirmation(order)
    def generate_invoice(self, order): InvoiceGenerator().generate_invoice(order)


class VipOrder(OrderCategorySchema):
    def payment_processor(self, order): PaymentManager().process_payment(order)
    def save_order(self, order): InventoryManager().save_order(order)
    def send_confirmation(self, order): TicketConfirmation().send_confirmation(order)
    def generate_invoice(self, order): InvoiceGenerator().generate_invoice(order)

    def process(self, order: Order):
        self.payment_processor(order)
        self.save_order(order)
        self.send_confirmation(order)
        print(f"Sending SMS to customer {order.customer_id}")
        self.generate_invoice(order)


class ResaleOrder(OrderCategorySchema):
    def payment_processor(self, order): PaymentManager().process_payment(order)
    def save_order(self, order): InventoryManager().save_order(order)
    def send_confirmation(self, order): TicketConfirmation().send_confirmation(order)
    def generate_invoice(self, order): InvoiceGenerator().generate_invoice(order)

    def process(self, order: Order):
        self.payment_processor(order)
        self.save_order(order)
        self.send_confirmation(order)
        print(f"Notifying seller {order.seller_id} at {order.seller_email}")
        self.generate_invoice(order)


type_map = {
    1: StandardOrder,
    2: VipOrder,
    3: ResaleOrder
}


class OrderProcessor:
    def process(self, order: Order):
        order_class = type_map[order.order_type]
        order_class().process(order)

Enter fullscreen mode Exit fullscreen mode

Adding a new order type tomorrow is just a new class and one entry in type_map. OrderProcessor never changes.

L — Liskov Substitution Principle

A child class should be fully substitutable for its parent. Wherever you use the parent, the child must work correctly — no surprises, no broken contracts.

With our current implementation, OrderProcessor calls process() on whatever OrderCategorySchema subclass it receives. Let’s verify all three are truly substitutable:

schemas = [StandardOrder(), VipOrder(), ResaleOrder()]
order = Order('Event', 'A1', 'A2', 1, uuid.uuid4(), uuid.uuid4(), '[email protected]')

for schema in schemas:
    schema.process(order)  # all three work correctly — LSP satisfied ✅

Enter fullscreen mode Exit fullscreen mode

LSP is violated when a subclass throws NotImplementedError, silently does nothing, or changes the behaviour a caller depends on. The classic example is a Penguin inheriting from a Bird class that has a fly() method. Penguins can’t fly — so Penguin.fly() either crashes or does nothing. The contract is broken.

In our case, every order type honours the process() contract. LSP satisfied.

I — Interface Segregation Principle

An interface should only contain methods that are genuinely related to each other.

Imagine a new order type arrives — DigitalTicketOrder. It’s an online-only ticket. No physical seat, no PDF invoice needed.

With our current OrderCategorySchema, DigitalTicketOrder is forced to implement generate_invoice() even though it has no business generating one. That’s an ISP violation — the interface has a method that not all implementors need.

The fix — split into focused interfaces:

from abc import ABC, abstractmethod

class Notificator(ABC):
    @abstractmethod
    def notify(self, order: Order): pass

class SMSNotification(Notificator):
    def notify(self, order: Order):
        print(f"Send SMS to customer {order.customer_id}")

class NotifySeller(Notificator):
    def notify(self, order: Order):
        print(f"Notify seller {order.seller_id} at {order.seller_email}")


class Invoiceable(ABC):
    @abstractmethod
    def generate_invoice(self, order: Order): pass

class PDFInvoiceGenerator(Invoiceable):
    def generate_invoice(self, order: Order):
        print(f"Generate PDF invoice for order {order.order_id}")


class TicketConfirmation(ABC):
    @abstractmethod
    def send_confirmation(self, order: Order): pass

class EmailTicketConfirmation(TicketConfirmation):
    def send_confirmation(self, order: Order):
        print(f"Email confirmation for order {order.order_id} to customer {order.customer_id}")

class WhatsappTicketConfirmation(TicketConfirmation):
    def send_confirmation(self, order: Order):
        print(f"WhatsApp confirmation for order {order.order_id} to customer {order.customer_id}")


class OrderCategorySchema(ABC):
    @abstractmethod
    def save_order(self, order: Order): pass

    @abstractmethod
    def process(self, order: Order): pass


class StandardOrder(OrderCategorySchema, PDFInvoiceGenerator):
    def save_order(self, order): InventoryManager().save_order(order)
    def process(self, order):
        PaymentManager().process_payment(order)
        self.save_order(order)
        EmailTicketConfirmation().send_confirmation(order)
        self.generate_invoice(order)


class VipOrder(OrderCategorySchema, PDFInvoiceGenerator, SMSNotification):
    def save_order(self, order): InventoryManager().save_order(order)
    def process(self, order):
        PaymentManager().process_payment(order)
        self.save_order(order)
        EmailTicketConfirmation().send_confirmation(order)
        self.notify(order)
        self.generate_invoice(order)


class ResaleOrder(OrderCategorySchema, NotifySeller, PDFInvoiceGenerator):
    def save_order(self, order): InventoryManager().save_order(order)
    def process(self, order):
        PaymentManager().process_payment(order)
        self.save_order(order)
        WhatsappTicketConfirmation().send_confirmation(order)
        self.notify(order)
        self.generate_invoice(order)


# DigitalTicketOrder — no invoice needed, doesn't implement Invoiceable
class DigitalTicketOrder(OrderCategorySchema):
    def save_order(self, order): InventoryManager().save_order(order)
    def process(self, order):
        PaymentManager().process_payment(order)
        self.save_order(order)
        EmailTicketConfirmation().send_confirmation(order)
        # no invoice — and nothing forces it to generate one ✅

Enter fullscreen mode Exit fullscreen mode

Each interface is focused and justified. Classes implement only what they need.

D — Dependency Inversion Principle

Classes should not depend directly on each other — they should both depend on an abstraction.

Look at what’s still hardcoded inside the order classes:

PaymentManager().process_payment(order)       # concrete class
EmailTicketConfirmation().send_confirmation() # concrete class

Enter fullscreen mode Exit fullscreen mode

If the payment gateway changes from Stripe to Razorpay, you touch every order class. If confirmation switches from email to WhatsApp, same story. These are concrete dependencies baked into business logic.

The fix — define abstractions and inject from outside:

from abc import ABC, abstractmethod
import enum
import uuid
from dataclasses import dataclass


class OrderType(enum.Enum):
    STANDARD = 1
    VIP = 2
    RESALE = 3


@dataclass
class Order:
    event_name: str
    start_seat: str
    end_seat: str
    order_type: int
    customer_id: uuid.UUID
    seller_id: uuid.UUID
    seller_email: str
    order_id: uuid.UUID = uuid.uuid4()


# ── Notification abstractions ─────────────────────────────────

class Notificator(ABC):
    @abstractmethod
    def notify(self, order: Order): pass

class SMSNotification(Notificator):
    def notify(self, order: Order):
        print(f"Send SMS to customer {order.customer_id}")

class NotifySeller(Notificator):
    def notify(self, order: Order):
        print(f"Notify seller {order.seller_id} at {order.seller_email}")


# ── Payment abstraction ───────────────────────────────────────

class PaymentService(ABC):
    @abstractmethod
    def process_payment(self, order: Order): pass

class StripeService(PaymentService):
    def process_payment(self, order: Order):
        print(f"Stripe: processing payment for order {order.order_id}")

class RazorPayService(PaymentService):
    def process_payment(self, order: Order):
        print(f"RazorPay: processing payment for order {order.order_id}")


# ── Inventory ─────────────────────────────────────────────────

class InventoryManager(ABC):
    @abstractmethod
    def save_order(self, order: Order): pass
    @abstractmethod
    def update_order(self, order: Order): pass
    @abstractmethod
    def delete_order(self, order: Order): pass
    @abstractmethod
    def create_order(self, order: Order): pass

class OrderManager(InventoryManager):
    def save_order(self, order: Order):
        print(f"Save order {order.order_id} for customer {order.customer_id}")
    def update_order(self, order: Order):
        print(f"Update order {order.order_id}")
    def delete_order(self, order: Order):
        print(f"Delete order {order.order_id}")
    def create_order(self, order: Order):
        print(f"Create order {order.order_id}")


# ── Invoice abstraction ───────────────────────────────────────

class Invoiceable(ABC):
    @abstractmethod
    def generate_invoice(self, order: Order): pass

class PDFInvoiceGenerator(Invoiceable):
    def generate_invoice(self, order: Order):
        print(f"Generate PDF invoice for order {order.order_id}")


# ── Confirmation abstraction ──────────────────────────────────

class TicketConfirmation(ABC):
    @abstractmethod
    def send_confirmation(self, order: Order): pass

class EmailTicketConfirmation(TicketConfirmation):
    def send_confirmation(self, order: Order):
        print(f"Email confirmation for order {order.order_id} to {order.customer_id}")

class WhatsappTicketConfirmation(TicketConfirmation):
    def send_confirmation(self, order: Order):
        print(f"WhatsApp confirmation for order {order.order_id} to {order.customer_id}")


# ── Order base ────────────────────────────────────────────────

class OrderCategorySchema(ABC):
    def __init__(self, payment_gateway: PaymentService, confirmation_manager: TicketConfirmation):
        self.payment_gateway = payment_gateway
        self.confirmation_manager = confirmation_manager

    @abstractmethod
    def save_order(self, order: Order): pass

    @abstractmethod
    def process(self, order: Order): pass


# ── Concrete order types ──────────────────────────────────────

class StandardOrder(OrderCategorySchema, PDFInvoiceGenerator):
    def save_order(self, order: Order):
        OrderManager().save_order(order)

    def process(self, order: Order):
        self.payment_gateway.process_payment(order)
        self.save_order(order)
        self.confirmation_manager.send_confirmation(order)
        self.generate_invoice(order)


class VipOrder(OrderCategorySchema, PDFInvoiceGenerator, SMSNotification):
    def save_order(self, order: Order):
        OrderManager().save_order(order)

    def process(self, order: Order):
        self.payment_gateway.process_payment(order)
        self.save_order(order)
        self.confirmation_manager.send_confirmation(order)
        self.notify(order)
        self.generate_invoice(order)


class ResaleOrder(OrderCategorySchema, NotifySeller, PDFInvoiceGenerator):
    def save_order(self, order: Order):
        OrderManager().save_order(order)

    def process(self, order: Order):
        self.payment_gateway.process_payment(order)
        self.save_order(order)
        self.confirmation_manager.send_confirmation(order)
        self.notify(order)
        self.generate_invoice(order)


# ── Wiring ────────────────────────────────────────────────────

type_map = {
    1: StandardOrder,
    2: VipOrder,
    3: ResaleOrder
}


class OrderProcessor:
    def process(self, order: Order):
        order_class = type_map[order.order_type]
        payment_service = RazorPayService()
        confirmation_manager = WhatsappTicketConfirmation()
        order_class(payment_service, confirmation_manager).process(order)

Enter fullscreen mode Exit fullscreen mode

Switching from RazorPay to Stripe, or from WhatsApp to email confirmation, is two lines in OrderProcessor. Zero changes to any order class.

# Switch payment and confirmation — zero order class changes
payment_service = StripeService()
confirmation_manager = EmailTicketConfirmation()

Enter fullscreen mode Exit fullscreen mode

OrderCategorySchema depends on PaymentService and TicketConfirmation abstractions — not on RazorPayService or WhatsappTicketConfirmation directly. DIP satisfied.

How the Principles Connect

Each principle solved a problem the previous one left behind:

SRP gave us small, focused classes. When a class does one thing, it becomes naturally easy to extend without modification — which is exactly what OCP requires.

OCP told us to extend via abstractions. But once you have inheritance hierarchies, LSP steps in and says: make sure your subclasses honour the parent’s contract.

LSP and ISP surface the same smell — a class implementing something it has no business implementing. LSP says fix your hierarchy. ISP says fix your interface. Two sides of the same coin.

DIP is the glue that ties everything together. SRP, OCP, LSP, and ISP all tell you to create abstractions. DIP says: now depend on those abstractions, not the concretions. This is what makes everything testable, swappable, and decoupled.

SRP creates focused classes → OCP makes them extendable → LSP keeps hierarchies honest → ISP keeps interfaces clean → DIP wires it all together.

The Tradeoffs

SOLID is a compass, not a religion. Over-applying it leads to premature abstraction — which is its own problem.

Principle Benefit Cost SRP Focused, easy to change in isolation More classes, more files, more navigation OCP Add features without breaking existing code More abstractions upfront — overkill if variants never appear LSP Subclasses are truly interchangeable Sometimes forces flat hierarchies or abandoning inheritance ISP Classes only know what they need Interface explosion — dozens of tiny interfaces DIP Testable, swappable, decoupled Wiring complexity — need a composition root

원문에서 계속 ↗

코멘트

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다