순환 및 동적 모듈 import

Beginner

This tutorial is from open-source community. Access the source code

소개

이 랩에서는 Python 에서 중요한 두 가지 import 관련 개념에 대해 배우게 됩니다. Python 의 모듈 import 는 때때로 복잡한 종속성을 초래하여 오류 또는 비효율적인 코드 구조로 이어질 수 있습니다. 순환 import (Circular import), 즉 두 개 이상의 모듈이 서로를 import 하는 경우, 제대로 관리하지 않으면 문제를 일으킬 수 있는 종속성 루프를 생성합니다.

또한 프로그램 시작 시점이 아닌 런타임에 모듈을 로드할 수 있게 해주는 동적 import (Dynamic import) 에 대해서도 살펴볼 것입니다. 이는 유연성을 제공하고 import 관련 문제를 방지하는 데 도움이 됩니다. 이 랩의 목표는 순환 import 문제를 이해하고, 이를 방지하기 위한 솔루션을 구현하며, 동적 모듈 import 를 효과적으로 사용하는 방법을 배우는 것입니다.

Import 문제 이해하기

모듈 import 가 무엇인지 이해하는 것부터 시작해 보겠습니다. Python 에서 다른 파일 (모듈) 의 함수, 클래스 또는 변수를 사용하려면 import 문을 사용합니다. 그러나 import 를 구성하는 방식에 따라 다양한 문제가 발생할 수 있습니다.

이제 문제가 있는 모듈 구조의 예를 살펴보겠습니다. tableformat/formatter.py의 코드는 파일 전체에 import 가 흩어져 있습니다. 처음에는 큰 문제가 아닌 것처럼 보일 수 있지만 유지 관리 및 종속성 문제를 야기합니다.

먼저 WebIDE 파일 탐색기를 열고 structly 디렉토리로 이동합니다. 몇 가지 명령을 실행하여 프로젝트의 현재 구조를 이해해 보겠습니다. cd 명령은 현재 작업 디렉토리를 변경하는 데 사용되며, ls -la 명령은 숨겨진 파일을 포함하여 현재 디렉토리의 모든 파일과 디렉토리를 나열합니다.

cd ~/project/structly
ls -la

이렇게 하면 프로젝트 디렉토리의 파일이 표시됩니다. 이제 cat 명령을 사용하여 문제가 있는 파일 중 하나를 살펴보겠습니다. 이 명령은 파일의 내용을 표시합니다.

cat tableformat/formatter.py

다음과 유사한 코드가 표시되어야 합니다.

## formatter.py
from abc import ABC, abstractmethod
from .mixins import ColumnFormatMixin, UpperHeadersMixin

class TableFormatter(ABC):
    @abstractmethod
    def headings(self, headers):
        pass

    @abstractmethod
    def row(self, rowdata):
        pass

from .formats.text import TextTableFormatter
from .formats.csv import CSVTableFormatter
from .formats.html import HTMLTableFormatter

def create_formatter(name, column_formats=None, upper_headers=False):
    if name == 'text':
        formatter_cls = TextTableFormatter
    elif name == 'csv':
        formatter_cls = CSVTableFormatter
    elif name == 'html':
        formatter_cls = HTMLTableFormatter
    else:
        raise RuntimeError('Unknown format %s' % name)

    if column_formats:
        class formatter_cls(ColumnFormatMixin, formatter_cls):
              formats = column_formats

    if upper_headers:
        class formatter_cls(UpperHeadersMixin, formatter_cls):
            pass

    return formatter_cls()

파일 중간에 import 문이 있는 것을 확인하세요. 이는 여러 가지 이유로 문제가 됩니다.

  1. 코드를 읽고 유지 관리하기가 더 어려워집니다. 파일을 볼 때 파일이 의존하는 외부 모듈을 빠르게 이해할 수 있도록 모든 import 가 처음에 표시될 것으로 예상합니다.
  2. 순환 import (Circular import) 문제가 발생할 수 있습니다. 순환 import 는 두 개 이상의 모듈이 서로 의존할 때 발생하며, 이로 인해 오류가 발생하고 코드가 예상치 않게 동작할 수 있습니다.
  3. 파일의 모든 import 를 맨 위에 배치하는 Python 규칙을 위반합니다. 규칙을 따르면 코드를 더 읽기 쉽고 다른 개발자가 이해하기 쉽게 만들 수 있습니다.

다음 단계에서는 이러한 문제를 자세히 살펴보고 해결하는 방법을 배우겠습니다.

순환 import (Circular Imports) 탐구

