본문 바로가기
Language/Python

[Python] refactoring - 02. 캡슐화, DI를 통한 리팩토링

by 며루치꽃 2022. 4. 20.

 

목적

리팩토링할 코드를 가지고, 여러 번의 단계에 걸쳐 리팩토링을 진행한다.
As-is에서 나온 개선해야할 부분을 To-be를 통해 개선한다.

리팩토링 전 문제점 파악

class Store(ABC):
@abstractclassmethod
def __init__(self):
self.money = 0
self.name = ""
self.products = {}
@abstractclassmethod
def set_money(self, money):
pass
@abstractclassmethod
def set_products(self, products):
pass
@abstractclassmethod
def get_money(self):
pass
@abstractclassmethod
def get_products(self):
pass
class EricStore:
def __init__(self):
self.money = 0
self.name = "에릭상점"
self.products = {
1: {"name": "키보드", "price": 30000},
2: {"name": "모니터", "price": 50000},
}
def set_money(self, money):
self.money = money
def set_products(self, products):
self.products = products
def get_money(self):
return self.money
def get_products(self):
return self.products
class User:
def __init__(self, store: Store):
self.money = 0
self.store = store # 의존성 주입(구현체 대신에 인터페이스가 들어간다) -> 런타임에서 생성자 주입
self.belongs = []
def purchase_product(self, product_id):
product = self.see_product(product_id)
if self.money >= product["price"]:
self.store.products.pop(product_id) # 상점에서 상품 꺼내기
self.money -= product["price"] # 사용자가 돈 내기
self.store.money += product["price"] # 상점에서 돈 받기
self.belongs.append(product)
return product
else:
raise Exception("잔돈이 부족합니다")

해당 코드의 문제점

  • User 입장에서 store의 product의 attribute에 직접 접근해서 상품을 꺼내고 있다
  • User 입장에서 store에게 돈을 받는 행위가 있다.
  • Store에 있는 상품과 돈을 마음대로 접근할 수 있다.
  • User가 상품의 속성을 알고있으면 알고 있을수록 결합도는 높아진다

개선할 점

  • 중요한 정보는 캡슐화를 통해 숨기고 객체가 외부에서 사용할 수 있는 메소드만을 정의만 해주고 사용하라고 지시한다
  • Store의 책임을 정의하고 캡슐화한다
  • User의 결제 로직을 수정한다
  • User도 캡슐화한다

to-be

  • 외부에서 재고를 제외한 진열된 물품을 보여줄 수 있게 한다
  • 상점은 상품을 고객에게 제공할 수 있어야한다
  • 상점은 고객에게 돈을 받아야한다

리팩토링

생성자 주입을 통한 관심사 분리

As-is

class EricStore:
def __init__(self):
self.money = 0
self.name = "에릭상점"
self.products = {
1: {"name": "키보드", "price": 30000},
2: {"name": "모니터", "price": 50000},
}
  • 기존에는 생성자에 물품을 제시하였다.

To-be

class EricStore:
def __init__(self, products):
self.money = 0
self.name = "에릭상점"
self.products = products
  • 외부에서 생성자를 통한 의존성 주입(생성자 주입)을 통해 관심사를 분리한다
  • 생성자 주입을 통해 외부에서 의존 관계를 설정함으로서 어떤 구현 객체가 들어올지 알 수 없어서 변경에 유리하다

캡슐화(상점)

As-is

class Store(ABC):
@abstractclassmethod
def __init__(self):
self.money = 0
self.name = ""
self.products = {}
@abstractclassmethod
def set_money(self, money):
pass
@abstractclassmethod
def set_products(self, products):
pass
@abstractclassmethod
def get_money(self):
pass
@abstractclassmethod
def get_products(self):
pass
  • 캡슐화가 되어있지않아 구현체에서 set_money, set_products 등의 메서드를 이용하여 상점의 속성을 직접 접근하고 있다.

To-be

