본문 바로가기
Coding/Python

Python Observer Pattern

by Hide­ 2019. 2. 20.
반응형

소프트웨어를 설계할 때 가장 먼저 고려할 점이 바로 아키텍쳐 구조이다. 또한 아키텍쳐의 구조를 제대로 설계하려면 디자인 패턴또한 고려해야한다. 나는 이번에 다음과 같은 프로그램을 짜고 있었다.


- 파일을 업로드하고 데이터베이스에 관련 정보를 저장하는 프로그램


위 프로그램의 플로우를 하나하나 살펴보자면 다음과 같다.


1. S3에 파일을 업로드한다.

2. 업로드된 파일의 URL을 통해 특정 비즈니스 로직을 거친다.

3. 로직을 거친 이후 나온 결과물을 최종적으로 데이터베이스에 저장한다.


여기서 발생할 수 있는 문제점이 있다. 1번 파일 업로드 부분에서 바로 오류가 발생하면 상관이 없겠지만, 2번 또는 3번처럼 파일을 업로드한 이후 오류가 발생한다면 S3에는 파일이 남아있지만 실제적으로 해당 파일을 Tracking할수가 없다. 우리는 데이터베이스에 쌓인 값들을 토대로 사용자에게 랜더링을 해주기 때문이다. 따라서 이 부분을 어떻게 탐지하고 롤백할 수 있을까에 대해 생각하다가 옵저버 패턴이 생각났다. 파이썬에서 옵저버 패턴으로 무언가 구현해본적은 없기에 이번 기회에 공부하여 써먹자고 다짐했다.


일단 프로젝트의 구조부터 설명하자면 다음과 같다.

.

├── interactors

│   └── __init__.py

├── main.py

├── observers

│   └── __init__.py

├── repositories

│   └── __init__.py

├── subject

│   └── __init__.py

└── views

    └── __init__.py


간단하게 장고라고 생각하면 될 것 같다.

main.py -> views -> interactors -> repository를 거치는 형태이다.

views는 사용자에게 랜더링을 해주고 interactors에는 비즈니스 로직이 들어있다.

마지막으로 repository에는 실제 데이터베이스와 연결하여 값을 가져온다. 참고로 이번 포스팅에서는 세부적인 코드는 작성하지 않을 것이고 옵저버 패턴 구현만을 목적으로 둔다. 먼저 다음과 같이 /subject/__init__.py에 Subject 클래스를 생성한다.

class Subject:
def __init__(self):
self.observers = []
self.state = []

def register(self, observer):
self.observers.append(observer)

def notify_all(self):
for observer in self.observers:
observer.notify(self)

self.observers에 옵저버들을 저장할것이고 state는 공유변수 역할을 한다. register()를 통해 옵저버를 등록하고 notify_all()을 통해 옵저버들에게 알림을 보낸다. 이제 /observers/__init__.py의 내용을 채운다.

class Observer:
def notify(self, subject):
print('Exception occur. Delete File -> ', self.subject.state)

옵저버들에서 특정한 작업을 진행하기 위한 함수를 만들어야하는데, 현 예제에서는 동일한 함수가 필요하므로 나중에 상속받아서 사용하기위해 만들어준 클래스이다.

다음으로 main.py이다.

from views import View


if __name__ == '__main__':
v = View()
v.view()

간단하다. View객체를 생성하고 view를 실행시킨다.

이제 /views/__init__.py에 아래의 내용을 채워넣는다.

from subject import Subject
from interactors import Interactor


class View:
def __init__(self):
self.subject = Subject()

def view(self):
print("[*] VIEW START")
Interactor(self.subject).execute()
print("[*] VIEW END")

뷰에서 특정 로직을 거치고 Interactor를 호출한다.

이때 위에서 정의한 subject를 같이 넘겨줘야한다.

/interactors/__init__.py에 아래의 내용을 넣는다.

from repositories import Repository
from observers import Observer


class Interactor(Observer):
def __init__(self, subject):
self.subject = subject
self.subject.register(self)
self.repository = Repository(self.subject)

def execute(self):
try:
print("[*] Interactor START")
file1 = '1.jpg'
file2 = '2.jpg'
self.subject.state.extend([file1, file2])
self.repository.save_user()
print("[*] Interactor STOP")

except Exception:
print("[*] Interactor Exception")
self.subject.notify_all()

특정한 연산을 거치는 로직이 있어야하는데 여기서는 넣지 않았다.

간단하게 file1, file2를 만들고 Subject의 공유변수인 state에 넣어줬다.

그리고 repository의 save_user()를 호출한다.

다음으로 /repositories/__init__.py도 아래의 내용을 넣는다.

from observers import Observer


class Repository(Observer):
def __init__(self, subject):
self.subject = subject

def save_user(self):
try:
# raise Exception()
print("[*] Repository START")
print(self.subject.state)
print("[*] Repository STOP")
except Exception:
print("[*] Repository Exception")
self.subject.notify_all()

(그냥 실행시킨 결과와 예외발생부분을 주석해제한 실행결과를 비교해보면 이해가 쉬울 것이다)

마찬가지로 특정 작업을 진행하는 형태인데 그 내용은 담지 않았다.

interacotr, repository모두 마찬가지로 오류가 발생하면(예외가 발생하면) Subject에 정의된 notify_all()을 실행시킨다.

notify_all()은 등록된 observer들의 notify라는 함수를 실행시킴으로써 특정한 작업을 실행시킨다.

많은 디자인 패턴들이 존재하기에 해당 패턴들에서 현재 내 소프트웨어의 구조에 맞는 패턴을 찾기란 쉽지 않다.

직접 코딩하여 겪어보고 그 경험을 바탕으로 소프트웨어에 녹여내야하는데 이는 쉽지 않은 작업인 것 같다.

본 포스팅을 통해 조금이나마 파이썬 옵저버 패턴에 대해 이해했으면 하는 바램이다.