순환 import 는 두 개 이상의 모듈이 서로 의존하는 상황입니다. 구체적으로, 모듈 A 가 모듈 B 를 import 하고, 모듈 B 도 직접 또는 간접적으로 모듈 A 를 import 하는 경우입니다. 이는 Python 의 import 시스템이 제대로 해결할 수 없는 종속성 루프를 생성합니다. 더 간단히 말하면, Python 은 어떤 모듈을 먼저 import 해야 할지 결정하기 위해 루프에 갇히게 되며, 이는 프로그램에서 오류로 이어질 수 있습니다.

순환 import 가 문제를 일으킬 수 있는 방식을 확인하기 위해 코드를 실험해 보겠습니다.

먼저, 현재 구조에서 주식 프로그램이 작동하는지 확인하기 위해 실행합니다. 이 단계는 기준선을 설정하고 변경하기 전에 프로그램이 예상대로 작동하는지 확인하는 데 도움이 됩니다.

cd ~/project/structly
python3 stock.py

프로그램은 올바르게 실행되어 형식화된 테이블에 주식 데이터를 표시해야 합니다. 그렇다면 현재 코드 구조는 순환 import 문제 없이 제대로 작동하고 있다는 의미입니다.

이제 formatter.py 파일을 수정해 보겠습니다. 일반적으로 import 를 파일 상단으로 이동하는 것이 좋습니다. 이렇게 하면 코드가 더 체계적이고 한눈에 이해하기 쉬워집니다.

cd ~/project/structly

WebIDE 에서 tableformat/formatter.py를 엽니다. 다음 import 를 기존 import 바로 뒤, 파일 상단으로 이동합니다. 이러한 import 는 텍스트, CSV 및 HTML 과 같은 다양한 테이블 포맷터에 대한 것입니다.

from .formats.text import TextTableFormatter
from .formats.csv import CSVTableFormatter
from .formats.html import HTMLTableFormatter

따라서 파일의 시작 부분은 다음과 같아야 합니다.

## formatter.py
from abc import ABC, abstractmethod
from .mixins import ColumnFormatMixin, UpperHeadersMixin
from .formats.text import TextTableFormatter
from .formats.csv import CSVTableFormatter
from .formats.html import HTMLTableFormatter

class TableFormatter(ABC):
    @abstractmethod
    def headings(self, headers):
        pass

    @abstractmethod
    def row(self, rowdata):
        pass

파일을 저장하고 주식 프로그램을 다시 실행해 봅니다.

python3 stock.py

TableFormatter가 정의되지 않았다는 오류 메시지가 표시되어야 합니다. 이는 순환 import 문제의 명확한 징후입니다.

이 문제는 다음과 같은 일련의 이벤트로 인해 발생합니다.

  1. formatter.pyformats/text.py에서 TextTableFormatter를 import 하려고 시도합니다.
  2. formats/text.pyformatter.py에서 TableFormatter를 import 합니다.
  3. Python 이 이러한 import 를 해결하려고 할 때, 어떤 모듈을 먼저 완전히 import 해야 할지 결정할 수 없기 때문에 루프에 갇히게 됩니다.

프로그램이 다시 작동하도록 변경 사항을 되돌려 보겠습니다. tableformat/formatter.py를 편집하고 import 를 원래 위치 ( TableFormatter 클래스 정의 뒤) 로 다시 이동합니다.

## formatter.py
from abc import ABC, abstractmethod
from .mixins import ColumnFormatMixin, UpperHeadersMixin

class TableFormatter(ABC):
    @abstractmethod
    def headings(self, headers):
        pass

    @abstractmethod
    def row(self, rowdata):
        pass

from .formats.text import TextTableFormatter
from .formats.csv import CSVTableFormatter
from .formats.html import HTMLTableFormatter

프로그램을 다시 실행하여 작동하는지 확인합니다.

python3 stock.py

이는 파일 중간에 import 가 있는 것이 코드 구성 측면에서 최선의 방법은 아니지만, 순환 import 문제를 피하기 위해 수행되었음을 보여줍니다. 다음 단계에서는 더 나은 솔루션을 살펴보겠습니다.

서브클래스 등록 구현하기

프로그래밍에서 순환 import 는 까다로운 문제일 수 있습니다. formatter 클래스를 직접 import 하는 대신, 등록 패턴을 사용할 수 있습니다. 이 패턴에서 서브클래스는 자체적으로 상위 클래스에 등록됩니다. 이는 순환 import 를 피하는 일반적이고 효과적인 방법입니다.

먼저, 클래스의 모듈 이름을 찾는 방법을 이해해 보겠습니다. 모듈 이름은 등록 패턴에서 사용할 것이므로 중요합니다. 이를 위해 터미널에서 Python 명령을 실행합니다.

cd ~/project/structly
python3 -c "from structly.tableformat.formats.text import TextTableFormatter; print(TextTableFormatter.__module__); print(TextTableFormatter.__module__.split('.')[-1])"

