객체지향 파이썬을 위하여

아샬 · 원프레딕트 테크니컬 디렉터
October 03, 2023

아샬이 파이콘 2023 Day 2 라이트닝 토크 때 발표한 내용을 풀어서 쓴 글입니다.

이유를 알 수 없는 오해

파이썬은 대표적인 객체지향 프로그래밍 언어입니다. C++의 연장선에 있길 원했던(그리고 대체재가 되는 데 성공한) Java와 달리, 기존 방식에 타협하지 않고 객체지향에 집중할 수 있게 설계된 언어죠. 주1 하지만 많은 사람들은 파이썬이 강력한 객체지향 언어라는 사실을 잊고 있습니다. 심지어 객체지향 프로그래밍 언어인지 의심을 품기도 하죠. 아마 쉬운 언어라는 홍보에, 객체지향이란 (상대적으로) 어려운 측면이 가려진 게 아닌가 싶기도 합니다.

파이썬을 정말로 쉽게 쓰고 있나요? 쉽다는 건 어떤 의미인가요? 파이썬으로 작성된 기존 프로젝트를 유지보수하면서 쉽다고 느끼시나요? 혹시 어렵다는 느낌을 받고 있진 않나요? 입문하기 쉽다고 했지, 유지보수가 쉽다고 하진 않았다고요?

그럴 리가요.

그렇다면 대체 뭐가 문제인 걸까요?

술이 문제야
술이 문제야

객체지향 패러다임이 등장한 건 소프트웨어 개발 및 유지보수의 복잡성을 관리하기 위함인데, 객체지향 언어인 파이썬을 쓰면서 왜 이런 문제를 겪게 되는 걸까요? 혹시 우리가 사용하는 도구(파이썬과 다양한 패키지)는 객체지향적이지만, 우리가 작성하는 코드는 여전히 절차지향적인 게 아닐까요?

절차지향이란?

흔히 “절차지향 프로그래밍”이라 옮기는 “Procedural Programming”은 좀 더 정확히 번역하면 “프로시저 중심의 프로그래밍”이라고 할 수 있습니다. 주2 기존에는 하나의 거대한 프로그램에서 GOTO 등을 통해 분기 및 반복 처리를 했다면, 프로시저라는 작은 프로그램 단위를 만들어서 활용하자는 패러다임이죠.

커다란 프로그램을 작은 부분으로 나눠서 복잡성을 통제한다는 아이디어는 매우 훌륭하지만, 거칠게 이야기하면 우리가 프로시저로 분해한 건 데이터 조작에 관한 겁니다.

만약 우리가 단순한 CRUD를 다루는 프로그램을 만든다면 이런 접근법으로도 충분할 겁니다. 하지만 우리는 좀 더 복잡한 비즈니스 규칙을 다루려고 하고, 단순히 데이터 조작에 관한 코드를 분해하는 것만으론 충분하지 않다는 걸 깨닫게 되죠.

Kick Me!
절...차

객체지향이란?

절차지향에서 우리는 커다란 프로그램을 프로시저라는 단위로 나눠준다고 했습니다. 그렇다면 객체지향은 어떨까요? 객체지향에서 우리는 커다란 프로그램을 객체라는 단위로 나눠줍니다.

객체는 데이터와 연산(프로시저, 메서드)을 하나로 묶어줍니다(캡슐화). 데이터는 오직 연산을 통해서만 변경 가능하고, 우리는 메시지를 통해 연산을 실행하도록 요청합니다. 즉, 다른 객체의 상태를 확인하지 않고, 그저 원하는 바를 시키게 됩니다. 이를 “Tell-Don't-Ask” 원칙이라고 하죠. 주3

한마디로 객체지향은 “자율적인 객체의 협력”이 핵심이라고 할 수 있습니다.

그렇다면 객체는 어떻게 협력할까요? 아니, 어떻게 협력하는 게 효과적일까요?

UML의 아버지라 불리는 Grady Booch는 “Object-Oriented Analysis and Design with Applications”에서 복잡성을 제어하는 계층 구조란 아이디어를 소개합니다. 주4 그리고 이 계층 구조는 크게 둘로 나눌 수 있습니다.

  1. 타입을 계층화
  2. 행동을 계층화

타입을 계층화하는 건 우리가 생물을 분류하는 것과 유사합니다. 생물을 동물과 식물로 나누고, 이를 다시 동물계 - 척삭동물문 - 포유강 - 식육목 - 고양잇과로 나누는 거죠. 그리고 고양잇과고양이, 호랑이, 등이 포함됩니다. 주5

칡 짤
저는 “칡”입니다

