본문 바로가기
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를 줄인다.

댓글