SOLID 설계 원칙, 코드 예제와 함께 차근차근 이해하기

1. SOLID 원칙이란 무엇인가요?

소프트웨어 개발에서 튼튼한 건물처럼 오래 버티는 코드를 짓고 싶으신가요? 그렇다면 SOLID 원칙을 꼭 익히셔야 합니다. SOLID는 다섯 가지 핵심 객체지향 설계 원칙의 약자로, 각각 Single Responsibility Principle(단일 책임 원칙), Open/Closed Principle(개방/폐쇄 원칙), Liskov Substitution Principle(리스코프 치환 원칙), Interface Segregation Principle(인터페이스 분리 원칙), Dependency Inversion Principle(의존 역전 원칙)을 의미합니다. 쉽게 말하면, 각 원칙은 우리가 코드를 더 견고하고 유지보수하기 쉽게, 또 확장하기 좋은 형태로 작성할 수 있도록 지침을 제공합니다. 예를 들어 집을 지을 때 튼튼한 기초가 필요하듯이, SOLID 원칙은 우리가 버그에 강하고 변화에 유연한 코드를 만드는 데 필요한 설계의 기초라고 할 수 있습니다.

2. 단일 책임 원칙 (SRP): 하나의 클래스, 하나의 책임

단일 책임 원칙은 말 그대로 “단 하나의 일만 하라”는 뜻입니다. 예를 들어, 커피머신 프로그램을 만든다고 가정해 보세요. 커피를 추출하는 기능과 재고를 관리하는 기능이 한 클래스에 뒤섞여 있다면, 커피 추출 로직을 수정하는데 재고 관련 코드까지 꼭 신경 써야 하므로 유지보수가 어렵죠. 대신, CoffeeMaker 클래스는 커피 추출만 담당하고, InventoryManager 클래스는 재고만 관리하게 한다면 더 명확하고, 오류 발생 가능성도 줄어듭니다.

python
class CoffeeMaker:
def brew(self):
print(“커피를 추출합니다.”)

class InventoryManager:
def update_stock(self, ingredient):
print(f”{ingredient}의 재고를 업데이트합니다.”)
이렇게 하면 각각의 클래스가 명확한 목적을 가지고, 수정사항도 쉽게 관리할 수 있습니다.

3. 개방/폐쇄 원칙 (OCP): 변화에는 닫히고, 확장에는 열려라

두 번째 원칙은 ‘기존 코드는 건드리지 않고, 새로운 기능은 쉽게 추가할 수 있어야 한다’는 철학을 담고 있습니다. 그림을 그리는 프로그램을 생각해보면, 동그라미, 네모 등 다양한 도형을 그릴 수 있어야 하죠. 매번 새로운 도형이 추가될 때마다 기존 도형의 코드를 수정한다면 버그가 생길 가능성이 커집니다. 하지만 추상 클래스나 인터페이스를 활용하면 기존 코드는 그대로 두고, 새로운 도형 클래스만 추가하면 그만입니다.

python
from abc import ABC, abstractmethod

class Shape(ABC):
@abstractmethod
def draw(self):
pass

class Circle(Shape):
def draw(self):
print(“원을 그립니다.”)

class Square(Shape):
def draw(self):
print(“사각형을 그립니다.”)

def paint(shape: Shape):
shape.draw()
나중에 Triangle 클래스를 추가해도 paint 함수나 기존 클래스를 건드릴 필요가 없습니다. 변화에는 닫히고, 확장에는 열려 있기 때문입니다.

4. 리스코프 치환 원칙 (LSP): 자식은 부모의 역할을 완벽히 대신해야 한다

리스코프 치환 원칙은 “부모 클래스를 사용하는 곳에 자식 클래스도 아무 문제 없이 넣을 수 있어야 한다”고 강조합니다. 만약 Square가 Rectangle을 상속받는데, 가로와 세로를 다르게 설정할 수 없게 만들었다면(정사각형은 가로=세로니까) Rectangle을 기대한 코드에서는 오작동이 발생할 수 있습니다. 자식 클래스는 부모 클래스의 규약을 철저히 따라야 하고, 예외를 만들면 안 됩니다.

python
class Rectangle:
def set_width(self, width):
self.width = width
def set_height(self, height):
self.height = height
def area(self):
return self.width * self.height

class Square(Rectangle):
def set_width(self, width):
self.width = width
self.height = width
def set_height(self, height):
self.width = height
self.height = height
이렇게 구현하면, Rectangle을 사용하는 코드는 Square를 넣어도 동일하게 동작합니다.

5. 인터페이스 분리 원칙 (ISP): 크고 무거운 인터페이스는 반드시 쪼갤 것

프로그램을 만들다 보면 동물 인터페이스에 ‘날기’, ‘수영하기’ 등 다양한 기능을 다 넣고 싶어질 수 있습니다. 그러나 펭귄(Penguin)처럼 못 나는 동물도 있는데, 굳이 ‘날기’ 기능을 구현하라고 강요하면 안 되겠죠. 이런 상황이 인터페이스 분리 원칙이 꼭 필요한 이유입니다.

python
class Flyable:
def fly(self):
print(“날 수 있습니다.”)

class Swimmable:
def swim(self):
print(“수영할 수 있습니다.”)

class Penguin(Swimmable):
def swim(self):
print(“펭귄은 수영을 잘합니다.”)
즉, 동물은 상황에 따라 필요한 인터페이스만 구현하는 것이 바람직합니다.

6. 의존 역전 원칙 (DIP): 구체적인 클래스가 아니라, 추상화에 의존하라

마지막으로 의존 역전 원칙을 살펴보겠습니다. 무거운 물건을 옮기는 컨베이어벨트 프로그램을 만든다고 가정해 볼까요? 만약 ConveyorBelt 클래스가 Duck나 Apple 같은 구체 객체에 직접 의존한다면 매번 새로운 물건이 등장할 때마다 ConveyorBelt 코드를 수정해야 합니다. 하지만 추상화(인터페이스)에 의존하도록 설계하면, 어떤 물건이든 해당 인터페이스만 구현하면 쉽게 ConveyorBelt에서 활용할 수 있습니다.

python
from abc import ABC, abstractmethod

class Transportable(ABC):
@abstractmethod
def move(self):
pass

class Duck(Transportable):
def move(self):
print(“오리가 움직입니다.”)

class Apple(Transportable):
def move(self):
print(“사과가 움직입니다.”)

class ConveyorBelt:
def carry(self, item: Transportable):
item.move()
이렇게 하면 신제품이 나와도 ConveyorBelt 코드에는 손댈 일이 거의 없습니다.

7. 결론: SOLID 원칙 실천이 변화를 이끕니다

오늘은 코드 예제를 통해 SOLID 원칙의 핵심을 알아보았습니다. 각 원칙은 혼자 두어도 좋지만, 함께 사용하면 시너지 효과가 더욱 커집니다. 개발 환경은 늘 변화하기 때문에, 처음부터 견고함과 유연함을 모두 잡을 수 있는 SOLID 원칙을 습관처럼 적용해 보세요. 여러분의 코드는 더욱 읽기 쉽고, 확장하기 쉬우며, 시간이 흘러도 여전히 탄탄하게 남아 있을 것입니다.

Similar Posts

답글 남기기

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