FastAPI Clean Architecture

clean architecture

신입 시절을 지나면서 어떻게 하면 ‘돌아가는’ 코드를 만들 수 있는지에서 서서히 ‘좋은’ 코드를 만드는 방법에 대한 물음으로 관심이 옮겨가고 있다. ‘좋은 코드’란 무엇일까? 여기에 대해서 많은 개발자들이 의견을 내고 있지만 정석적으로는 클린 아키텍처를 따르고, 클린 코드를 만드는 것 이라는 답변이 주를 이루고 있다. 결국에는 지속 가능한 코드를 구현하고 유지보수에 드는 비용을 줄이는 방법에 대해서 고민하는 것이 좋은 코드를 만드는 것 같다.
나는 백엔드 서버를 구축하는게 주된 직무가 아니었기 때문에 이번에 클린 아키텍처의 기본 개념과 각 계층의 역할을 학습하고 정리해보려고 한다.

클린 아키텍처의 규칙

클린 아키텍처의 규칙을 간단히 말하자면 의존성 규칙이다. 모든 소스코드의 의존성은 바깥에서 안쪽으로만 향해야하며, 안쪽 레이어는 바깥쪽 레이어에 대해 아무것도 알지 못해야한다.

1. 도메인 (entity) 계층

도메인이란 애플리케이션이 해결하고자하는 특정한 주제나 분야를 의미한다. 도메인 모델은 이 비즈니스 영역의 핵심 개념, 엔티티, 관계, 그리고 반드시 지켜야하는 규칙과 상태를 나타낸다.

엔티티는 이 비즈니스 도메인의 실제 개념이나 객체를 표현하는 것으로, 독립적으로 존재하며 비즈니스 도메인 그 자체에 집중한다. 가장 중요한 점은, 이 도메인 계층은 데이터베이스 스키마나 ui 같은 외부 시스템에 대한 의존성을 가져서는 안된다는 점이다. 오직 엔티티의 상태가 바뀜에 따라 도메인 규칙이 제대로 동작하는 지만 검증하면 된다.

  • 여러 엔티티는 같은 계층에 있기 때문에 직접 상호작용할 수 있다. = 엔티티를 구현한 어떤 파이썬 클래스가 다른 클래스를 직접 인스턴스로 만들어 해당 메서드를 호출할 수 있다.
  • 엔티티는 외부 계층에 있는 것은 아무것도 알지 못한다. = 데이터베이스를 직접 호출하거나, 프레젠테이션 프레임워크에서 제공하는 메서드에 접근할 수 없다.
  • 엔티티의 상태가 변경되면 엔티티를 저장하고 있는 데이터베이스가 변경되어야한다. 하지만 도메인은 데이터베이스를 직접 호출할 수 없으므로, 이 문제는 보통 이벤트 프로그래밍을 사용해 도메인 이벤트 발송 -> 외부 모듈이 이벤트를 받아 처리 하는 방식으로 해결한다. (도메인 주도 설계 방법론을 참고할것)

2. 애플리케이션(use case) 계층

애플리케이션이 해결하고자하는 문제와 나름의 해결방법을 유스케이스로 정의하고 구현한다. 이 계층은 여러 서비스로 이루어진다.

  • 서비스는 특정 도메인을 다루는 도메인 서비스, 특정 작업을 수행하는 애플리케이션 서비스, 인프라스트럭처와 상호작용하는 인프라 서비스 등 다양한 종류로 나눌 수 있다.
  • 애플리케이션 계층의 모듈은 도메인 계층에 대한 접근 권한이 있다. 즉 도메인 객체를 직접 인스턴스로 만들어 사용할 수 있다.
  • 서비스는 서로 호출할 수 있다.

3. 인프라 스트럭처 (프레임워크 및 드라이버) 계층