이렇게 타입을 계층화하는 기법이 바로 “상속”이죠.

하지만 더 중요한 게 있습니다. 바로 행동을 계층화하는 거죠. 커다란 조직은 피라미드 형태로 계층화가 되고, 위에서부터 아래로 목표가 전달됩니다. 즉, “위임”을 하게 되는 거죠. 한마디로 우리는 위임을 통해 복잡성을 통제하게 됩니다.

그렇다면 이제 본질적인 질문으로 들어가 보겠습니다. 어떻게 계층 구조를 만들어야 효과적일까요?

Pyramid of Capitalist System

계층을 나누는 기준

컴퓨터 과학엔 “관심사의 분리”라는 오래된 설계 원칙이 있습니다. 주6 관심사를 나누는 방법은 여러 가지가 있지만, 우리는 다시 오래된 기준을 활용하겠습니다.

바로 “비즈니스 관심사”와 “기술 관심사”란 기준이죠. 흔히 “비즈니스 도메인과 UI를 섞지 않아야 한다”고 이야기하던 거고, 최근에는 “DB가 아니라 비즈니스 도메인을 중심으로 설계해야 한다”고 이야기하는 것이죠. 프로그래밍을 처음 배웠던 시절로 돌아가면, Input - Process - Output이란 틀에 맞춰서 코드를 작성하던 게 여기에 속한다고 볼 수도 있습니다.

이를 “Layered Architecture”라고 하고, 전통적으론 크게 3개의 계층으로 나누게 됩니다. 주7 주8

User-side
Application
Data-side

일반적인 웹 개발에서 User-side와 Data-side는 다음과 같습니다.

Web (User-side)
Application
DB (Data-side)

Flask와 SQLAlchemy를 통해 기술적 세부 사항을 추상화한다면 다음과 같이 표현할 수 있습니다.

Flask (User-side)
Application
SQLAlchemy (Data-side)

여기서 기술적 세부 사항을 다른 걸로 교체해 봅시다.

FastAPI (User-side)
Application
ormar (Data-side)

여기서 주목할 부분은, 기술적 세부 사항이 바뀐다고 해도 비즈니스 도메인을 다루는 Application Layer는 변함이 없다는 겁니다. Eric Evans는 “도메인 주도 설계”에서 비즈니스 도메인에 집중하는 객체들을 Domain Layer로 분리하고, Application Layer는 이를 위한 진입점으로 활용합니다. 주9 Uncle Bob은 클린 아키텍처에서 Application Layer에 명확히 Feature(Use Case)가 드러나게 하고, 이를 “소리치는 아키텍처”라고 설명합니다. 주10 주11

중요한 건 우리가 비즈니스 관심사와 기술적 관심사를 분리했다는 거고, 비즈니스 관심사를 자율적인 객체의 협력으로 구성할 수 있다는 겁니다.

어떻게 조립할까?

또봇 인티그레이션
또봇 X! Y! Z! 트라이탄! 인티그레이션!

계층 구조, 의존 관계를 코드로 표현하는 건 상당히 귀찮은 일입니다. 의존 관계를 손으로 적어주는 건 아주 명확하지만, 사실 그렇게 재밌는 일이 아니죠. 우리는 의존 관계를 알고 있고, 이를 자동으로 구성할 방법이 필요합니다.

그래서 많은 사람들이 IoC 컨테이너 기술을 개발했습니다. 이를 DI 컨테이너라고 부르기도 하는데, Martin Fowler가 프레임워크가 IoC를 말하는 걸 “내 자동차엔 바퀴가 있다”고 이야기하는 것과 마찬가지라고 하면서 “Dependency Injection(DI)”이란 용어를 제안했기 때문입니다. 주12

우리는 IoC 컨테이너를 통해 아주 쉽게 객체를 조립할 수 있습니다. 예를 들어 InjectorFlask-Injector를 사용한 코드를 보겠습니다. 주13 주14

User-side에서 Application Service에 대한 의존성을 이렇게 써줄 수 있습니다.

@app.post("/channels")
def create_channel(channel_service: ChanndelService):
    channel = channel_service.create_channel()
    return channel.to_dto(), HTTPStatus.CREATED

프로그램을 시작할 때, 다음과 같이 써주면 됩니다.

def create_app():
    from app import app

    injector = Injector()

    def configure(binder):
        # 여기서 DB 엔진 등에 대한 팩터리를 지정할 수 있습니다.
        pass

    # 여기서 Flask 엔드포인트에 대한 의존성을 파악하고 주입합니다.
    FlaskInjector(app=app, modules=[configure], injector=injector)

    return app