이 명령을 실행하면 다음과 같은 출력이 표시됩니다.

structly.tableformat.formats.text
text

이 출력은 클래스 자체에서 모듈의 이름을 추출할 수 있음을 보여줍니다. 나중에 이 모듈 이름을 사용하여 서브클래스를 등록합니다.

이제 tableformat/formatter.py 파일에서 TableFormatter 클래스를 수정하여 등록 메커니즘을 추가해 보겠습니다. WebIDE 에서 이 파일을 엽니다. TableFormatter 클래스에 몇 가지 코드를 추가합니다. 이 코드는 서브클래스를 자동으로 등록하는 데 도움이 됩니다.

class TableFormatter(ABC):
    _formats = { }  ## 등록된 포맷터를 저장하는 딕셔너리

    @classmethod
    def __init_subclass__(cls):
        name = cls.__module__.split('.')[-1]
        TableFormatter._formats[name] = cls

    @abstractmethod
    def headings(self, headers):
        pass

    @abstractmethod
    def row(self, rowdata):
        pass

__init_subclass__ 메서드는 Python 의 특수 메서드입니다. TableFormatter의 서브클래스가 생성될 때마다 호출됩니다. 이 메서드에서 서브클래스의 모듈 이름을 추출하여 _formats 딕셔너리에 서브클래스를 등록하는 키로 사용합니다.

다음으로, 등록 딕셔너리를 사용하도록 create_formatter 함수를 수정해야 합니다. 이 함수는 주어진 이름에 따라 적절한 formatter 를 생성하는 역할을 합니다.

def create_formatter(name, column_formats=None, upper_headers=False):
    formatter_cls = TableFormatter._formats.get(name)
    if not formatter_cls:
        raise RuntimeError('Unknown format %s' % name)

    if column_formats:
        class formatter_cls(ColumnFormatMixin, formatter_cls):
              formats = column_formats

    if upper_headers:
        class formatter_cls(UpperHeadersMixin, formatter_cls):
            pass

    return formatter_cls()

이러한 변경을 수행한 후 파일을 저장합니다. 그런 다음 프로그램이 여전히 작동하는지 테스트해 보겠습니다. stock.py 스크립트를 실행합니다.

python3 stock.py

프로그램이 올바르게 실행되면 변경 사항이 아무것도 손상시키지 않았다는 의미입니다. 이제 등록이 어떻게 작동하는지 확인하기 위해 _formats 딕셔너리의 내용을 살펴보겠습니다.

python3 -c "from structly.tableformat.formatter import TableFormatter; print(TableFormatter._formats)"

다음과 같은 출력이 표시되어야 합니다.

{'text': <class 'structly.tableformat.formats.text.TextTableFormatter'>, 'csv': <class 'structly.tableformat.formats.csv.CSVTableFormatter'>, 'html': <class 'structly.tableformat.formats.html.HTMLTableFormatter'>}

이 출력은 서브클래스가 _formats 딕셔너리에 올바르게 등록되고 있음을 확인합니다. 그러나 여전히 파일 중간에 몇 가지 import 가 있습니다. 다음 단계에서는 동적 import 를 사용하여 이 문제를 해결합니다.

동적 import (Dynamic Imports) 사용하기

프로그래밍에서 import 는 다른 모듈의 코드를 가져와 해당 기능을 사용할 수 있도록 하는 데 사용됩니다. 그러나 파일 중간에 import 가 있으면 코드가 약간 지저분해지고 이해하기 어려울 수 있습니다. 이 부분에서는 이 문제를 해결하기 위해 동적 import 를 사용하는 방법을 배우겠습니다. 동적 import 는 런타임에 모듈을 로드할 수 있는 강력한 기능으로, 실제로 필요할 때만 모듈을 로드한다는 의미입니다.

먼저, 현재 TableFormatter 클래스 뒤에 있는 import 문을 제거해야 합니다. 이러한 import 는 프로그램이 시작될 때 로드되는 정적 import 입니다. 이렇게 하려면 WebIDE 에서 tableformat/formatter.py 파일을 엽니다. 파일을 연 후 다음 줄을 찾아 삭제합니다.

from .formats.text import TextTableFormatter
from .formats.csv import CSVTableFormatter
from .formats.html import HTMLTableFormatter

터미널에서 다음 명령을 실행하여 지금 프로그램을 실행하려고 하면:

python3 stock.py

프로그램이 실패합니다. 그 이유는 formatter 가 _formats 딕셔너리에 등록되지 않기 때문입니다. 알 수 없는 형식에 대한 오류 메시지가 표시됩니다. 이는 프로그램이 제대로 작동하는 데 필요한 formatter 클래스를 찾을 수 없기 때문입니다.

