본문 바로가기

개발

파이썬 예제로 보는 직접 의존성과 간접 의존성

반응형

의존성 (Dependencies) 없는 프로그램은 존재하지 않습니다. 소프트웨어의 일부분(함수, 클래스, 모듈 등)은 어떤 기능을 수행하기 위해 다른 부분에 의존하고 있습니다. 그리고 의존성은 직접 의존성 (또는 명시적 의존성, Explicit Dependencies)과 간접 의존성 (또는 암시적 의존성, Implict Dependencies)으로 나눌 수 있습니다. 이번 포스트에서는, 이러한 의존성들의 정의와 각각의 장단점에 대해서 알아보는 시간을 가져보도록 하겠습니다. 😎

직접 의존성

직접 의존성은 어떤 함수나, 클래스가 각자의 인터페이스를 통해 '의존 대상'에 직접적으로 의존하고 있는 것을 의미합니다. 의존 대상이 인터페이스에 직접적으로 정의되어 있기 때문에 인터페이스만 보고도 의존하고 있는 대상에 대해서 알 수 있습니다. 의존성을 주입한다고 해서 'Dependency Injection' (의존성 주입)이라고 부르기도 합니다. 예를 들어서, say라는 함수를 호출했을 때 db에 텍스트를 저장하고, sns에도 해당 텍스트를 공유하는 함수가 있다고 해봅시다.

def say(text: str, db: Database, sns: SNS) -> None:
    print(text)
    db.save(text)
    sns.share(text)

함수 내에서 사용되는 dbsns가 인자에 정의되어 있기 때문에 say 함수가 DatabaseSNS 객체에 의존하고 있다는 것을 쉽게 알 수 있습니다. 이번에는 클래스 예제를 한 번 살펴봅시다.

class Speaker:

    def __init__(self, db: Database, sns: SNS) -> None:
        self._db = db
        self._sns = sns

    def say(self, text: str) -> None:
        print(text)
        self._db.save(text)
        self._sns.share(text)

이번에는 Speaker 클래스의 컨스트럭터(__init__)의 인자에 dbsns가 정의되어 있습니다. 즉, 해당 클래스가 DatabaseSNS 객체에 직접적으로 의존하고 있습니다. 해당 클래스 내의 메서드들은 self를 통해 해당 객체에 접근할 수 있습니다.

간접 의존성

간접 의존성은 어떤 함수나, 클래스가 '의존 대상'에 간접적으로 의존하고 있는 것을 의미합니다. 인자를 통해 의존 대상을 알 수 있는 직접 의존성과는 달리, 간접 의존성의 경우 의존 대상이 함수나 클래스의 코드 블록 외부에 정의되어 있습니다. 위에서 본 예제를 간접 의존성으로 표현해 보면 다음과 같습니다.

함수 예제

from app.db import db
from app.sns import sns

def say(text: str) -> None:
    print(text)
    db.save(text)
    sns.share(text)

클래스 예제

from app.db import db
from app.sns import sns

class Speaker:

    def say(self, text: str) -> None:
        print(text)
        db.save(text)
        sns.share(text)

함수(메서드) 내에서 인자로 dbsns를 받지 않고, 함수와 클래스 외부(여기서는 외부 모듈)에 정의되어 있는 db와 sns를 로드하여 간접적으로 사용합니다.

직접의존성과 간접의존성의 장단점

직접의존성과 간접의존성 중 어떤 것이 낫다고 말할 수는 없습니다. 각 방식의 장점과 단점을 이해하고, 본인이 진행하는 프로젝트의 환경과 상황에 더 적합한 방식을 사용하는 것이 중요합니다.

직접의존성의 장점 (간접의존성의 잠재적인 문제점)

1. 구현사항을 보지 않아도 의존성을 명시적으로 이해할 수 있다.

직접의존성은 의존 관계가 인터페이스에 명시적으로 정의되어 있기 때문에, 해당 코드의 의존 관계를 쉽고 빠르게 파악할 수 있습니다. 반대로 간접의존성의 경우 코드의 구현을 읽어야만 의존성을 확인할 수 있습니다. 코드가 복잡할수록 의존관계가 복잡해지기 때문에, 인터페이스에서 의존관계를 파악할 수 있는 것은 매우 큰 장점 중 하나입니다.

2. 보다 유연하게 코드를 변경할 수 있다.

의존성 주입 패턴은 코드를 변경하기가 상대적으로 쉽습니다. 의존하고 있는 객체를 추상화하고, 코드가 추상화된 객체에 의존하고 있다면 코드의 구현을 변경하지 않고도 손쉽게 코드를 변경할 수 있습니다. 설명이 어려우니, 예제를 통해서 살펴보겠습니다.