내부 시스템이 사용하고자 하는 외부 시스템을 다룬다.

  • 외부 프레임워크와 라이브러리, api 등을 활용해 실제로 사용자와 시스템 사이의 상호작용을 구현한다.
  • 내부에서 제공하는 인터페이스를 구현하는 구현체들로 이루어진다.

4. 인터페이스 (interface adapter) 계층

인프라 계층의 시스템(외부에서 만든 도구나 프레임워크 또는 데이터베이스와 같은 시스템)과 내부의 애플리케이션 및 도메인 계층을 연결하는 역할을 한다.

주요 특징

  • 외부와 내부 데이터 사이의 데이터 변환 외부 시스템이 사용하는 특정 형식의 데이터를 내부 시스템의 형식으로 맞춘다
  • 인터페이스 구현 외부시스템에서 제공하는 api나 다양한 데이터베이스와 웹서비스 등의 시스템과의 연결을 구현
  • 외부 종속성의 분리 외부 요구사항에 대한 종속성을 분리하는 역할을 한다.

인터페이스 계층에는 컨트롤러, 게이트웨이, 프레젠터가 사용된다.

  • 컨트롤러 : ui를 통해 전달된 사용자의 입력과 요청을 내부로 전달, 웹 애플리케이션에서는 Http 요청을 처리하고, 그와 관련된 데이터를 다룬다.
  • 게이트웨이 : 외부 데이터 소스와의 통신을 담당한다. 외부 db, api, 파일 시스템 등과의 상호작용을 처리한다. 내부 유스케이스와 외부 데이터 소스 간의 중간 역할을 수행하며, 외부 데이터를 내부 시스템에서 사용할 수 있는 형식으로 변환한다.
  • 프레젠터 : 내부 유스케이스가 처리한 데이터를 전달받아 사용자가 볼 수 있는 형태로 가공해 사용자 인터페이스로 전달. 일반적으로 웹 애플리케이션에서 템플릿을 구성하거나 json, html 등의 데이터를 사용자 인터페이스에 맞게 구성하는 역할을 수행한다.

의존 관계 역전 원칙

계층을 나누는 기준은 간단하다. 단순히 지금 구현하고자하는 사항이 내부의 비즈니스 로직에 관련된 일인지 / 외부의 시스템을 이용하는 것인지 / 그와의 인터페이스를 다루는 것인지 만 구분해서 판단하면 된다.

이때 계층 간에는 의존성이 발생하는데, 클린 아키텍처의 핵심은 의존성의 방향이다.

고수준의 구성요소(즉 엔티티와 더 가까운 쪽)이 저수준의 형식에 의존하면 안된다.

즉 안쪽 계층의 구성요소가 바깥쪽 계층의 구성요소를 사용하고자 한다면, 인터페이스를 이용하고 그 인터페이스의 구현체를 외부의 계층에 둬야 한다.

애플리케이션 계층의 UserService가 유저 정보를 DB에 저장해야 한다고 가정해 보자.

  1. 인터페이스 정의 (안쪽): 먼저 애플리케이션 계층에 UserRepository라는 ‘인터페이스(추상 클래스)’를 정의한다. 여기에는 save(user)라는 메서드 명세만 있다.
  2. 서비스 구현 (안쪽): UserService는 구체적인 DB 기술이 아닌, 오직 UserRepository라는 인터페이스에만 의존해서 user_repo.save(user) 코드를 작성한다.
  3. 구현체 작성 (바깥쪽): 인프라 계층에서 UserRepository 인터페이스를 상속받아, PostgreSQLUserRepository라는 실제 구현 클래스를 만든다. 이 클래스의 save 메서드 안에 실제 ORM 코드나 DB 접속 코드가 들어간다.

이렇게 하면, UserService는 DB가 PostgreSQL인지 MySQL인지 전혀 모르지만 DB 저장 기능을 사용할 수 있게 된다. 의존성의 방향이 애플리케이션 -> 인프라가 아니라, 인프라 -> 애플리케이션으로 역전된 것이다.