개발블로그

[객체지향] SOLID 원칙 : 좋은 객체지향 설계를 판단하는 5가지 기준 본문

STUDY/CS

[객체지향] SOLID 원칙 : 좋은 객체지향 설계를 판단하는 5가지 기준

devmel 2026. 9. 3. 17:23
Contents 접기
 

왜 SOLID 원칙이 필요할까?

 

객체지향으로 코드를 작성한다고 해서 자동으로 좋은 설계가 되는 것은 아님.

 

클래스를 나누었지만 하나의 클래스가 너무 많은 일을 할 수 있고, 기능을 하나 추가할 때마다 기존 코드를 계속 수정해야 할 수도 있다. 또한 상속을 사용했지만 자식 클래스가 부모 클래스처럼 동작하지 않아 예상치 못한 오류가 생길 수도 있다. 

 

즉, 객체지향의 핵심은 단순히 클래스를 사용하는 것이 아니라 변경에 강하고 이해하기 쉬운 구조를 만드는 것

 


 

5가지 기준

 

원칙 이름 핵심 질문
SRP 단일 책임 원칙 이 클래스가 바뀌는 이유가 하나인가?
OCP 개방-폐쇄 원칙 기능 추가 때 기존 코드를 계속 수정해야 하는가?
LSP 리스코프 치환 원칙 자식 객체를 부모 객체처럼 사용해도 문제가 없는가?
ISP 인터페이스 분리 원칙 사용하지 않는 기능까지 의존하고 있지는 않은가?
DIP 의존성 역전 원칙 상위 로직이 구체적인 구현에 직접 의존하고 있지는 않은가?

 


 

SRP : 단일 책임 원칙

 

하나의 클래스는 하나의 책임을 가져야 한다. 

하나의 클래스가 바뀌는 이유가 너무 많으면 안 됨.

=> 변경 이유가 같은 코드끼리 모으고, 다른 코드끼리는 분리

 

ex)

# 이 클래스는 결제 처리, 결제 내역 저장, 알림 발송을 모두 담당함
class PaymentService:
    def pay(self, amount):
        pass

    def save_history(self):
        pass

    def send_notification(self):
        pass
        
# 문제 : 결제 방식이 바뀔 때, 저장소가 바뀔 때, 알림 방식이 바뀔 때 모두 같은 클래스가 수정됨
        
# 변경 이유가 다르다면, 책임도 분리하는 것이 좋다. 
class PaymentService:
    def pay(self, amount):
        pass

class PaymentHistoryRepository:
    def save(self, payment):
        pass

class NotificationService:
    def send(self, message):
        pass

 

 


 

OCP : 개방-폐쇄 원칙

 

확장에는 열려있고, 변경에는 닫혀 있어야 함. 

 

새로운 기능을 추가할 때 기존 코드를 계속 수정해야 한다면, 그 코드는 변경에 약한 구조일 수 있음.

=> 자주 바뀌는 부분을 분리해서, 확장할 때 수정 범위를 줄이기 

 

EX)

# 쿠폰 결제가 추가되면 pay() 함수 자체를 수정해야 함
def pay(payment_type, amount):
    if payment_type == "card":
        return "카드 결제"
    elif payment_type == "point":
        return "포인트 결제"
        
# => 쿠폰 결제가 추가되면 기존 pay() 함수 안에 조건문을 또 추가해야 함
# => 확장할 때 기존 코드가 계속 열려있다. 
        
# 결제 방식을 객체로 분리하면 새로운 결제 수단을 추가하기 쉬워짐
class CardPayment:
    def pay(self, amount):
        return "카드 결제"

class PointPayment:
    def pay(self, amount):
        return "포인트 결제"

class CouponPayment:
    def pay(self, amount):
        return "쿠폰 결제"

 

 


 

LSP :  리스코프 치환 원칙

 

자식 클래스는 부모 클래스를 대체할 수 있어야 함

 

상속 관계를 만들었는데 자식 클래스가 부모 클래스의 기대 동작을 지키지 못하면 문제가 생긴다. 

 

ex)

# 모든 결제 수단은 pay()를 할 수 있다고 가정
class Payment:
    def pay(self, amount):
        pass

class CardPayment(Payment):
    def pay(self, amount):
        return "카드 결제 완료"

class PointPayment(Payment):
    def pay(self, amount):
        return "포인트 결제 완료"
        
# 주문 코드는 구체적인 결제 방식이 무엇인지 몰라도 됨
def checkout(payment, amount):
    return payment.pay(amount)
    
# 여기까지는 LSP를 잘 지킨 구조 : 자식 클래스들이 모두 부모의 기대 "결제할 수 있다"는 약속을 지킴

# 만약 무료 쿠폰을 결제 수단이라고 보고, Payment를 상속하게 만들었을 때
class FreeCouponPayment(Payment):
    def pay(self, amount):
        if amount > 0:
            raise Exception("무료 쿠폰은 결제 금액을 처리할 수 없습니다.")
        return "무료 쿠폰 적용"
        
# 문제 : checkout()은 Payment라면 당연히 pay(amount)를 처리할 수 있다고 기대, 
# 그런데 FreeCouponPayment는 특정 금액에서 예외를 던짐
# => 부모 타입으로 사용할 수 있을거라 믿었는데 자식 클래스가 그 기대를 깨버림


# 이때는 FreeCouponPayment를 Payment의 자식으로 두는 것이 어색할 수 있다. 
# 무료쿠폰은 결제수단이라기 보다는 할인 정책에 가까울 수 있음
class DiscountPolicy:
    def discount(self, amount):
        pass

class FreeCouponDiscount(DiscountPolicy):
    def discount(self, amount):
        return amount

 

 


 

ISP : 인터페이스 분리 원칙

 

사용하지 않는 기능에 의존하지 않도록 인터페이스를 나눠라

=> 사용하는 쪽이 필요한 기능에만 의존하게 만드는 것

 

EX)

class Machine:
    def print(self):
        pass

    def scan(self):
        pass

    def fax(self):
        pass
        
# 이 인터페이스를 모든 기계가 따라야 한다면, 단순 프린터도 scan()과 fax()를 억지로 구현해야 함
# => 불필요한 기능까지 강제로 의존하게 만듦
class SimplePrinter(Machine):
    def print(self):
        pass

    def scan(self):
        raise Exception("지원하지 않습니다.")
        
# 역할별로 나누기
class Printer:
    def print(self):
        pass

class Scanner:
    def scan(self):
        pass

 

 

 


 

DIP : 의존성 역전 원칙

 

상위 수준의 로직이 구체적인 구현에 직접 의존하지 않도록 하라

 

EX]

class OrderService:
    def __init__(self):
        self.payment = CardPayment()

    def order(self, amount):
        return self.payment.pay(amount)
        
# OrderService는 CardPayment에 직접 의존함
# => 다른 결제 수단을 사용하려면 OrderService를 수정해야 함

# 결제 방식을 외부에서 주입받으면 더 유연해짐
class OrderService:
    def __init__(self, payment):
        self.payment = payment

    def order(self, amount):
        return self.payment.pay(amount)
# => OrderService는 구체적인 결제 방식이 무엇인지 알 필요가 없음. 
# 그저 pay()를 수행할 수 있는 객체에 의존하면 됨.