파이썬으로 이해하는 퍼사드 패턴

들어가며

프로젝트가 커지면 “디자인 패턴” 이라는 개념에 눈이 저절로 가게 됩니다… 디자인 패턴이란 특정 유형의 문제들을 풀기 위한 솔루션 패턴이라고 이해하시면 쉬울 것 같습니다. 처음 디자인 패턴이 나왔을 때는 23개의 디자인 패턴이 나왔는데요. 저는 그중에 우선 “파사드 패턴”을 공부해보았습니다. 그리고 자바가 아닌 파이썬으로 알기 쉽게 정리해보았습니다. “파이썬으로 이해하는 퍼사드 패턴” 들어갑니다.

퍼사드 패턴 이란?

우선 wiki에서 퍼사드 패턴을 찾으면 아래와 같이 알려줍니다.

facade pattern

다시 말해 퍼사드 패턴은 복잡한 시스템이나 라이브러리를 간단하게 제공하기 위해 사용되는 패턴이라는 겁니다.

핵심은 복잡한 클래스를 -> 간단하게 제공 입니다.

프로젝트의 크기가 커지면 여러 클래스들이 등장하게 되고 복잡하게 되겠죠? 이런 것들을 간단하게 제공하기 위한 패턴이라는 겁니다.

퍼사드 패턴을 나타내는 다이어그램

퍼사드 패턴을 위와 같은 다이어그램으로 나타내는데요. class diagram, sequence diagram이 뭐냐구요?

일단 다이어그램 개념을 몰라도 중요한 사실은 위에서 말씀 드린 것과 같이 복잡한 클래스들을 퍼사드 클래스로 간단하게 사용자에게 제공한다는 점 입니다.

그림에서도 여러 클래스들이 결국엔 퍼사드 클래스를 통해서 프로세스가 흐른다는 것을 알 수 있겠죠?

abc 라이브러리 : 퍼사드 패턴을 이해하기 위한 사전지식

퍼사드 패턴을 이해하기 위해서는 구조 패턴에 많이 쓰이는 abc 라이브러리 부터 알아야 합니다.

아래 간단한 예제 코드를 가져왔는데요.

여러 동물들 클래스를 만든다고 가정해봅시다. 그런데 이 동물들 클래스의 공통된 구조 (여기서는 울음소리를 내는 speak()함수)가 있기 때문에 이것을 구조화 할 수 있습니다.

이런 구조화를 돕는 게 추상 클래스 (여기서는 Animal 클래스) 입니다.

추상 클래스는 metaclass 인자에 ABCMeta 클래스를 입력하고요.

여러 하위 클래스에서 사용할 함수는 함수 선언 위에 @abstractmethod를 입력합니다.

from abc import ABCMeta, abstractmethod


class Animal(metaclass=ABCMeta):
    @abstractmethod
    def speak(self):
        pass

class Dog(Animal):
    def speak(self):
        return "Woof!"

class Cat(Animal):
    def speak(self):
        return "Meow!"

dog, cat = Dog(), Cat()
print(f'dog: {dog.speak()}, cat:{cat.speak()}')

abc 라이브러리는 왜 쓰는건가요?

클래스의 기본적인 개념을 아시면 상속으로 하위 클래스를 만들고 메소드 오버라이드도 언제든 마음껏 할 수 있다는 점도 아실 겁니다.

그런데 abc 라이브러리는 왜 쓰는 걸까요?

하위 클래스들을 구조화 해줄 수 있기 때문입니다. 아래 ABCMeta와 abstractmethod 없이 상속만으로 하위 클래스를 구현한 예제 코드를 봅시다

class Animal:
    def speak(self):
        pass

class Dog(Animal):
    def howling(self):
        return "Woof!"

class Cat(Animal):
    def speak(self):
        return "Meow!"

dog, cat = Dog(), Cat()
print(f'dog: {dog.speak()}, cat:{cat.speak()}')  # dog: None, cat:Meow!