이 문제를 해결하기 위해 create_formatter 함수를 수정합니다. 목표는 필요할 때 필요한 모듈을 동적으로 import 하는 것입니다. 아래와 같이 함수를 업데이트합니다.

def create_formatter(name, column_formats=None, upper_headers=False):
    if name not in TableFormatter._formats:
        __import__(f'{__package__}.formats.{name}')

    formatter_cls = TableFormatter._formats.get(name)
    if not formatter_cls:
        raise RuntimeError('Unknown format %s' % name)

    if column_formats:
        class formatter_cls(ColumnFormatMixin, formatter_cls):
              formats = column_formats

    if upper_headers:
        class formatter_cls(UpperHeadersMixin, formatter_cls):
            pass

    return formatter_cls()

이 함수에서 가장 중요한 줄은 다음과 같습니다.

__import__(f'{__package__}.formats.{name}')

이 줄은 형식 이름을 기반으로 모듈을 동적으로 import 합니다. 모듈이 import 되면 TableFormatter의 서브클래스가 자동으로 자체적으로 등록됩니다. 이는 앞서 추가한 __init_subclass__ 메서드 덕분입니다. 이 메서드는 서브클래스가 생성될 때 호출되는 특수 Python 메서드이며, 이 경우 formatter 클래스를 등록하는 데 사용됩니다.

이러한 변경을 수행한 후 파일을 저장합니다. 그런 다음 다음 명령을 사용하여 프로그램을 다시 실행합니다.

python3 stock.py

이제 정적 import 를 제거했음에도 불구하고 프로그램이 올바르게 작동해야 합니다. 동적 import 가 예상대로 작동하는지 확인하기 위해 _formats 딕셔너리를 지우고 create_formatter 함수를 호출합니다. 터미널에서 다음 명령을 실행합니다.

python3 -c "from structly.tableformat.formatter import TableFormatter, create_formatter; TableFormatter._formats.clear(); print('Before:', TableFormatter._formats); create_formatter('text'); print('After:', TableFormatter._formats)"

다음과 유사한 출력이 표시되어야 합니다.

Before: {}
After: {'text': <class 'structly.tableformat.formats.text.TextTableFormatter'>}

이 출력은 동적 import 가 필요할 때 모듈을 로드하고 formatter 클래스를 등록하고 있음을 확인합니다.

동적 import 및 클래스 등록을 사용하여 더 깨끗하고 유지 관리 가능한 코드 구조를 만들었습니다. 다음과 같은 이점이 있습니다.

  1. 모든 import 가 이제 파일 상단에 있으며, 이는 Python 규칙을 따릅니다. 이렇게 하면 코드를 읽고 이해하기가 더 쉬워집니다.
  2. 순환 import 를 제거했습니다. 순환 import 는 무한 루프 또는 디버깅하기 어려운 오류와 같은 프로그램에서 문제를 일으킬 수 있습니다.
  3. 코드가 더 유연합니다. 이제 create_formatter 함수를 수정하지 않고도 새로운 formatter 를 추가할 수 있습니다. 이는 새로운 기능이 시간이 지남에 따라 추가될 수 있는 실제 시나리오에서 매우 유용합니다.

동적 import 및 클래스 등록을 사용하는 이 패턴은 플러그인 시스템 및 프레임워크에서 일반적으로 사용됩니다. 이러한 시스템에서 구성 요소는 사용자의 요구 사항 또는 프로그램의 요구 사항에 따라 동적으로 로드되어야 합니다.

요약

이 랩에서는 중요한 Python 모듈 import 개념과 기술에 대해 배웠습니다. 먼저, 순환 import 를 탐구하여 모듈 간의 순환 종속성이 문제를 일으킬 수 있는 방식과 이를 피하기 위해 주의 깊은 처리가 필요한 이유를 이해했습니다. 둘째, 서브클래스가 상위 클래스에 등록되는 패턴인 서브클래스 등록을 구현하여 직접적인 서브클래스 import 의 필요성을 제거했습니다.

또한 런타임에 필요한 경우에만 모듈을 로드하는 동적 import 를 위해 __import__() 함수를 사용했습니다. 이를 통해 코드가 더 유연해지고 순환 종속성을 방지할 수 있습니다. 이러한 기술은 복잡한 모듈 관계를 가진 유지 관리 가능한 Python 패키지를 만드는 데 필수적이며 프레임워크 및 라이브러리에서 일반적으로 사용됩니다. 이러한 패턴을 프로젝트에 적용하면 더 모듈화되고 확장 가능하며 유지 관리 가능한 코드 구조를 구축하는 데 도움이 될 수 있습니다.