>[!Notice] >이 글의 내용은 이전 글인 [[0. Context manager를 이용한 composition 관계 다루기|Context manager를 이용한 composition 관계 다루기]]와 이어집니다. ## 예시 코드 : BookShelf 이전 글에서 `Book`과 `Page`를 만들었으니, 이제 책들을 넣을 `BookShelf`를 만들자. 전형적인 코드는 아래와 같을 것이다. ```python #...Book, Page 정의 생략 class BookShelf:     _INSTANCE = None     def __init__(self):         self.books = []     def __new__(cls, *args, **kwargs):         if not cls._INSTANCE:             cls._INSTANCE = super(BookShelf, cls).__new__(cls, *args, **kwargs)         return cls._INSTANCE     def add_book(self, book):         self.books.append(book) def do_something(self): for book in self.books: for page in book.pages: print(page.content) bookShelf = BookShelf() with Book() as book1: for i in range(3): Page(f"book1, page : {i}") bookShelf.add_book(book1) with Book() as book2: for i in range(3): Page(f"book2, page : {i}") bookShelf.add_book(book2) bookShelf.do_something() ``` 위 코드를 조금 부연하자면, `__new__` 부분은 `BookShelf`를 singleton으로 정의하는 내용이다. 또한, `do_something`은 `BookShelf`에서 이뤄지는 특별한 비즈니스 로직이라고 가정하자. 여기서는 결과 확인을 위해 페이지를 출력하는 함수로 구현했다. 위 코드의 결과는 아래와 같다. ``` # 실행 결과 book1, page : 0 book1, page : 1 book1, page : 2 book2, page : 0 book2, page : 1 book2, page : 2 ``` 위 코드는 잘 작동하지만, 한 가지 문제점이 있다. 그건 바로 `Book` 인스턴스를 생성하는 코드와 비즈니스 로직을 실행하는 코드가 같은 파일에 존재한다는 점이다. 두 코드의 성격을 잘 생각해보자. 인스턴스 생성 코드는 인스턴스가 변경되거나 늘었을 때마다 수정해야 한다. 반면, 비즈니스 로직을 실행하는 코드는 수정할 일이 별로 없고 수정이 잦으면 안 된다. 이런 상반되는 성격의 코드가 같이 있는 건 유지보수에 좋지 않다. ## 예시 코드 개선 : BookShelf 이 문제점의 해결 방안 중 하나는 인스턴스 별로 파일을 만들고 해당 파일에 생성 코드를 모두 작성하고(1), 이후 비즈니스 로직이 실행될 때 해당 코드를 실행하고 인스턴스들을 불러오는 것이다(2). ### 폴더 구조 변경 일단 먼저 폴더 구조부터 변경한다. ``` # 폴더 구조 ./classes/__init__.py # Book, Page, BookShelf 정의 ./books/book1.py ./books/book2.py # book 별로 파일 생성 후 생성 코드 작성 ./run.py # BookShelf의 비즈니스 로직 실행 파일 ``` 각 파일 속 코드는 아래와 같다. ```python # ./classes/__init__.py class Page: def __init__(self, content): self.content = content if _BOOK: _BOOK.add_page(self) class Book: def __init__(self): self.pages = [] def add_page(self, page): self.pages.append(page) def __enter__(self): global _BOOK _BOOK = self return self def __exit__(self, *args): global _BOOK _BOOK = None class BookShelf: _INSTANCE = None def __init__(self): self.books = [] def __new__(cls, *args, **kwargs): if not cls._INSTANCE: cls._INSTANCE = super(BookShelf, cls).__new__(cls, *args, **kwargs) return cls._INSTANCE def add_book(self, book): self.books.append(book) def do_something(self): for book in self.books: for page in book.pages: print(page.content) ``` ```python #./books/book1.py from classes import Book, Page with Book() as book1:     for i in range(3):         Page(f"book1, page : {i}") ``` ```python #./books/book2.py from classes import Book, Page with Book() as book2:     for i in range(3):         Page(f"book2, page : {i}") ``` 여기서 두 가지 단계 중 첫 단계, **인스턴스 별로 파일을 만들고 해당 파일에 생성 코드를 모두 작성**(1)이 끝났다. ### run.py 작성 다음 단계는 **비즈니스 로직이 실행될 때 해당 코드를 실행하고 인스턴스들을 불러오는 것**(2)이다. 이 단계는 동적 실행과 네임스페이스를 이용하면 쉽게 달성할 수 있다. #### 동적 실행과 네임스페이스를 이용한 테크닉 ##### `exec`을 통한 동적 실행 동적 실행이란 프로그램 내부 로직으로 코드를 생성하여 실행하는 기능이다. 파이썬에서는 `exec` 함수가 동적 실행 함수이고, `exec` 함수의 첫 번째 인자로 입력한 **문자열**이 코드로 실행된다. 코드 파일도 문자열이기 때문에 아래 코드를 이용하면 `book1.py` 코드와 `book2.py` 코드를 동적 실행할 수 있다. ```python import os exec(open("books/book1.py").read()) exec(open("books/book2.py").read()) for page in book1.pages: print(page.content) for page in book2.pages: print(page.content) ``` ``` # 실행 결과 book1, page : 0 book1, page : 1 book1, page : 2 book2, page : 0 book2, page : 1 book2, page : 2 ``` **위 코드에서 눈여겨볼 점은 동적으로 실행된 코드 속 변수들도 모두 네임스페이스에 저장되었다는 점이다**. 이는 파이썬이 인터프리터 언어라는 점을 생각하면 쉽게 이해할 수 있다. 동적 실행된 코드나 기존 코드나 그냥 한 줄 한 줄 인터프리터에 전달되어 실행되기 때문에 동적 실행된 코드 속 변수들도 네임스페이스에 저장되는 것이다. 위 코드에서도 표면적으로는 따로 `book1`과 `book2`를 생성하지 않았지만, 동적 실행 과정에서 생성 코드가 인터프리터에 전달되어 `book1`과 `book2`가 네임스페이스에 저장되었다. ##### `isinstance`를 통한 변수 필터링 앞으로 `Book` 인스턴스들이 더 추가될 수 있기 때문에 코드 파일을 하나 하나 불러오기 보다는 `./books` 폴더 안에 있는 모든 `.py` 파일을 읽어오도록 코드를 수정해야 한다. 수정한 코드는 아래와 같다. ```python import os directory = "./books" for filename in os.listdir(directory): if filename.endswith(".py"): file_path = os.path.join(directory, filename) with open(file_path) as file: exec(file.read()) for page in book1.pages: print(page.content) for page in book2.pages: print(page.content) ``` 이제 미래에 `book3.py` 코드가 추가된다고 해도 기존의 `book1.py`, `book2.py` 코드와 같이 동적 실행되어 인스턴스가 네임스페이스에 저장될 것이다. 그런데 문제가 하나 있다. 정작 아래에 있는 코드에서는 `book3`를 처리하는 코드가 없다는 점이다. 생각을 달리 해야 한다. 미래에 추가될 인스턴스의 변수명을 알 순 없다. 다만, 해당 인스턴스는 최소한 `Book` 타입인 것은 확실하다. 그러니 알 수 없는 **변수명**이 아니라 **타입**으로 네임스페이스에서 인스턴스를 불러와야 한다. 이때 쓸 수 있는 것이 `isinstance` 함수이다. ```python import os directory = "./books" for filename in os.listdir(directory): if filename.endswith(".py"): file_path = os.path.join(directory, filename) with open(file_path) as file: exec(file.read()) for var in globals().values(): if isinstance(var, Book): for page in var.pages: print(page.content) ``` `globals().values()`는 글로벌 네임스페이스에 저장된 모든 변수들을 불러오는 코드이다. ##### 동적 실행 시 로컬 네임스페이스 지정하기 동적 실행으로 생성된 변수를 글로벌 네임스페이스에서 불러오는 방식은 변수의 수가 많을 때는 사용하기 힘들다. 따라서 더 좋은 방법은 코드 속 변수들을 로컬 네임스페이스에 저장하도록 하는 것이다. 파이썬에서는 아래의 코드로 동적 실행 시 변수들이 로컬 네임스페이스에 저장되도록 설정할 수 있다.[^1] ```python exec([코드, String], [글로벌 네임스페이스, Dictionary], [로컬 네임스페이스, Dictionary]) ``` 로컬 네임스페이스에 변수를 저장하도록 수정한 코드는 아래와 같다. ```python import os directory = "./books" for filename in os.listdir(directory): if filename.endswith(".py"): file_path = os.path.join(directory, filename) local_vars = {} with open(file_path) as file: exec(file.read(), {}, local_vars) for var in local_vars.values(): if isinstance(var, Book): bookShelf.add_book(var) ``` 이게 비즈니스 로직을 처리하는 코드만 추가하면 `run.py`를 완성할 수 있다. #### `run.py` 코드 완성 ```python # ./run.py import os from classes import BookShelf, Book bookShelf = BookShelf() directory = "./books" for filename in os.listdir(directory): if filename.endswith(".py"): file_path = os.path.join(directory, filename) local_vars = {} with open(file_path) as file: exec(file.read(), {}, local_vars) for var in local_vars.values(): if isinstance(var, Book): bookShelf.add_book(var) bookShelf.do_something() ``` ``` # 실행 결과 book1, page : 0 book1, page : 1 book1, page : 2 book2, page : 0 book2, page : 1 book2, page : 2 ``` ## vs IoC [[A. 제어의 역전(Inversion of Control)|IoC]] 도 인스턴스 생성 코드와 해당 인스턴스를 처리하는 코드를 분리시키기 때문에 위 방법과 비슷하다고 볼 수 있다. 그러나 조금 다른 것이 IoC는 개발자가 처리 코드를 작성/수정할 수 있는 상황에서 쓰이지만, 위 방법은 처리 코드는 수정할 수 없고 오직 코드 파일을 통한 인스턴스 추가만 허용될 때 쓰인다. ## 활용 사례 - [click CLI command 설정](https://click.palletsprojects.com/en/8.1.x/commands/#custom-multi-commands) - CLI 설정을 도와주는 click에서 CLI command를 일일이 추가하지 않고, 동적 실행으로 추가할 때 이 방식을 사용한다. [^1]:[Python Doc](https://docs.python.org/ko/3/library/functions.html#exec)