Dog 클래스에서 소리를 내는 메소드는 howling()으로 구현하고 Cat 클래스의 메소드는 speak()로 구현 하였습니다.

이 단순한 코드만 보면 “뭐야 바보인가? 저거 2개 하나 함수를 제대로 못 맞추나?” 싶을 수도 있습니다.

하지만 코드가 수천 수십만 줄이 되다 보면 비슷한 기능의 함수들의 이름들이 헷갈릴 수 있습니다.

위의 코드에서도 Dog 클래스와 Cat 클래스와 비슷하다 보니 함수 호출할 때 Dog의 인스턴스에도 speak()로 메소드를 호출하는 것을 볼 수 있습니다.

이런 실수를 덜고자 클래스를 구조화 시킬 수 있게 만드는 게 ABCMeta 클래스와 @abstractmethod 입니다.위

와 동일한 코드인데 아래와 같이 ABCMeta와 @abstractmethod 을 활용하여 코드를 작성하면 클래스 자체가 잘못 선언되었다고 TypeErorr를 뿌려줍니다.

class Animal(metaclass=ABCMeta):
    @abstractmethod
    def speak(self):
        pass


class Dog(Animal):
    def howling(self):
        return "Woof!"


class Cat(Animal):
    def speak(self):
        return "Woof!"


dog, cat = Dog(), Cat()
print(f'dog: {dog.speak()}, cat:{cat.speak()}')

파이썬으로 이해하는 퍼사드 패턴 1

좋은 예제 코드가 있는 것 같아서 가져 왔습니다. 해당 코드는 OS을 코드로 구현한 건데요.

듣기만 해도 복잡해 보이죠…?

여기서는 여러 서버 기능들을 구현하려고 했는데요. File server, Process server, window server 등 여러 클래스를 구현 해야하는 상황입니다.

따라서 여러 클래스들 중에 공통적인 기능을 가진 클래스는 추상 클래스로 (여기서는 Server) 선언하고 하위 클래스들이 이 추상 클래스를 상속 받는 것을 확인 할 수 있습니다.

그래서 이런저런 복작복작한 클래스들이 있는데, 여기서 중요한 건 최종족으로는 퍼사드 클래스 (여기서는 OperatingSystem)로 묶어서 사용자가 편하게 사용할 수 있게 만들어야 하는 겁니다.

이렇게 되면 궁극적으로는 OperatingSystem은 start()하고 create_file(), create_process() 매소드로 동작할 수 있는 거구나 쉽게 알 수 있는 겁니다.

예제 코드

from enum import Enum 
from abc import ABCMeta, abstractmethod 
 
State = Enum('State', 'new running sleeping restart zombie') 
 
class User: 
    pass 
 
class Process: 
    pass 
 
class File: 
    pass 
 
class Server(metaclass=ABCMeta): 
    @abstractmethod 
    def __init__(self): 
        pass 
 
    def __str__(self): 
        return self.name 
 
    @abstractmethod 
    def boot(self): 
        pass 
 
    @abstractmethod  
    def kill(self, restart=True): 
        pass 
 
class FileServer(Server): 
    def __init__(self): 
        '''actions required for initializing the file server''' 
        self.name = 'FileServer' 
        self.state = State.new 
 
    def boot(self): 
        print(f'booting the {self}') 
        '''actions required for booting the file server''' 
        self.state = State.running 
 
    def kill(self, restart=True): 
        print(f'Killing {self}') 
        '''actions required for killing the file server''' 
        self.state = State.restart if restart else State.zombie 
 
    def create_file(self, user, name, permissions): 
        '''check validity of permissions, user rights, etc.''' 
        print(f"trying to create the file '{name}' for user '{user}' with permissions {permissions}") 
 
class ProcessServer(Server): 
    def __init__(self): 
        '''actions required for initializing the process server''' 
        self.name = 'ProcessServer' 
        self.state = State.new 
 
    def boot(self): 
        print(f'booting the {self}') 
        '''actions required for booting the process server''' 
        self.state = State.running 
 
    def kill(self, restart=True): 
        print(f'Killing {self}') 
        '''actions required for killing the process server''' 
        self.state = State.restart if restart else State.zombie 
 
    def create_process(self, user, name): 
        '''check user rights, generate PID, etc.''' 
        print(f"trying to create the process '{name}' for user '{user}'") 
 