이제 테스트 코드는 다음과 같이 작성할 수 있습니다.

@pytest.fixture
def channel_service():
    channel_service = Mock()
    channel_service.create_channel = Mock(
        return_value=Channel(),
    )
    return channel_service


@pytest.fixture
def client(channel_service):
    def configure(binder):
        binder.bind(ChannelService, to=channel_service)

    FlaskInjector(app=app, modules=[configure])

    return app.test_client()


def test_create_channel(client):
    response = client.post("/channels")

    assert response.status_code == HTTPStatus.CREATED

    # 추가로 create_channel 메서드 호출 여부 등 다양한 테스트 가능.

우리는 User-side와 Application의 경계를 명확히 구분했습니다. 서로 다른 관심사가 섞이는 것을 막을 수 있고, 테스트도 더 쉽게 할 수 있습니다.

직사의 마안

마찬가지로 Data-side와 Application의 경계를 명확히 구분해 봅시다.

@inject
class ChannelService:
    channel_repository: ChannelRepository

    def __init__(self, channel_repository: ChannelRepository):
        self.channel_repository = channel_repository

    def create_channel(self) -> Channel:
        channel = Channel()

        self.channel_repository.save(channel)

        return channel

이런 순수한 파이썬 코드에 대해 테스트 코드를 작성하는 건 매우 쉽습니다.

@pytest.fixture
def channel_repository():
    channel_repository = Mock()
    channel_repository.save = Mock()
    return channel_repository


def channel_service(channel_repository):
    return ChannelService(channel_repository=channel_repository)


def test_create_channel(channel_service: ChannelService):
    channel = channel_repository.create_channel()

    assert channel is not None

    # 추가로 channel에 대한 제대로 된 테스트 가능.

결론

이제 우리는 기술적 이슈로부터 자유로운 Application Layer, 더 나아가 Domain Layer를 분리할 수 있게 되었습니다. Domain Layer는 순수한 파이썬 객체로 구성되므로, 평소 갈고 닦은 객체지향 프로그래밍 실력을 충분히 발휘할 수 있습니다. 이를 위해 테스트 코드를 활용하는 것도 매우 쉽습니다.

원프레딕트는 복잡한 비즈니스 문제를 더 나은 기술과 더 나은 협력을 통해 풀어낼 동료를 찾고 있습니다. 언제나 더 나은 방법이 있음을 믿고, 비즈니스와 코드를 조금씩 개선하는 일에 매진하고 싶은 분, 이를 위해 도메인 주도 설계(DDD), 이벤트 기반 아키텍처(EDA), 데이터 중심 애플리케이션 설계(DDIA) 등에 관심있는 분을 환영합니다.

원프레딕트 채용 정보 확인하기

Happy Cat


  1. 이 부분은 약간 오해할 수 있게 쓰였는데, 대중에 공개된 시기만 살펴보면 파이썬은 1991년, 자바는 1995년입니다.
  2. https://en.wikipedia.org/wiki/Procedural_programming
  3. https://martinfowler.com/bliki/TellDontAsk.html
  4. 번역서 제목은 “UML을 활용한 객체지향 분석 설계” http://aladin.kr/p/AGuQ
  5. 흔히 “고양이과”라고 하지만 표준어는 “고양잇과”입니다. 🐈
  6. https://en.wikipedia.org/wiki/Separation_of_concerns
  7. https://github.com/ahastudio/til/blob/main/architecture/layered-architecture.md
  8. 여기선 일반적인 구분과 다르게 User-side, Data-side란 표현을 썼는데, 이는 Alistair Cockburn이 Hexagonal Architexture를 설명할 때 사용한 표현입니다. https://alistair.cockburn.us/hexagonal-architecture/
  9. 위키북스에서 Eric Evans의 “도메인 주도 설계” 번역서에 실린 “LAYERED ARCHITECTURE” 패턴에 관한 부분을 웹에 공개했습니다. 정말 감사해요! https://wikibook.co.kr/article/layered-architecture/
  10. https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html
  11. https://blog.cleancoder.com/uncle-bob/2011/09/30/Screaming-Architecture.html
  12. Martin Fowler가 쓴 “Inversion of Control Containers and the Dependency Injection pattern”을 꼭 읽어보세요. https://www.martinfowler.com/articles/injection.html
  13. https://github.com/python-injector/injector
  14. https://github.com/python-injector/flask_injector
원프레딕트는 더 나은 제품을 고민하며 기술적인 문제를 함께 풀어낼 동료를 찾고 있습니다.
자세한 내용은 채용 사이트를 참고해 주세요.