대규모 파이썬 프로젝트를 시작하면 가장 먼저 눈에 띄는 것 중 하나는 다음과 같습니다. 해당 코드는 이해하고, 테스트하고, 확장하기가 어려워집니다. 몇 가지 기본적인 설계 규칙을 따르지 않으면 문제가 발생합니다. 바로 이럴 때 유명한 SOLID 원칙이 빛을 발합니다. SOLID 원칙은 팀의 업무를 훨씬 수월하게 만들어주는 모범 사례 모음입니다.
이러한 원칙들은 다음 분야에서 유래되었습니다. 고전적인 객체 지향 프로그래밍(Java, C++, C# 등)하지만 클래스와 객체를 어느 정도 진지하게 사용한다면 파이썬과 완벽하게 어울립니다. 클래스와 객체가 무엇인지, 어디에서 유래했는지, 왜 중요한지, 그리고 무엇보다도 어떻게 사용하는지 자세히 살펴보겠습니다. SOLID 원칙을 파이썬에서 명확한 예시와 함께 적용해 보세요. 코드를 더욱 유지보수하기 쉽고, 확장 가능하며, 사용하기 편리하게 만들기 위해서입니다.
SOLID란 무엇이며, 그것은 어디에서 유래하는 걸까요?
용어 SOLID는 마이클 페더스가 널리 알린 약어입니다. 로버트 C. 마틴, 일명 엉클 밥이 처음 제안한 다섯 가지 설계 원칙을 그룹화한 것입니다. 애자일 선언문의 서명자 중 한 명인 이 미국 소프트웨어 엔지니어는 90년대 중반에 "객체 지향 설계의 원칙(The Principles of OOD)"이라는 논문을 발표했고, 이후 "설계 원칙과 디자인 패턴(Design Principles and Design Patterns)"을 통해 현대 객체 지향 설계의 많은 토대를 마련했습니다.
시간이 흐르면서 다른 저자들도 등장했습니다. 바바라 리스코프와 베르트랑 메이어 그들은 또한 이러한 원칙들에 통합된 아이디어들을 제공했습니다. 마이클 페더스는 단순히 이 원칙들의 이니셜을 재배열하여 SOLID라는 단어를 만드는 (매우 예리한) 아이디어를 냈고, 이것이 개발 커뮤니티에서 순식간에 퍼져나가는 데 도움이 되었습니다.
SOLID라는 다섯 글자는 파이썬에도 적용 가능한 객체 지향 설계 원칙을 나타냅니다.
- S – 단일 책임 원칙 (단일 책임 원칙)
- O – 개방/폐쇄 원칙 (개방/폐쇄 원칙)
- L – 리스코프 치환 원리 (리스코프 치환 원리)
- I – 인터페이스 분리 원칙 (인터페이스 분리 원리)
- D – 의존성 역전 원리 (의존성 역전의 원리)
요컨대, 이 다섯 가지 원칙을 함께 사용하면, 이러한 도구들은 유연하고 테스트하기 쉽고 유지보수하기 편리한 소프트웨어를 작성하는 데 도움을 줍니다.이는 더 빠른 배포, 원인 불명의 버그 감소, 코드 재사용성 향상, 그리고 프로젝트가 몇 년 동안 운영된 후에도 골칫거리가 줄어드는 결과로 이어집니다.
파이썬에서 SOLID 원칙은 무엇에 사용되나요?
파이썬에서 SOLID 원칙을 적용하는 것은 단순히 학문적인 연습에 그치는 것이 아니라, 팀의 일상 업무에 직접적인 영향을 미칩니다. 이러한 원칙을 준수하면, 스파게티 코드를 줄이고, 코드 스멜을 감소시키며, 코드베이스에서 "썩은 냄새"가 나는 것을 방지합니다.유명한 비유를 빌리자면, "냄새가 나쁘면 설계가 잘못된 것이다." 윈도우에서 많은 개발자들은 다음과 같은 선택을 합니다. WSL2 설치 및 구성 실제 운영 환경에 더 가까운 리눅스 환경을 구축하기 위해서입니다.
협업 환경(백엔드 개발팀, 데이터 엔지니어링, 개발 주기가 긴 제품 등)에서는 이러한 원칙들이 매우 중요합니다. 여러 사람이 각자의 권한을 넘어서거나 사소한 수정에도 모든 것을 망가뜨리지 않고 동일한 코드베이스에서 작업할 수 있습니다.또한, 파이썬은 유연하고 동적이면서도 추상 클래스, 상속 계층 구조, 합성 및 인터페이스와 같은 전형적인 객체 지향 프로그래밍 추상화 개념을 원활하게 적용할 수 있도록 해줍니다. abc등
요약하자면, SOLID는 다음과 같은 목표 달성을 도와줍니다.
- 더 깔끔하고 읽기 쉬운 코드그 책을 쓴 지 몇 년이 지난 후에도 마찬가지입니다.
- 테스트 가능성 향상책임이 명확하게 분리되어 있기 때문입니다.
- 높은 재사용성과 확장성 모듈 간의 엄격한 종속성이 줄어들었기 때문입니다.
- 부수적인 오류 감소한 모듈에서 무언가를 변경하면 실수로 다른 다섯 가지 기능이 망가지는 일이 없습니다.
S – 단일 책임 원칙
첫 번째 원칙은 다음과 같습니다. 수업이 변경될 이유는 단 하나뿐이어야 합니다.다시 말해, 명확하게 정의된 단일한 책임을 져야 합니다. 이는 단 하나의 방법만 가져야 한다는 의미가 아니라, 모든 논리가 하나의 일관된 목적을 향해야 한다는 뜻입니다.
사용자를 나타내고, 사용자 데이터를 저장하는 것 외에도 데이터베이스에 접근하고 보고서를 생성하는 기능을 갖춘 파이썬 클래스를 상상해 보세요.
class User:
def __init__(self, name: str):
self.name = name
def get_user_from_database(self, user_id: int) -> dict:
# Recupera datos desde la base de datos
# ...
pass
def save_user_to_database(self) -> None:
# Persiste el usuario en la base de datos
# ...
pass
def generate_user_report(self) -> str:
# Genera un informe del usuario
# ...
pass
여기 수업이 있습니다 세 가지 서로 다른 책임을 혼합합니다.사용자를 표현하고, 데이터 영속성을 관리하며, 보고서를 생성합니다. 데이터베이스, 보고서 형식 또는 사용자 속성을 변경하려면 동일한 클래스를 수정해야 하므로, 공통적인 버그가 발생할 위험이 높아집니다.
이러한 요소들을 분리하면 디자인이 크게 개선됩니다.
class User:
def __init__(self, name: str):
self.name = name
class UserDB:
@staticmethod
def get_user(user_id: int) -> User:
# Lógica para obtener usuarios de la base de datos
# ...
return User("John Doe")
@staticmethod
def save_user(user: User) -> None:
# Lógica para guardar el usuario
# ...
pass
class UserReportGenerator:
@staticmethod
def generate_report(user: User) -> str:
# Lógica para generar informes de usuario
# ...
return f"Report for user: {user.name}"
이제 수업은 User는 사용자라는 개체만을 나타냅니다.보고서 생성 방식이 변경되면 탭하기만 하면 됩니다. UserReportGenerator데이터베이스를 변경하려면 그냥 터치하기만 하면 됩니다. UserDB각 클래스는 변경 사유가 하나씩만 존재하므로 디버깅과 시스템 개발이 간소화됩니다.
보다 현실적인 사례에 SRP를 적용해 보았습니다: 오리와 소통
고전적인 시나리오를 응용한 예를 살펴보겠습니다. 바로 수업입니다. 오리 처음에 구현된 시스템은 점차 책임이 추가되면서 유지 관리가 어려운 거대한 시스템으로 변모합니다. 단순한 구현 방식을 상상해 보세요.
class Duck:
def __init__(self, name: str):
self.name = name
def fly(self) -> None:
print(f"{self.name} is flying not very high")
def swim(self) -> None:
print(f"{self.name} swims in the lake and quacks")
def do_sound(self) -> str:
return "Quack"
def greet(self, other_duck: "Duck") -> None:
print(f"{self.name}: {self.do_sound()}, hello {other_duck.name}")
수업 그것은 간단히 "오리"라고 정의되어야 합니다.하지만 이는 오리들이 서로 소통하는 방식도 관리합니다. 만약 내일 대화의 논리를 변경한다면(더 많은 문구, 다른 언어, 다른 채널 등), 이미 하나의 개체로서 잘 작동하는 오리 클래스를 수정해야 합니다.
SRP를 준수하는 해결책은 통신을 전문으로 하는 다른 클래스에서 두 번째 책임을 분리하는 것입니다.
class Duck:
def __init__(self, name: str):
self.name = name
def fly(self) -> None:
print(f"{self.name} is flying not very high")
def swim(self) -> None:
print(f"{self.name} swims in the lake and quacks")
def do_sound(self) -> str:
return "Quack"
class Communicator:
def __init__(self, channel: str):
self.channel = channel
def communicate(self, duck1: Duck, duck2: Duck) -> None:
sentence1 = f"{duck1.name}: {duck1.do_sound()}, hello {duck2.name}"
sentence2 = f"{duck2.name}: {duck2.do_sound()}, hello {duck1.name}"
conversation =
print(*conversation, f"(via {self.channel})", sep="\n")
이러한 분리 덕분에, 오리의 정의를 건드리지 않고도 소통 논리를 발전시킬 수 있습니다.게다가 코드를 테스트하기도 더 쉽습니다. 동작 방식만 테스트하면 되니까요. Duck 반면에, 그 중 하나는 Communicator책임을 뒤섞지 않고.
O – 개방/폐쇄 원칙
OCP 원칙은 다음과 같이 명시합니다. 소프트웨어 구성 요소는 동작 확장에 개방적이어야 하지만 직접적인 수정은 허용하지 않아야 합니다.즉, 새로운 기능을 추가할 때 이미 작동하고 다른 모듈에서 사용되는 클래스를 다시 작성할 필요가 없어야 한다는 것입니다.
대표적인 예로 기하 도형의 면적을 계산하는 것을 들 수 있습니다. 먼저 다음과 같은 예시를 살펴보겠습니다. OCP를 존중하지 않습니다:
class Rectangle:
def __init__(self, width: float, height: float):
self.width = width
self.height = height
class Circle:
def __init__(self, radius: float):
self.radius = radius
class AreaCalculator:
def calculate_area(self, shape) -> float:
if isinstance(shape, Rectangle):
return shape.width * shape.height
elif isinstance(shape, Circle):
return 3.14159 * shape.radius * shape.radius
else:
raise ValueError("Forma no soportada")
내일 삼각형을 추가하고 싶다면, 어쩔 수 없이 그렇게 해야 할 겁니다. 코드 수정 AreaCalculator또 다른 것을 추가합니다 elif이는 클래스가 더 이상 변경에 대해 "닫혀" 있지 않기 때문에 OCP(개방-폐쇄 원칙)를 위반합니다.
올바른 버전은 추상화를 도입하는 것을 포함합니다. Shape 방법을 사용하여 area() 각 인물이 자신만의 방식으로 이를 구현합니다.
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self) -> float:
pass
class Rectangle(Shape):
def __init__(self, width: float, height: float):
self.width = width
self.height = height
def area(self) -> float:
return self.width * self.height
class Circle(Shape):
def __init__(self, radius: float):
self.radius = radius
def area(self) -> float:
return 3.14159 * self.radius * self.radius
class AreaCalculator:
def calculate_area(self, shape: Shape) -> float:
return shape.area()
이 디자인 덕분에, 건드리지 않는 삼각형을 추가하세요 AreaCalculator새로운 하위 클래스를 생성하기만 하면 됩니다.
class Triangle(Shape):
def __init__(self, base: float, height: float):
self.base = base
self.height = height
def area(self) -> float:
return 0.5 * self.base * self.height
개방/폐쇄 원칙은 다음과 같은 개념과 매우 잘 맞아떨어집니다. 추상화를 통해 명확한 확장 지점을 정의합니다.인터페이스, 추상 클래스, 훅 등. 파이썬에서는 모듈이 abc 이를 통해 언어가 동적일지라도 이를 명시적으로 표현할 수 있습니다.
의사소통 사례에 OCP를 적용한 예
다시 이전 예시로 돌아가 보겠습니다. 전달자한 단계 더 나아가, 매번 커뮤니케이터를 다시 작성하지 않고도 다양한 유형의 대화를 지원할 수 있도록 설계를 준비할 수 있습니다. 이를 위해 대화 추상화를 정의하고 커뮤니케이터가 해당 추상화만 사용하도록 합니다.
from typing import final
from abc import ABC, abstractmethod
class AbstractConversation(ABC):
@abstractmethod
def do_conversation(self) -> list:
pass
class SimpleConversation(AbstractConversation):
def __init__(self, duck1: Duck, duck2: Duck):
self.duck1 = duck1
self.duck2 = duck2
def do_conversation(self) -> list:
sentence1 = f"{self.duck1.name}: {self.duck1.do_sound()}, hello {self.duck2.name}"
sentence2 = f"{self.duck2.name}: {self.duck2.do_sound()}, hello {self.duck1.name}"
return
class Communicator:
def __init__(self, channel: str):
self.channel = channel
@final
def communicate(self, conversation: AbstractConversation) -> None:
print(*conversation.do_conversation(), f"(via {self.channel})", sep="\n")
이 버전에서는 새로운 표현 방식을 추가하고 싶다면 (예를 들어, 공격적인 대화, 차례대로 말하는 대화 등) 이러한 상황에 맞는 새로운 하위 클래스를 만들면 됩니다. AbstractConversation. 방법 communicate() de Communicator 이는 OCP를 문자 그대로 준수하는 것으로, 변함이 없습니다.
L – 리스코프 치환 원리
바바라 리스코프가 정립한 리스코프 치환 원리는 다음과 같이 명시합니다. 서브클래스는 프로그램의 예상 동작을 변경하지 않고도 기본 클래스를 대체할 수 있어야 합니다.실제로 이는 기본 클래스의 한 인스턴스에서 코드가 제대로 작동하면 하위 클래스의 어떤 인스턴스에서도 똑같이 잘 작동해야 한다는 것을 의미합니다.
LSP 위반의 전형적인 예는 모든 새를 하나의 방법으로 모델링하는 것입니다. fly()타조를 포함하여:
class Bird:
def fly(self) -> None:
pass
class Duck(Bird):
def fly(self) -> None:
print("¡El pato está volando!")
class Ostrich(Bird):
def fly(self) -> None:
# Las avestruces no vuelan
raise NotImplementedError("Las avestruces no pueden volar")
다음과 같은 가정을 하는 모든 코드 날 수 있는 모든 새는 타조를 받으면 날지 못하게 된다.. 내 말은 Ostrich 그것은 다음을 대체할 수 없습니다. Bird이로써 LSP를 위반하게 됩니다.
해결책은 현실을 더 잘 반영하도록 계층 구조를 조정하는 것입니다. 모든 새가 날지는 않으니까요. 새들 중 일부만 그 방법을 사용해야 합니다. fly():
class Bird:
pass
class FlyingBird(Bird):
def fly(self) -> None:
pass
class Duck(FlyingBird):
def fly(self) -> None:
print("¡El pato está volando!")
class Ostrich(Bird):
# No vuela, así que no implementa fly()
pass
이 디자인으로, 날아다니는 새가 필요한 모든 함수는 해당 함수에 날아다니는 새가 필요하다고 명시적으로 선언합니다. FlyingBird그리고 절대 타조를 받지 않을 것입니다. 이렇게 하면 LSP가 준수되고 예상치 못한 런타임 예외를 방지할 수 있습니다.
LSP와 조류 대화
대화 예시로 돌아가서, 처음에는 오리만 생각하며 코딩을 시작했다가 나중에 까마귀나 다른 새들을 추가하고 싶어지는 경우가 흔합니다. 만약 대화 클래스가 다른 새들에 의존한다면, Duck, 다른 종류의 새에게는 재사용할 수 없습니다. 코드를 건드리지 않고:
class Crow:
# Implementación específica del cuervo
...
Si SimpleConversation 이 코드는 오리에만 적용되도록 설계되었기 때문에, 까마귀를 적용하려면 코드를 수정해야 합니다. 올바른 방법은 공통된 추상화 클래스를 만드는 것입니다. Bird 그리고 대화가 그 추상적인 개념에 의존하도록 만드세요:
from abc import ABC, abstractmethod
class Bird(ABC):
def __init__(self, name: str):
self.name = name
@abstractmethod
def do_sound(self) -> str:
pass
class Crow(Bird):
def do_sound(self) -> str:
return "Caw"
class Duck(Bird):
def do_sound(self) -> str:
return "Quack"
class SimpleConversation(AbstractConversation):
def __init__(self, bird1: Bird, bird2: Bird):
self.bird1 = bird1
self.bird2 = bird2
def do_conversation(self) -> list:
sentence1 = f"{self.bird1.name}: {self.bird1.do_sound()}, hello {self.bird2.name}"
sentence2 = f"{self.bird2.name}: {self.bird2.do_sound()}, hello {self.bird1.name}"
return
이런 식으로, 의 모든 하위 클래스 Bird 계약을 존중하는 (do_sound()(이름 등)은 유효한 대체품 예상되는 동작을 깨뜨리지 않을 것입니다. SimpleConversation.
I – 인터페이스 분리 원칙
ISP 원칙은 다음과 같습니다. 어떤 고객도 자신이 사용하지 않는 방법에 의존하도록 강요받아서는 안 됩니다.추상 클래스나 인터페이스로 표현하자면, 하나의 거대하고 일반적인 인터페이스보다는 여러 개의 작고 구체적인 인터페이스를 갖는 것이 더 낫다는 뜻입니다.
인터페이스가 포함된 이 디자인을 살펴보세요. Worker 이를 실행하려면 모든 참여자가 특정한 업무 및 식사 방법을 갖춰야 합니다.
from abc import ABC, abstractmethod
class Worker(ABC):
@abstractmethod
def work(self) -> None:
pass
@abstractmethod
def eat(self) -> None:
pass
class Human(Worker):
def work(self) -> None:
print("El humano está trabajando")
def eat(self) -> None:
print("El humano está comiendo")
class Robot(Worker):
def work(self) -> None:
print("El robot está trabajando")
def eat(self) -> None:
# El robot no come, pero está obligado a declarar este método
pass
수업 로봇은 특정 방법에 의존합니다. eat() 필요하지 않은음식과 관련된 변화는 로봇의 행동과 직접적인 관련이 없더라도 로봇에게 영향을 미칩니다.
ISP를 적용함으로써 인터페이스를 두 개의 더 작고 구체적인 인터페이스로 나누었습니다.
class Workable(ABC):
@abstractmethod
def work(self) -> None:
pass
class Eatable(ABC):
@abstractmethod
def eat(self) -> None:
pass
class Human(Workable, Eatable):
def work(self) -> None:
print("El humano está trabajando")
def eat(self) -> None:
print("El humano está comiendo")
class Robot(Workable):
def work(self) -> None:
print("El robot está trabajando")
자, 각 클래스는 실제로 필요한 메서드만 구현합니다.이렇게 하면 결합도가 낮아지고 설계 진화가 용이해지며 코드가 더욱 표현력이 풍부해집니다. 누가 무엇을 할 수 있는지 매우 명확해집니다.
조류 모델링에서의 ISP: 비행 및 수영
날고 헤엄치는 새를 모델링할 때도 비슷한 일이 일어납니다. 기본적인 추상화가 잘못되면... Bird 이를 위해서는 두 가지 모두를 구현해야 합니다. fly() 으로 swim()결과적으로 다음과 같은 수업을 듣게 될 것입니다. Crow 수영할 줄 아는 척해야 하는 사람들:
class Bird(ABC):
def __init__(self, name: str):
self.name = name
@abstractmethod
def fly(self) -> None:
pass
@abstractmethod
def swim(self) -> None:
pass
@abstractmethod
def do_sound(self) -> str:
pass
ISP 측에서 제시한 해결책은 다음과 같습니다. 인터페이스를 보다 구체적인 기능으로 분리합니다.:
class Bird(ABC):
def __init__(self, name: str):
self.name = name
@abstractmethod
def do_sound(self) -> str:
pass
class FlyingBird(Bird):
@abstractmethod
def fly(self) -> None:
pass
class SwimmingBird(Bird):
@abstractmethod
def swim(self) -> None:
pass
class Crow(FlyingBird):
def fly(self) -> None:
print(f"{self.name} is flying high and fast!")
def do_sound(self) -> str:
return "Caw"
class Duck(SwimmingBird, FlyingBird):
def fly(self) -> None:
print(f"{self.name} is flying not very high")
def swim(self) -> None:
print(f"{self.name} swims in the lake and quacks")
def do_sound(self) -> str:
return "Quack"
만약 펭귄 모델을 만들어보고 싶다면, 간단히 당신은 그에게 상속받게 합니다 SwimmingBird 하지만 ~에서 FlyingBird그러면 빈 메서드를 구현하거나 인위적인 예외를 발생시킬 필요가 없습니다.
D – 의존성 역전 원리
마지막 원칙인 DIP는 두 가지 핵심 아이디어로 요약할 수 있습니다. 고수준 모듈은 저수준 모듈에 의존해서는 안 되며, 둘 다 추상화에 의존해야 합니다.추상적인 개념은 세부적인 개념에 의존해서는 안 되며, 오히려 세부적인 개념이 추상적인 개념에 의존해야 합니다.
실제로 이는 비즈니스 로직이 "MySQL을 사용한다", "로컬 파일에 쓴다", "이 제공업체를 통해 SMS 메시지를 보낸다"와 같은 특정 세부 사항에 얽매여서는 안 된다는 것을 의미합니다. 대신, 다음과 같이 정의해야 합니다. 추상 인터페이스 (예 : Database, Channel, NotificationService) 그리고 상위 수준 코드가 오직 그들과만 소통하도록 만듭니다.
디자인 브레이크 딥 이는 MySQL 데이터베이스를 직접 인스턴스화하는 사용자 저장소입니다.
class MySQLDatabase:
def connect(self) -> None:
# Conectar a MySQL
pass
def query(self, sql: str) -> list:
# Ejecutar consulta
return []
class UserRepository:
def __init__(self) -> None:
self.database = MySQLDatabase() # Dependencia directa
def get_users(self) -> list:
return self.database.query("SELECT * FROM users")
내일 PostgreSQL을 사용하기로 결정했다면, 다음 사항들을 고려해야 합니다. 상위 클래스를 수정합니다 UserRepository당신은 특정 구현 세부 사항에 묶여 있습니다.
DIP를 적용하면 먼저 데이터베이스 추상화를 정의한 다음 구체적인 구현체가 이를 상속받도록 합니다.
from abc import ABC, abstractmethod
class Database(ABC):
@abstractmethod
def connect(self) -> None:
pass
@abstractmethod
def query(self, sql: str) -> list:
pass
class MySQLDatabase(Database):
def connect(self) -> None:
# Conexión a MySQL
pass
def query(self, sql: str) -> list:
# Consulta en MySQL
return []
class PostgreSQLDatabase(Database):
def connect(self) -> None:
# Conexión a PostgreSQL
pass
def query(self, sql: str) -> list:
# Consulta en PostgreSQL
return []
class UserRepository:
def __init__(self, database: Database) -> None:
self.database = database # Depende de una abstracción
def get_users(self) -> list:
return self.database.query("SELECT * FROM users")
따라서, 어떤 구현체든 주입할 수 있습니다. Database 저장소를 생성할 때, 내부 코드는 건드리지 않습니다.
mysql_db = MySQLDatabase()
user_repo = UserRepository(mysql_db)
postgres_db = PostgreSQLDatabase()
user_repo = UserRepository(postgres_db)
이 패턴은 다음과 같이 알려져 있습니다. 의존성 주입 그리고 이것이 DIP를 적용하는 가장 일반적인 방법입니다. 클래스는 자체적으로 의존성을 생성하지 않고 외부에서 (생성자나 특정 메서드를 통해) 의존성을 받으며, 항상 추상화를 타입으로 사용합니다.
DIP는 채널 및 통신 장치에 적용됩니다.
새들의 대화 예시에서도 DIP를 적용하여 채널 관리를 개선할 수 있습니다. 채널에 대한 추상화와 통신자에 대한 추상화를 각각 정의했다고 가정해 보겠습니다.
class AbstractChannel(ABC):
@abstractmethod
def get_channel_message(self) -> str:
pass
class AbstractCommunicator(ABC):
@abstractmethod
def get_channel(self) -> AbstractChannel:
pass
@final
def communicate(self, conversation: AbstractConversation) -> None:
print(*conversation.do_conversation(),
self.get_channel().get_channel_message(),
sep="\n")
첫 번째로 시도해 볼 수 있는 다소 단순한 구현 방법은 다음과 같습니다.
class SMSChannel(AbstractChannel):
def get_channel_message(self) -> str:
return "(via SMS)"
class SMSCommunicator(AbstractCommunicator):
def __init__(self) -> None:
self._channel = SMSChannel() # Depende de detalle concreto
def get_channel(self) -> AbstractChannel:
return self._channel
맞는 것처럼 보이지만, 이 통신기는 여전히 직접 연결되어 있습니다. SMSChannel통신자가 외부에서 채널을 수신하도록(의존성 주입) 설계를 개선하여 추상화에만 의존하게 했습니다.
class SimpleCommunicator(AbstractCommunicator):
def __init__(self, channel: AbstractChannel) -> None:
self._channel = channel
def get_channel(self) -> AbstractChannel:
return self._channel
이 접근 방식을 사용하면 모든 새로운 채널(이메일, 푸시 알림 등)이 구현됩니다. AbstractChannel y 통신 코드를 변경하지 않고도 사용할 수 있습니다.다시 말해, 고수준 클래스는 세부 사항이 아닌 추상화에 의존합니다.
SOLID를 무시하면 어떻게 될까요?
이러한 원칙들을 고려하지 않으면 코드는 다음과 같은 문제점을 겪기 쉽습니다. 코드 냄새, 코드 부패, 그리고 풀기 불가능한 연결즉, 수많은 책임을 가진 거대한 클래스, 계약을 위반하는 서브클래스, 순환 종속성, 그리고 너무 많은 일을 처리하느라 거의 매일 바뀌는 메서드들을 의미합니다.
그 결과는 분명하며 어떤 팀에게든 매우 고통스러울 것입니다. 취약점 증가, 버그 증가, 끊임없는 리팩토링, 그리고 최악의 경우 실질적으로 사용할 수 없는 코드가 탄생하게 됩니다.흔히 "스파게티 코드"라고 불리는 것인데, 이해하기 어렵고, 패치가 많으며, 중요한 부분을 망가뜨리지 않고는 확장하기가 거의 불가능합니다.
SOLID 원칙은 절대적인 것이 아니며, 특히 신속한 프로토타이핑이나 소규모 프로젝트에서는 모든 원칙을 엄격하게 적용하는 것이 항상 바람직한 것은 아닙니다. 그럼에도 불구하고, 이 점들을 명심하고 파이썬으로 객체 지향 설계를 할 때 대부분 적용해 보세요. 이는 프로젝트가 시간이 지남에 따라 확장될 수 있는지, 아니면 조금만 커져도 무너지는지를 결정짓는 요소입니다.