class WindowServer: 
    pass 
 
class NetworkServer: 
    pass 
 
class OperatingSystem: 
    '''The Facade''' 
    def __init__(self): 
        self.fs = FileServer() 
        self.ps = ProcessServer() 
 
    def start(self): 
        [i.boot() for i in (self.fs, self.ps)] 
 
    def create_file(self, user, name, permissions): 
        return self.fs.create_file(user, name, permissions) 
 
    def create_process(self, user, name): 
        return self.ps.create_process(user, name) 
 
def main(): 
    os = OperatingSystem() 
    os.start()  
    os.create_file('foo', 'hello', '-rw-r-r') 
    os.create_process('bar', 'ls /tmp') 
 
if __name__ == '__main__': 
    main()

파이썬으로 이해하는 퍼사드 패턴 2

예제 하나를 더 가지고 퍼사드 패턴을 이해해보도록 하겠습니다.

만일, ETL 도구를 만든다고 가정해봅시다. ETL이란 Extraction, Transformation, Load의 약자로 주기적으로 내부 및 외부 데이터베이스로부터 정보를 추출하고 정해진 규약에 따라 정보를 변환한 후 Data 웨어하우스에 정보를 적재하는 프로세스를 뜯합니다.

여기서는 유럽, 아시아, 미국에서 특정 정보를 Extract 하고 Transformation하고 Load하는 프로세스를 구현한다고 가정해봅시다.

유럽에서는 파일로 데이터를 제공하고 아시아에서는 API로 데이터를 제공하고 USA는 파일을 제공하지 않아 크롤링을 해야 한다고 하면

각각 다른 로직으로 Extraction을 해야겠지만 큰 틀로 보면 “1.데이터를 가져오고”, “2.가져온 데이터의 통계 를 확인한다”

이 2가지 스텝으로 이루워 질 수 있습니다.

이를 추상 클래스로 선언을 합니다. 그리고 유럽, 아시아, 미국에서 각 extraction을 과정을 클래스로 구현하는 것이지요.

이렇게 extraction, transformation, load 단계들을 여러 클래스들 만들어 구현 할 수 있습니다.

그런 후, 퍼사드 패턴의 중요한 점은 간단한 인터페이스를 사용자에게 제공하는 것 입니다.

그래서 아래 코드에서는 Etl 클래스로 퍼사드를 완성하는 것을 확인 할 수 있습니다.

예제 코드

from abc import ABCMeta, abstractclassmethod
import pandas as pd
import numpy as np
from typing import Union
import os
import requests
from bs4 import BeautifulSoup
import pickle
import pymongo

"""
Extraction, Transformation, Load
주기적으로 내부 및 외부 데이터베이스로부터 정보를 추출하고 
정해진 규약에 따라 정보를 변환한 후 DW에 정보를 적재하는 프로세스
"""


class Extraction(metaclass=ABCMeta):
    @abstractclassmethod
    def __init__(self) -> None:
        pass

    @abstractclassmethod
    def get_data(self, data: Union[dict, pd.DataFrame, str]) -> None:
        pass

    @abstractclassmethod
    def statistics(self) -> pd.DataFrame:
        pass


class Extract_europe(Extraction):
    def __init__(self, url):
        self.path = './europe_extraction'
        self.url = url

    def get_data(self):
        os.system(f"wget -P {self.path} {self.url}")

    def statistics(self):
        """
        데이터 통계
        """
        return


class Extract_asia(Extraction):
    def __init__(self, url):
        self.path = './asia_extraction'
        self.file = 'asia_original_collection.pickle'
        self.url = url

    def get_data(self):
        response = requests.get(self.url)
        if response.status_code == 200:
            data = response.json()
            with open(f'{self.path}/{self.file}', 'wb') as fw:
                pickle.dump(data, fw)
        else:
            print('Error:', response.status_code)

    def statistics(self):
        """
        데이터 통계
        """
        return


