객체지향의 사실과 오해 라는 책을 읽고 정리겸 후기를 남겨본다.
내가 느낀 이 책의 경우에 객체지향을 유지보수,확장이 쉬운 코드로 설계하는방법인 SOLID 원칙을 구체적인 예시로 설명하는 책이라고 느꼈다.
SOLID란? 객체 지향 프로그래밍의 5대 원칙이다.
- SRP(단일 책임 원칙)
- OCP(개방-폐쇄 원칙)
- LSP(리스코프 치환 원칙)
- ISP(인터페이스 분리 원칙)
- DIP(의존 역전 원칙)
의 앞글자만 모아 SOLID라 부른다.
보통, 객체지향을 처음 접하게 되면 객체지향이란 현실 세계 속의 존재하는 '사물'을 코드화 시키는것 이라고 배운다. 하지만 우리가 코드화 시키는 작업은 현실 세계 속에 존재하지 않을 수 있는 점을 명심하자.
python 코드로 객체지향 설계 5대원칙인 SOLID를 후불 신용카드 서비스 예시로 각 원칙에 맞게 나쁜(bad)예시와 좋은(good)예시를 간단히 작성해보았다.
아래 작성되는 코드는 모두 git 바로가기에도 있다.
class CardService:
def __init__(self, card_number: str):
self.card_number = card_number
def process_payment(self, amount: int):
print(
f"SRP bad 결제 진행: {amount}원이 후불신용카드({self.card_number[-3:]})로 결제됨")
srp_card = CardService('xxx-xxx-xxx-SRP').process_payment(1000)
출력
SRP bad 결제 진행: 1000원이 후불신용카드(SRP)로 결제됨
위 코드에서 CardService 객체는 카드의종류(번호), 결제진행의 책임을 모두 맡고 있다. 이는 SRP원칙에 위배가 되는 상황이다.
class SRPPaymentGateway:
def srp_process_payment(self, card_num: str, amount: int):
print(
f"SRP good 결제 진행: {amount}원이 후불신용카드({card_num[-3:]})로 결제됨")
class CreditCard:
def __init__(self, card_num: str):
self.card_num = card_num
class CardService:
def __init__(self, my_card: CreditCard, pay_gw: SRPPaymentGateway):
self.my_card = my_card
self.pay_gw = pay_gw
if not isinstance(pay_gw, SRPPaymentGateway):
raise Exception("OCPPaymentGateway가 아님")
def pay_after(self, amount: int):
card_num = self.my_card.card_num
self.pay_gw.srp_process_payment(card_num, amount)
srp_card = CreditCard('xxx-xxx-xxx-SRP')
srp_pay_gw = SRPPaymentGateway()
card_service = CardService(srp_card, srp_pay_gw).pay_after(1000)
출력
SRP good 결제 진행: 1000원이 후불신용카드(SRP)로 결제됨
CardService의 카드종류(카드번호), 결제진행을 각각
CreditCard 와 SRPPaymentGateway 로 나누어 책임을 분리 했다.
이 상황에서 카드의 결제정책이 달라져 수정해야 하는 상황이 오면 어떻게 해야할까?
from typing import Union
class SRPPaymentGateway:
def srp_process_payment(self, card_num: str, amount: int):
print(f"SRP good 결제 진행: {amount}원이 후불신용카드({card_num[-3:]})로 결제됨")
class OCPPaymentGateway:
def ocp_process_payment(self, card_num: str, amount: int):
print(f"OCP bad 결제 진행: {amount}원이 후불신용카드({card_num[-3:]})로 결제됨")
class CreditCard:
def __init__(self, card_num: str):
self.card_num = card_num
class CardService:
def __init__(self, my_card: CreditCard, pay_gw: Union[SRPPaymentGateway, OCPPaymentGateway]):
self.my_card = my_card
self.pay_gw = pay_gw
if not isinstance(pay_gw, OCPPaymentGateway):
raise Exception("OCPPaymentGateway가 아님")
def pay_after(self, amount: int):
card_num = self.my_card.card_num
self.pay_gw.ocp_process_payment(card_num, amount)
ocp_card = CreditCard('xxx-xxx-xxx-OCP')
ocp_pay_gw = OCPPaymentGateway()
card_service = CardService(ocp_card, ocp_pay_gw).pay_after(1000)
# SRP로 다시 바꿀 경우 에러가 발생됨
srp_card = CreditCard('xxx-xxx-xxx-SRP')
srp_pay_gw = SRPPaymentGateway()
card_service2 = CardService(srp_card, srp_pay_gw).pay_after(1000)
출력
OCP bad 결제 진행: 1000원이 후불신용카드(OCP)로 결제됨
Traceback (most recent call last):
File "OCP_bad.py", line 35, in <module>
card_service2 = CardService(srp_card, srp_pay_gw).pay_after(1000)
File "OCP_bad.py", line 21, in __init__
raise Exception("OCPPaymentGateway가 아님")
Exception: OCPPaymentGateway가 아님
위와 출력과 같이 결제 방식을 바꿀 때 마다, 결제 정책과는 전혀 상관없는
CardService 객체의 수정이 불가피해진다.
OCP는 확장에는 열려있고 수정에는 닫혀있어야 하는 원칙이므로, 아래와 같이 수정할 수 있다.
from typing import Union
from abc import ABC, abstractclassmethod
class PaymentGateway(ABC):
"""Payment 인터페이스"""
@abstractclassmethod
def process_payment(self, card_num: str, amount: int):
"""추상화 Payment Gateway 메서드"""
class SRPPaymentGateway(PaymentGateway):
def process_payment(self, card_num: str, amount: int):
print(
f"SRP good 결제 진행: {amount}원이 후불신용카드({card_num[-3:]})로 결제됨")
class OCPPaymentGateway(PaymentGateway):
def process_payment(self, card_num: str, amount: int):
print(
f"OCP good 결제 진행: {amount}원이 후불신용카드({card_num[-3:]})로 결제됨")
class CreditCard:
def __init__(self, card_num: str):
self.card_num = card_num
class CardService:
def __init__(self, my_card: CreditCard, pay_gw: Union[SRPPaymentGateway, OCPPaymentGateway]):
self.my_card = my_card
self.pay_gw = pay_gw
if not isinstance(pay_gw, PaymentGateway):
raise Exception("PaymentGateway가 아님")
def pay_after(self, amount: int):
card_num = self.my_card.card_num
self.pay_gw.process_payment(card_num, amount)
ocp_card = CreditCard('xxx-xxx-xxx-OCP')
ocp_pay_gw = OCPPaymentGateway()
card_service = CardService(ocp_card, ocp_pay_gw).pay_after(1000)
# 이젠 SRP로 해도 에러가 발생하지 않음
srp_card = CreditCard('xxx-xxx-xxx-SRP')
srp_pay_gw = SRPPaymentGateway()
card_service2 = CardService(srp_card, srp_pay_gw).pay_after(1000)
출력
OCP good 결제 진행: 1000원이 후불신용카드(OCP)로 결제됨
SRP good 결제 진행: 1000원이 후불신용카드(SRP)로 결제됨
위와같이 작성하게 되면 앞으로 새로운 PaymentGateway 즉, 새로운 결제방식이 추가 되어도 CardService는 더이상 수정하지 않아도 된다.
이는 확장은 개방되어 있고 수정은 닫혀있는 OCP의 원칙을 잘 지켰다고 볼 수 있다.
또한, 리스코프 치환 원칙인 LSP관점에서 보면 SRP,OCP Payment 객체(하위객체)들은 Payment인터페이스인 Payment(상위객체)에 포함되므로 (즉, SRP,OCP는 결제정책이다 라는 말) LSP또한 잘 지켰다고 볼 수 있다.
물론, 위 예시의 CardService 객체에서
def __init__(self, my_card: CreditCard, pay_gw: Union[SRPPaymentGateway, OCPPaymentGateway]):
self.my_card = my_card
self.pay_gw = pay_gw
if not isinstance(pay_gw, PaymentGateway):
raise Exception("PaymentGateway가 아님")
이 부분을 아예 빼고 작성해도 python은 정상적으로 작동하게 된다.
이는 python이 런타임에 타입을 할당받는 동적 타이핑 언어이기 때문인데, 지금은 객체지향의 설계 5대원칙을 알아보기 위함이므로 무시하자.
또한, pay_gw: 뒤의 Union[]부분도 동작시에는 전혀 상관없는 파이썬 타입힌트이므로 무시하자.
자 이제 결제정책을 확장하는데엔 CardService의 수정이 필요없어졌다.
헌데, 새로 추가할 결제정책은 지불금액의 500원을 할인해주는 정책을 가지고 있다.
이를 ISP의 원칙을 잘지켜 작성해보자.
from typing import Union
from abc import ABC, abstractclassmethod
class PaymentGateway(ABC):
"""Payment 인터페이스"""
@abstractclassmethod
def process_payment(self, card_num: str, amount: int):
"""추상화 process_payment 메서드"""
@abstractclassmethod
def _discount(self, discount_amount: int):
"""추상화 discount 메서드"""
class SRPPaymentGateway(PaymentGateway):
def _discount():
"""강제로 작성해야하는 빈 메서드"""
def process_payment(self, card_num: str, amount: int):
print(
f"SRP good 결제 진행: {amount}원이 후불신용카드({card_num[-3:]})로 결제됨")
class OCPPaymentGateway(PaymentGateway):
def _discount():
"""강제로 작성해야하는 빈 메서드"""
def process_payment(self, card_num: str, amount: int):
print(
f"OCP good 결제 진행: {amount}원이 후불신용카드({card_num[-3:]})로 결제됨")
class ISPPaymentGateway(PaymentGateway):
def _discount(self, amount: int, discount_amount: int):
return amount - discount_amount
def process_payment(self, card_num: str, amount: int):
amount = self._discount(amount, 500)
print(
f"ISP bad 결제 진행: {amount}원이 후불신용카드({card_num[-3:]})로 결제됨")
class CreditCard:
def __init__(self, card_num: str):
self.card_num = card_num
class CardService:
def __init__(self, my_card: CreditCard, pay_gw: Union[SRPPaymentGateway, OCPPaymentGateway, ISPPaymentGateway]):
self.my_card = my_card
self.pay_gw = pay_gw
if not isinstance(pay_gw, PaymentGateway):
raise Exception("PaymentGateway가 아님")
def pay_after(self, amount: int):
card_num = self.my_card.card_num
self.pay_gw.process_payment(card_num, amount)
isp_card = CreditCard('xxx-xxx-xxx-ISP')
isp_pay_gw = ISPPaymentGateway()
card_service = CardService(isp_card, isp_pay_gw).pay_after(1000)
출력
ISP bad 결제 진행: 500원이 후불신용카드(ISP)로 결제됨
지금 상황을 보면, ISP 카드 정책을 확장하기 위해, 기존에 있던 Payment 인터페이스에는 _discount를 추가하고,
기존 카드 정책들에 전혀 필요없는 빈 메서드(_discount)를 강제로 추가 시켜야 했다.
이는 인터페이스를 분리하는 ISP에 위배되었다고 볼 수 있으며, 이와 같은 Payment 인터페이스를 뚱뚱한 인터페이스 라고 부른다.
from typing import Union
from abc import ABC, abstractclassmethod
class PaymentGateway(ABC):
"""Payment 인터페이스"""
@abstractclassmethod
def process_payment(self, card_num: str, amount: int):
"""추상화 process_payment 메서드"""
class Event(ABC):
"""Event 인터페이스"""
@abstractclassmethod
def _discount(self, discount_amount: int):
"""추상화 discount 메서드"""
class SRPPaymentGateway(PaymentGateway):
def process_payment(self, card_num: str, amount: int):
print(
f"SRP good 결제 진행: {amount}원이 후불신용카드({card_num[-3:]})로 결제됨")
class OCPPaymentGateway(PaymentGateway):
def process_payment(self, card_num: str, amount: int):
print(
f"OCP good 결제 진행: {amount}원이 후불신용카드({card_num[-3:]})로 결제됨")
class ISPPaymentGateway(PaymentGateway, Event):
def _discount(self, amount: int, discount_amount: int):
return amount - discount_amount
def process_payment(self, card_num: str, amount: int):
amount = self._discount(amount, 500)
print(
f"ISP bad 결제 진행: {amount}원이 후불신용카드({card_num[-3:]})로 결제됨")
class CreditCard:
def __init__(self, card_num: str):
self.card_num = card_num
class CardService:
def __init__(self, my_card: CreditCard, pay_gw: Union[SRPPaymentGateway, OCPPaymentGateway, ISPPaymentGateway]):
self.my_card = my_card
self.pay_gw = pay_gw
if not isinstance(pay_gw, PaymentGateway):
raise Exception("PaymentGateway가 아님")
def pay_after(self, amount: int):
card_num = self.my_card.card_num
self.pay_gw.process_payment(card_num, amount)
my_card = CreditCard('xxx-xxx-xxx-ISP')
pay_gw = ISPPaymentGateway()
card_service = CardService(my_card, pay_gw).pay_after(1000)
출력
ISP good 결제 진행: 500원이 후불신용카드(ISP)로 결제됨
위와같이 할인정책을 명시하는 Event 인터페이스를 하나 추가하여
할인정책이 필요하지 않은 기존 정책에는 필요없는 메서드를 추가않고,
필요한 ISP결제정책에만 적용하여 인터페이스를 분리하였다.
DIP 원칙은 저수준 객체는 추상적인 객체에 의존해야하는 원칙이다.
위 카드서비스 예시 코드에서
CardService(저수준 객체)는 Payment(추상화 인터페이스 객체)에 의존하고 있다.
물론, CreditCard 객체에도 의존하고 있지만, 위 가정에서 카드는 1개로 가정했다. 위 예제는 간단히 만들어본 것이며, CreditCard객체도 Payment 인터페이스처럼 여러 종류의 카드 정보를 갖고있는 추상화 객체로써 사용할 수 있다.