class Store(ABC):
@abstractclassmethod
def __init__(self):
self.money = 0
self.name = ""
self.products = {}
@abstractclassmethod
def show_product(self, product_id):
pass
@abstractclassmethod
def give_product(self, product_id):
pass
@abstractclassmethod
def take_money(self, money):
pass
  • 해당 속성에 직접 접근하는 대신 메서드를 통한 접근을 통해 직접 접근을 막는다.

As-is

class EricStore:
def __init__(self):
self.money = 0
self.name = "에릭상점"
self.products = {
1: {"name": "키보드", "price": 30000},
2: {"name": "모니터", "price": 50000},
}
def set_money(self, money):
self.money = money
def set_products(self, products):
self.products = products
def get_money(self):
return self.money
def get_products(self):
return self.products
  • attribute에 직접 접근하여 의도하지 않은 값을 수정할 노출이 있다.

To-be

class EricStore:
def __init__(self, products):
self._money = 0
self.name = "에릭상점"
self._products = products
def set_money(self, money):
self._money = money
def set_products(self, products):
self._products = products
def show_product(self, product_id):
return self._products[product_id]
def give_product(self, product_id):
self._products.pop(product_id) # products에 product_id를 key로 가지는 value를 지운다
def take_money(self, money):
self._money += money
  • 캡슐화를 통한 직접 접근을 제한한다
  • 구현체에서의 private 접근제어자(underscore)을 이용하여 관리할 속성들을 private하게 관리한다
    • 외부 메서드나 public 접근제어자를 통해 접근하게 만든다.

캡슐화(User)

As-is

class User:
def __init__(self, store: Store):
self.money = 0
self.store = store # 의존성 주입(구현체 대신에 인터페이스가 들어간다) -> 런타임에서 생성자 주입
self.belongs = []
def set_money(self, money):
self.money = money
def set_belongs(self, belongs):
self.belongs = belongs
def get_money(self):
return self.money
def get_belongs(self):
return self.belongs
def get_store(self):
return self.store
def see_product(self, product_id):
products = self.store.get_products()
return products[product_id]
def purchase_product(self, product_id):
product = self.see_product(product_id)
if self.money >= product["price"]:
self.store.products.pop(product_id) # 상점에서 상품 꺼내기
self.money -= product["price"] # 사용자가 돈 내기
self.store.money += product["price"] # 상점에서 돈 받기
self.belongs.append(product)
return product
else:
raise Exception("잔돈이 부족합니다")
  • User 입장에서 store의 product의 attribute에 직접 접근해서 상품을 꺼내고 있다
  • User 입장에서 store에게 돈을 받는 행위가 있다.

To-be

  • 유저가 가지고 있는 돈도 생성자 주입을 통해 받는다
  • 상황에 따라서 getter, setter가 필요없을 경우 메서드를 삭제한다
  • get, set 보다 직관적인 메서드 이름으로 네이밍을 하는 것이 좋다

As-is

def purchase_product(self, product_id):
product = self.see_product(product_id)
if self.money >= product["price"]:
self.store.products.pop(product_id) # 상점에서 상품 꺼내기
self.money -= product["price"] # 사용자가 돈 내기
self.store.money += product["price"] # 상점에서 돈 받기
self.belongs.append(product)
return product
else:
raise Exception("잔돈이 부족합니다")
  • 유저가 마음대로 store에 있는 상품을 꺼내고 있다
  • 유저가 가게의 돈을 올리고 있다.

To-be

def purchase_product(self, product_id):
product = self.see_product(product_id=product_id)
if self.money >= product["price"]:
self.store.give_product(product_id=product_id) # 상점에서 상품 꺼내기
self.money -= product["price"] # 사용자가 돈 내기
self.store.take_money(product["price"]) # 상점에서 돈 받기
self.belongs.append(product)
return product
else:
raise Exception("잔돈이 부족합니다")
  • 위에서 구현한 give_product() 를 이용하여 고객이 상점의 물품을 건드리는 것이 아닌, 상점이 고객에게 물품을 제공하는 것으로 바꾼다
  • take_money() 를 이용하여 상점이 스스로 물품의 돈을 얻을 수 있도록 한다
  • 중요한 속성에 직접 접근하지 못하게 하여서 side-effect를 줄인다.

댓글