Server System/Software Architecture

클린 아키텍쳐 vs 헥사고날 아키텍쳐 (3)

알파해커 테크노트 2024. 6. 9. 22:01
반응형

2024.06.03 - [Server System/Software Architecture] - 클린 아키텍쳐 vs 헥사고날 아키텍쳐 (1)

 

클린 아키텍쳐 vs 헥사고날 아키텍쳐 (1)

클린아키텍쳐를 처음 소개한 Bob Martin의 블로그(blog entry about Clean Architecture)에 다음과 같은 말이 나온다(https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html)."The diagram at the top of this article [

alphahackerhan.tistory.com

2024.06.05 - [Server System/Software Architecture] - 클린 아키텍쳐 vs 헥사고날 아키텍쳐 (2)

 

클린 아키텍쳐 vs 헥사고날 아키텍쳐 (2)

2024.06.03 - [Server System/Software Architecture] - 클린 아키텍쳐 vs 헥사고날 아키텍쳐 (1) 클린 아키텍쳐 vs 헥사고날 아키텍쳐 (1)클린아키텍쳐를 처음 소개한 Bob Martin의 블로그(blog entry about Clean Architectur

alphahackerhan.tistory.com

 

클린 아키텍쳐, 헥사고날 아키텍쳐의 공통의 목표와 의존성 방향에 대한 이해와 두 아키텍쳐의 개념적인 차이에 대해서 알아보고 싶으면 위 글을 참고하세요!

 


 

이번에는 두 아키텍쳐를 파이썬으로 구현한다면 각 요소를 어떻게 구현할 수 있는지에 대해 실제 코드 예제를 통해 살펴보려고 한다.

 

코드예제

헥사고날 아키텍쳐와 클린 아키텍쳐의 미묘한(?) 차이를 보여주기 위해, 도서관 애플리케이션을 만든다고 가정해보자.

도서관 애플리케이션의 기능에는 책을 추가하는 기능(add book)과 책을 찾는 기능(get book) 두 가지가 있다.

 

(1) 헥사고날 아키텍쳐를 활용한 구현

도서관 애플리케이션에서 엔티티라 할 수 있는 것은 책(Book)이다. 도메인 영역에 다음과 같이 Book 클래스를 만들 수 있다.

 

<<도메인>>

# book.py
class Book:
    def __init__(self, book_id, title, author):
        self.book_id = book_id
        self.title = title
        self.author = author

 

그 다음은 책에 대한 정보를 저장하고, 저장된 것을 불러오기 위한 기능을 만들어야 한다. 외부 요소와의 인터페이스 역할을 하는 “포트”를 다음과 같이 만들 수 있다.

 

<<포트>>

# book_repository_port.py
from abc import ABC, abstractmethod

class BookRepositoryPort(ABC):
    @abstractmethod
    def add_book(self, book):
        pass

    @abstractmethod
    def get_book(self, book_id):
        pass

 

포트를 통해 만든 인터페이스를 상속해서 실제 외부 요소의 구현체를 “어댑터”라는 개념으로 다음과 같이 구현할 수 있다.

 

<<어댑터>>

# in_memory_book_repository.py
from book_repository_port import BookRepositoryPort

class InMemoryBookRepository(BookRepositoryPort):
    def __init__(self):
        self.books = {}

    def add_book(self, book):
        self.books[book.book_id] = book

    def get_book(self, book_id):
        return self.books.get(book_id, None)

 

다음으로 책과 관련한 기능들(add book, get book)을 Service로 만들어서, 사용자 인터페이스에서 해당 서비스를 통해 도서관 애플리케이션 기능을 이용할 수 있도록 만들 수 있다.

 

<<어플리케이션 서비스>>

# book_service.py
from book_repository_port import BookRepositoryPort

class BookService:
    def __init__(self, book_repository: BookRepositoryPort):
        self.book_repository = book_repository

    def add_book(self, book):
        self.book_repository.add_book(book)

    def get_book(self, book_id):
        return self.book_repository.get_book(book_id)

 

마지막으로, 사용자의 입력이 최초로 닿는 사용자 인터페이스를 다음과 같이 만들 수 있다. 이곳에서 사용자의 요청을 실제 도메인 영역으로 보내 처리될 수 있도록 한다. 또 외부 요소의 실제 구현체를 인스턴스화 해서, 인터페이스에 의존성 주입을 하는 곳도 이곳이다.

 

<<사용자 인터페이스>>

# main.py
from book import Book
from book_service import BookService
from in_memory_book_repository import InMemoryBookRepository

# Initialize repository and service
book_repository = InMemoryBookRepository()
book_service = BookService(book_repository)

# Add a book
book1 = Book(1, "Clean Code", "Robert C. Martin")
book_service.add_book(book1)

# Retrieve the book
retrieved_book = book_service.get_book(1)
print(f"Retrieved Book: {retrieved_book.title}, Author: {retrieved_book.author}")

 

헥사고날 아키텍쳐에서는 포트와 어댑터라는 개념을 이용해서 외부 요소와의 인터페이스와 외부 요소의 구현체를 표현할 수 있었다. 그리고 사용자 요청을 받아 비즈니스 로직을 실제로 수행하는 부분을 서비스라는 개념을 이용해서 구현했다.

 

이제 클린 아키텍쳐에서는 위 개념들을 어떻게 표현하는지 살펴보자.

 

(2) 클린 아키텍쳐를 활용한 구현

헥사고날 아키텍쳐에서와 마찬가지로 도서관 애플리케이션을 구성하는 가장 기본 단위는 Book이라고 할 수 있고, 그것이 엔티티가 될 것이다.

 

<<엔티티>>

# entities.py
class Book:
    def __init__(self, book_id, title, author):
        self.book_id = book_id
        self.title = title
        self.author = author

 

헥사고날 아키텍쳐에서 BookService라는 서비스를 만들어서, 책과 관련한 기능들을 그곳에 다 정의한 것과는 다르게, 클린 아키텍쳐에서는 책 추가(add book), 책 찾기(get book)와 같은 사용자의 각 요청에 대응되는 유즈케이스 클래스(AddBook, GetBook)를 만들어 각 클래스에서 비즈니스 로직이 수행되도록 하는 것이 차이점이다.

 

<<유즈케이스>>

# use_cases.py
class AddBook:
    def __init__(self, book_repository):
        self.book_repository = book_repository

    def execute(self, book):
        self.book_repository.add(book)

class GetBook:
    def __init__(self, book_repository):
        self.book_repository = book_repository

    def execute(self, book_id):
        return self.book_repository.get(book_id)

 

헥사고날 아키텍쳐에서 포트와 어댑터는 클린 아키텍쳐에서 인터페이스 어댑터(Interface adapter)라는 layer에서 표현되며, 인터페이스(BookRepository)와 그 인터페이스를 상속해 외부 요소의 구현체(InMemoryBookRepository)를 구현하는 것은 동일하다.

 

<<인터페이스 어댑터>>

# interface_adapters.py
from abc import ABC, abstractmethod

class BookRepository(ABC):
    @abstractmethod
    def add(self, book):
        pass

    @abstractmethod
    def get(self, book_id):
        pass

class InMemoryBookRepository(BookRepository):
    def __init__(self):
        self.books = {}

    def add(self, book):
        self.books[book.book_id] = book

    def get(self, book_id):
        return self.books.get(book_id, None)

 

마지막으로 사용자의 요청을 받는 곳이 프레임워크와 드라이버 레이어에서 표현될 수 있다. 헥사고날에서와 마찬가지로 이 곳에서 사용자의 요청을 받아 도메인 영역에 해당하는 유즈케이스 레이어로 요청을 보내 비즈니스 로직이 수행되도록 한다. 또한 이곳에서 외부 요소의 구현체를 인스턴스화 해서 의존성 주입을 한다.

 

<<프레임워크와 드라이버>>

# main.py
from entities import Book
from use_cases import AddBook, GetBook
from interface_adapters import InMemoryBookRepository

# Initialize repository and use cases
book_repository = InMemoryBookRepository()
add_book_use_case = AddBook(book_repository)
get_book_use_case = GetBook(book_repository)

# Add a book
book1 = Book(1, "Clean Code", "Robert C. Martin")
add_book_use_case.execute(book1)

# Retrieve the book
retrieved_book = get_book_use_case.execute(1)
print(f"Retrieved Book: {retrieved_book.title}, Author: {retrieved_book.author}")

 

이러한 차이가 있다고 볼 수 있지만, 개인적으로 이런 차이 구분은 크게 의미를 가지지 않는다고 보는 편이다. 앞서 두 아키텍쳐가 공통적으로 가지는 주요한 목표에서 얘기했듯이 외부 요소와 도메인 로직을 구분하고, 의존성 관리를 함으로써 코드 변경과 테스트에 용이하도록 만드는 것이다.

 

 

반응형