class Extract_usa(Extraction):
    def __init__(self, url):
        self.path = './usa_extraction'
        self.data = pd.DataFrame()
        self.url = url

    def get_data(self):
        response = requests.get(self.url)
        html_data = response.text
        soup = BeautifulSoup(html_data, 'html.parser')

    def statistics(self):
        """
        데이터 통계
        """
        return


class Transformation(metaclass=ABCMeta):
    @abstractclassmethod
    def __init__(self) -> None:
        pass

    @abstractclassmethod
    def pre_statistics(self):
        """
        transformation 이전의 통계 값
        """
        pass

    @abstractclassmethod
    def filtering(self):
        """
        데이터 클렌징
        """
        pass

    @abstractclassmethod
    def transform(self):
        """
        데이터 형식 변환 
        """
        pass

    @abstractclassmethod
    def standardization(self):
        """
        데이터 표준화
        """
        pass

    @abstractclassmethod
    def post_statistics(self):
        """
        데이터 transformation 이후 통계 값
        """
        pass

    @abstractclassmethod
    def save(self):
        pass


class Transform_europe(Transformation):
    def __init__(self):
        pass

    def pre_statistics(self):
        pass

    def filtering(self):
        pass

    def transform(self):
        pass

    def standardization(self):
        pass

    def post_statistics(self):
        pass

    def save(self):
        pass


class Transform_asia(Transformation):
    def __init__(self):
        pass

    def pre_statistics(self):
        pass

    def filtering(self):
        pass

    def transform(self):
        pass

    def standardization(self):
        pass

    def post_statistics(self):
        pass

    def save(self):
        pass


class Transform_usa(Transformation):
    def __init__(self):
        pass

    def pre_statistics(self):
        pass

    def filtering(self):
        pass

    def transform(self):
        pass

    def standardization(self):
        pass

    def post_statistics(self):
        pass

    def save(self):
        pass


class Load_data:
    def __init__(self, data) -> None:
        self.data = data
        self._MONGO_URI = os.environ.get('MONGO_URI')
        self._MONGO_DB = os.environ.get('MONGO_DB')

    def load(self):
        client = pymongo.MongoClient(self._MONGO_URI)
        db = client[self._MONGO_DB]
        collection = db.mycollection
        result = collection.insert_one(self.data)


class Etl:
    def __init__(self) -> None:
        self.europe_extraction = Extract_europe('url1')
        self.asia_extraction = Extract_asia('url2')
        self.usa_extraction = Extract_usa('url3')
        self.europe_transformation = Transform_europe()
        self.asia_transformation = Transform_asia()
        self.usa_transformation = Transform_usa()

    def extraction(self):
        for continent in (
                self.europe_extraction, self.asia_extraction, self.usa_extraction):
            continent.get_data()
            continent.statistics()

    def transformation(self):
        for continent in (self.europe_transformation, self.asia_transformation,
                          self.usa_transformation):
            continent.pre_statistics()
            continent.filtering()
            continent.transform()
            continent.standardization()
            continent.post_statistics()
            continent.save()

    def load(self):
        data_load = Load_data()
        data_load.load()


if __name__ == '__main__':
    etl = Etl()
    etl.extraction()
    etl.transformation()
    etl.load()

마치며

퍼사드 패턴을 비롯해 대부분의 패턴은 복잡한 문제를 해결하기 위한 패턴 입니다.

때문에 간단하고 단순한 문제를 해결할 때는 디자인 패턴을 굳이 사용하지 않아도 됨을 알 수 있습니다.

다만 위의 예에서 보시다시피 복잡한 기능을 구현한다면 퍼사드로 패턴을 잡고 각 클래스 구현을 여러 인원에게 분배하는 식으로 프로젝트를 진행한다면 체계적으로 기능 구현을 완성 할 수 있을 것 같습니다.

참고하면 좋은 글

Leave a Comment

목차