소프트웨어 설계에서 중요한 SOLID 원칙을 FastAPI와 Python을 활용해 어떻게 적용할 수 있는지 알아보겠습니다. 🚀
SOLID는 로버트 C. 마틴(일명 "Uncle Bob")이 제안한 5가지 설계 원칙으로, 코드를 더 깔끔하고 유지보수하기 쉽도록 도와줍니다. 특히 객체 지향 프로그래밍과 API 개발에서 빛을 발하죠. ✨
이 글에서는 온라인 서점의 도서 재고 관리 API를 예제로 삼아 각 원칙을 하나씩 살펴보겠습니다. 이미 FastAPI로 API를 만들어 본 경험이 있는 분들을 위해, 기초적인 설명은 건너뛰고 실전 적용에 집중하겠습니다. 💡 그럼 시작해볼까요? 💨
📚 예제 시나리오: 온라인 서점 재고 관리 시스템
우리가 만들 API는 온라인 서점에서 도서를 관리하는 시스템입니다. 이 API는 다음 기능을 제공해야 합니다:
✅ 새 도서 추가 ✅ 도서 정보 수정 ✅ 도서 정보 조회 ✅ 도서 삭제
이제 이 시나리오를 바탕으로 SOLID 원칙을 하나씩 적용하며 코드를 설계해 보겠습니다. 🏗️
1️⃣ 단일 책임 원칙 (SRP): 한 가지 일만 잘하자 🎯
원칙: "클래스는 단 하나의 책임만 가져야 하며, 변경 이유도 하나여야 한다."
쉽게 말해, 한 클래스가 너무 많은 일을 하면 안 됩니다! ❌ 예를 들어, 도서 데이터를 정의하는 일과 도서를 관리하는 로직을 한꺼번에 처리하면 코드가 복잡해지겠죠? 🤯 그래서 이 둘을 분리해 보겠습니다.
✅ 적용 예시
from pydantic import BaseModel
# 📖 도서 데이터 모델
class Book(BaseModel):
book_id: int
title: str
price: float
stock: int
# 📦 도서 관리 로직
class BookManager:
def __init__(self):
self.book_list = []
def register_book(self, book: Book):
self.book_list.append(book)
def edit_book(self, book_id: int, new_book: Book):
for idx, item in enumerate(self.book_list):
if item.book_id == book_id:
self.book_list[idx] = new_book
return True
return False
✅ Book은 데이터 구조만 정의하고, BookManager는 도서 관리 로직만 담당합니다. 🎯 ✅ 이렇게 하면 코드가 훨씬 깔끔해지고, 유지보수도 쉬워집니다! 🛠️
반응형
2️⃣ 개방/폐쇄 원칙 (OCP): 확장은 OK, 수정은 NO 🚪
원칙: "클래스는 확장에 열려 있고 수정에는 닫혀 있어야 한다."
기존 코드를 수정하지 않고도 새로운 기능을 추가할 수 있어야 합니다. 🛠️ 예를 들어, 도서에 할인 기능을 추가하고 싶다면, BookManager를 직접 고치는 대신 확장하는 방식으로 해결할 수 있습니다. ✅
✅ 적용 예시
class SaleBookManager(BookManager):
def apply_sale(self, book_id: int, sale_rate: float):
book = self.find_book(book_id)
if book:
book.price *= (1 - sale_rate / 100)
return True
return False
✅ BookManager를 수정하지 않고 SaleBookManager에서 기능을 추가! 🎉 ✅ 기존 코드를 건드리지 않고도 기능을 확장했으니 OCP를 잘 지켰다! ✅
3️⃣ 리스코프 치환 원칙 (LSP): 부모와 자식의 조화 🤝
원칙: "하위 클래스는 상위 클래스로 대체될 수 있어야 하며, 프로그램 동작이 바뀌면 안 된다."
쉽게 말해, SaleBookManager가 BookManager를 완벽하게 대체할 수 있어야 합니다. 🔄
✅ 적용 예시
def handle_book(manager: BookManager):
book = Book(book_id=1, title="Python Guide", price=30000, stock=5)
manager.register_book(book)
return manager.find_book(1)
# 두 클래스 모두 문제없이 동작 ✅
basic_manager = BookManager()
sale_manager = SaleBookManager()
print(handle_book(basic_manager)) # 정상 출력 ✅
print(handle_book(sale_manager)) # 정상 출력 ✅
✅ BookManager를 기대하는 곳에서 SaleBookManager도 문제없이 동작 💯 ✅ LSP를 충족했으니 안전한 상속을 적용한 셈!
4️⃣ 인터페이스 분리 원칙 (ISP): 필요한 것만 가져가자 ✂️
원칙: "클라이언트가 사용하지 않는 인터페이스에 강제로 의존하지 않아야 한다."
큰 인터페이스를 만들기보다, 작고 명확한 인터페이스로 나누는 것이 좋습니다. 🛠️
✅ 적용 예시
from abc import ABC, abstractmethod
class BookRecorder(ABC):
@abstractmethod
def record(self, book: Book) -> Book:
pass
✅ 이렇게 필요한 기능만 분리해서 구현하면 불필요한 코드 작성을 줄일 수 있습니다! 🚀
5️⃣ 의존성 역전 원칙 (DIP): 추상화에 의존하자 🏗️
원칙: "고수준 모듈은 저수준 모듈에 의존하면 안 된다. 둘 다 추상화에 의존해야 한다."
직접 구현(예: 데이터베이스)에 의존하지 않고, **인터페이스(추상화)**에 의존하도록 설계합니다. 🎯
✅ 적용 예시
class BookManager:
def __init__(self, storage: BookStorage):
self.storage = storage
✅ BookManager는 BookStorage라는 추상 인터페이스에 의존! ✅ ✅ PostgreSQL, MongoDB 등 다양한 저장소로 쉽게 교체 가능! 🔄
🎯 마무리: SOLID로 더 나은 코드를!
FastAPI와 SOLID 원칙을 결합하면 유지보수와 확장이 쉬운 API를 만들 수 있습니다. 💡