긴 코드를 읽기 전에 구조가 어떻게 되어있는지 알기 위해 찾아본 내용들을 정리
처음 배우는 내용들이라 틀린 내용이 있을 수 있으므로 추후 수정 필요.
SOLID란 로버트 C. 마틴이 2000년대 초반에 명명한 객체 지향 프로그래밍 및 설계의 다섯 가지 기본 원칙🔗이다.
참고
- Controller (컨트롤러)
클라이언트 요청을 처리하는 역할 (HTTP 요청을 받고, 응답을 반환)
비즈니스 로직을 직접 처리하지 않고 Service(서비스)에 위임
주로 API 엔드포인트를 정의- Service (서비스)
비즈니스 로직을 처리하는 핵심 계층
DB에서 데이터를 가져오거나 가공하는 작업 수행
컨트롤러에서 요청한 데이터를 조회, 수정, 삭제하는 역할- Interface (인터페이스)
객체의 형태(Shape)를 정의하는 역할
Port로도 표현됨
의존관계 역전 원칙DIP은 소프트웨어 모듈들을 분리하는 특정한 방식을 지칭한다.
전통적으로 모듈들은 상위 계층이(정책 결정)이 하위 계층(세부 사항)에 의존하는 관계를 가졌다.
이 경우 리팩토링 등의 이유로 저수준 모듈을 수정하는 경우 고수준 모듈도 같이 수정해줘야 할 가능성이 높다.
DIP는 이런 전통적인 의존관계를 역전(하위 계층 또한 추상화에 의존하게 함)시켜 상위 계층이 하위 계층의 구현으로부터 독립되게 한다.
이 원칙은 다음과 같은 내용을 담고 있다.
요약하면 상위와 하위 객체 모두가 동일한 추상화에 의존해야 한다는 객체 지향적 설계의 대원칙을 제공한다.
아래 예시에서 컨트롤러는 서비스에 의존하고 있다.
Controller -> Service
# 저수준 모듈 (구체적인 서비스)
class UserService:
def get_user(self, user_id: int) -> str:
return f"User {user_id} data"
# 고수준 모듈 (컨트롤러)
class UserController:
def __init__(self):
self.service = UserService() # ⚠ 직접 특정 서비스 구현체를 생성해서 의존함
def get_user(self, user_id: int):
return self.service.get_user(user_id)
# 실행
controller = UserController()
print(controller.get_user(1)) # "User 1 data"
아래 예시에서 고수준, 저수준 모듈이 모두 UserServicePort
라는 추상화(인터페이스, Port)에 의존하고 있다.
Controller -> Port <-(역전) Service
from typing import Protocol
# 1️⃣ Port 역할 (추상화, 인터페이스)
class UserServicePort(Protocol):
def get_user(self, user_id: int) -> str:
pass
# 2️⃣ 저수준 모듈 (구체적인 서비스)
class BasicUserService:
def get_user(self, user_id: int) -> str:
return f"Basic User {user_id} data"
class PremiumUserService:
def get_user(self, user_id: int) -> str:
return f"Premium User {user_id} data"
# 3️⃣ 고수준 모듈 (컨트롤러)
class UserController:
def __init__(self, service: UserServicePort): # 인터페이스(추상화)에 의존
self.service = service
def get_user(self, user_id: int):
return self.service.get_user(user_id)
# 실행
basic_controller = UserController(BasicUserService())
print(basic_controller.get_user(1)) # "Basic User 1 data"
premium_controller = UserController(PremiumUserService())
print(premium_controller.get_user(1)) # "Premium User 1 data"
이렇게 하면 컨트롤러가 특정 서비스의 구현에 의존하지 않으며 서비스 로직을 변경하더라도 컨트롤러 수정이 필요가 없다.
또한 Mocking이 쉬워져 단위 테스트가 편리해진다.
DIP를 구현하는 방식 중 하나로 객체 내부에서 직접 의존성을 생성하는 대신, 외부에서 주입받는 방식을 말한다.
위의 코드에서 컨트롤러 인스턴스를 생성할 때 서비스 인스턴스를 인자로 전달하는 부분UserController(BasicUserService())
이 의존성 주입(Dependency Injection)이다.
NestJS는 의존성 주입 컨테이너(DI Container)를 사용해서 객체 생성을 자동으로 관리한다.
따라서 직접new UserService()
와 같이 객체를 직접 생성해주지 않아도 자동으로 재사용한다.
https://medium.com/ssense-tech/hexagonal-architecture-there-are-always-two-sides-to-every-story-bc0780ed7d9c
Hexagonal Architecture(Ports & Adapters 아키텍처)는 알리스테어 코번(Alistair Cockburn)이 제안한 소프트웨어 설계 방식이다.
이 아키텍처의 핵심은 의존성 역전 원칙을 강하게 적용하는 것으로, 애플리케이션의 핵심 로직(애플리케이션 코어. 육각형으로 표현됨)을 모든 외부 시스템(데이터베이스, UI, 외부 API 등)과 분리하여 테스트 용이성과 유지보수성을 높이는 데 있다.
DI를 설명할 때의 예시에서 외부 사용자가 기능을 조작할 때(그림의 Driving side) Controller-Port-Service 계층을 통해 작동했던 것과 같이 Service에서 DB와의 통신 등을 할 때(즉 외부 인프라에 접근할 때, 그림의 Driven side)에도 Port와 Adapter를 거치도록 하여 애플리케이션 코어를 외부와 강하게 격리시키는 것이 핵심 아이디어이다.
왼쪽에서 오른쪽으로 흐르며 입력 포트는 서비스가 구현하며, 출력 포트는 출력 어댑터가 구현한다.
출력 포트를 사용한 외부 인프라로의 접근은 핵심 비즈니스 로직이 무관할 뿐더러, 코어(육각형)는 외부에 대한 정보가 없어야한다는 핵심 아이디어를 생각하면 당연하다.
├adapter
│├in
││└controller
│└out
│ └repository
├port
│├in
││└UserServiceInterface
│└out
│ └RepositoryInterface
├service
│└UserService
├domain
└DomainEntity
기존의 개발 방식과 비교하여 설명
기존의 방식에서는 요구사항을 기능 단위로 나누어 구현하였다.
데이터베이스 설계를 먼저 하고, 이를 기반으로 애플리케이션을 개발되는 것이 보통.
Controller, Service, Repository와 같은 계층구조는 전통적 개발 방식에도 존재
개발자역시 도메인을 이해하고 이를 통해 구현
도메인 전문가와 개발자가 도메인 지식을 공유하여 의사소통과 코드 작성에 일관성을 유지
복잡한 도메인을 작은 단위로 나누어 정의한 명확한 컨텍스트(Bounded Context)에 기초해 개발
DDD는 복잡한 비즈니스 로직을 가진 프로젝트에 적합하며, 전통적 개발 방식은 단순한 애플리케이션에 더 적합