# db.py
import abc

class BaseDB(abc.ABC)

    @abc.abstractmethod
    def save(self, text: str) -> None:
        ...

class MySqlDB(BaseDB)

    def save(self, text: str) -> None:
        ... # mysql db 저장 로직

class MongoDB(BaseDB)

    def save(text: str) -> None:
        ... # MongoDB db 저장로직

# main.py
def say(text: str, db: BaseDB) -> None:
    print(text)
    db.save(text)


mongo = MongoDB()
say("Hello, World", mongo)

mysql = MySqlDB()
say("Hello, World", mysql)

say 함수는 BaseDB라는 추상화 클래스에 의존하고 있고, MySqlDBMongoDBBaseDB를 구현하고 있는 구체화된 클래스입니다. 이 경우 데이터베이스를 MongoDB에서 MySql로 변경하더라도, 세부적인 구현 사항 수정 없이 함수가 의존하고 있는 db만 MySqlDB 객체로 변경해 주면 됩니다. 즉, 변경 범위를 최소화할 수 있고, 이는 사이드 이펙트 역시 최소화할 수 있다는 의미이기도 합니다.

3. 테스트하기 쉽다.

직접 의존성을 사용한 경우 일반적으로 테스트하기 훨씬 쉽습니다. 직접의존성을 가짜 객체(목, 스텁, 스파이 등)로 변경하여 관심사를 쉽게 테스트할 수 있습니다. 예제를 통해 살펴보겠습니다.

# test.py

class InMemoryDB(BaseDB):

    data = []

    def save(self, text: str) -> None:
        data.append(text)


    def get(self, text: str) -> str | None:
        try:
            return [item for item in self.data if item == text][0]
        except IndexError:
            return None

def test_say_works() -> None:
    db = InMemoryDB()
    say("Hello, World", db)

    assert db.get("Hello, World")
    assert db.get("Hello, Korea") is None

위와 같이 say의 결과가 db에 저장되는지를 쉽게 테스트할 수 있습니다. 만약 간접의존성을 사용했다면 테스트 환경에서 db를 대체하지 못해, 데이터베이스 세팅 및 각 테스트 환경의 독립성을 보장하기 위한 추가 조치를 취해야 하고 이는 일반적으로 테스트하기 어려운 경우가 많습니다. (다만, db까지 모두 테스트할 수 있다는 것은 가짜 객체를 사용하는 것보다 가정이 적기 때문에 테스트 신뢰도는 훨씬 높을 수 있습니다.)

간접의존성의 장점 (직접의존성의 잠재적인 문제점)

1. 인터페이스를 단순하게 유지할 수 있다. 보일러 플레이트 코드가 적다.

직접의존성을 사용한 경우에는 클래스의 컨스트럭터나 함수의 인자에 의존성을 명시해줘야 하기 때문에, 상대적으로 인자가 많습니다. 또한 의존성을 다루기 위한 코드를 추가적으로 작성해야 합니다. 반면 간접의존성은 코드블록 외부에 의존성이 정의되어 있기 때문에 코드를 단순하게 가져갈 수 있다는 장점이 있습니다. 이 때문에 코드 베이스의 규모가 작은 경우에는 간접의존성을 사용하는 것이 가독성이 높은 경우가 많습니다.

2. 성능적인 이점을 얻을 수 있는 경우가 있다.

이 부분은 구현에 따라 편차가 있을 수 있습니다. 하지만, 추상화를 통해 직접의존성을 사용하는 경우에는 인터페이스에 제약이 있는 경우가 많습니다. 예를 들어, 추상화 없이 db모듈을 사용했을 때 최적화된 쿼리를 만들어 낼 수 있는 경우가 있는데, 이를 추상화하려는 경우 구현에 따라 성능을 포기하거나 잃게 되는 경우가 발생할 수 있습니다. 이 경우 성능을 최적화하려면 보다 많은 고민이 필요합니다.

정리

이번 포스트에서는 직접의존성과 간접의존성에 대해 알아봤습니다. 직접 의존성과 간접 의존성은 모두 각각 장단점을 가지고 있기 때문에, 각각의 트레이드오프 관계를 잘 이해하고 현재의 환경(코드의 복잡도, 유연성, 성능, 그리고 프로젝트(서비스)의 기대 수명 등)에 맞게 선택하는 것이 중요합니다. 긴 글 읽어주셔서 감사합니다. :)

반응형