https://youtu.be/C7MRkqP5NRI?si=NHG-qDp1cweFg7ue
이 글은 위의 영상을 정리하기 위해 필자의 개인적인 생각과 함께 작성된 글입니다.
소프트웨어 아키텍처를 설계하고 이를 개발해 나갈 때 아래와 같은 고민을 하게 된다.
"어떻게 하면 변화에 강하고 확장 가능한 시스템을 만들 수 있을까?"
이에 대한 답변으로 영상에서는 Clean Architecture를 위한 "The Golden Rule"을 제시한다. 이는 의존성 역전 원칙(Dependency Inversion Principle, DIP)을 바탕으로 시스템을 내부와 외부로 명확히 구분하여 유연성과 확장성을 극대화해야 한다는 원칙을 담고 있다고 생각한다.
Clean Architecture
우선, Clean Architecture를 이루어내기 위해서 Clean Architecture가 무엇인지 이해하는 것이 필요하다. 영상에서는 Clean Architecture를 아래와 같이 설명하였다.
프로그램을 이루는 각 구성요소에 대해 그것이 무엇이고, 어디에 있으며, 왜 있는지 명확하게 아는 것
이는 위 그림처럼 entity, usecase, gateways, external systems 등 다양한 구성 요소들이 명확한 계층 구조를 가져야 한다는 것이다. 이를 위해선, 자신이 속한 레이어의 바깥에 위치하는 것에 직접적인 접근을 피하고, 계층 간의 dependency를 이용한 연결이 이뤄져야 한다.
The Golden Rule
영상에서는 Clean Architecture의 핵심인 The Golden Rule을 아래와 같이 두 문장으로 설명한다.
Talk inward with simple structures, Talk outward through interface
각 원칙을 아래와 같이 필자의 말로 풀어서 설명해 보았다.
- "Talk inward with simple structures"
내부 계층으로 데이터를 전달할 때는 간단한 구조/모델을 사용해 내부 로직의 순수성을 유지해야 한다. (ex. HTTP 요청을 웹 프레임워크는 Request 객체로 변환하고, 엔드포인트 함수는 내부 UseCase로 내부에 정의된 단순한 DTO나 타입을 이용하여 넘겨준다. - "Talk outward through interface"
외부 시스템(ex. 데이터베이스, API)과 통신할 때 구체적인 구현체가 아닌 추상화(인터페이스)를 통해 의존성을 역전시켜야 한다.
의존성 역전 원칙이란?
위에서 "Talk outward through interface" 원칙에서 "추상화를 통해 의존성을 역전"시켜야 한다고 했는데, 그렇다면 의존성 역전이란 무엇일까?
의존성 역전 원칙은 객체 지향 설계 원칙 중 하나로, 고수준 모듈(High-level Module)이 저수준 모듈(Low-level Module)의 구체적인 구현에 의존하지 않고, 추상화(Interface)를 통해 서로를 연결해야 한다는 원칙이다. 이를 통해 다음과 같은 유연성과 확장성을 얻을 수 있다.
1. 유연성
- 하위 모듈의 특정 구현체에 종속되지 않으므로 변화에 강한 코드를 작성할 수 있다.
예를 들어, 저장소를 In-memory에서 MongoDB로 교체하더라도 비즈니스 로직은 전혀 변경할 필요가 없다. - 의존성 주입을 활용하여 런타임에 필요한 구현체를 동적으로 주입할 수 있기에 다양한 상황에 유연하게 대응할 수 있다.
2. 확장성
- 새로운 기능을 추가할 때 기존 코드를 변경하지 않고도 쉽게 확장할 수 있다. -> 개방-폐쇄 원칙(Open-Closed Principle, OCP)
예를 들어, 기존 결제 인터페이스를 구현하는 새로운 결제 방식을 추가하더라도, 다른 코드의 변경 없이 시스템 전체가 이를 지원할 수 있다.
이제 실제 코드를 통해 The Golden Rule을 적용한 Clean Architecture를 살펴보자.
The Golden Rule을 적용한 결제 시스템 예시
결제 서비스에서 특정 결제 수단(KakaoPay)을 추가한다고 가정해 보자. 앞서 살펴본 것처럼 코드는 아래의 두 가지 사항을 지키며 작성하였다.
- 단순한 구조를 사용하여 비즈니스 로직의 순수성을 유지해야 한다.
- 외부 시스템의 인터페이스를 통해 의존 관계를 역전하여야 한다.
요구사항
- 사용자는 여러 결제 수단(예: 신용카드, 카카오페이)을 통해 결제할 수 있어야 한다.
- 사용자가 결제를 요청하면 이벤트 기반으로 결제 프로세스가 실행된다.
- 새로운 결제 수단이 추가되더라도 기존 코드에 변경이 없어야 한다.
- 결제 요청을 처리할 때는 간단한 데이터 구조를 사용해야 한다.
- 결제 요청은 외부 결제 시스템과의 통신을 위해 인터페이스를 통해 이루어져야 한다.
- 결제 처리 로직은 DB, 외부 API 등의 외부 시스템과 독립적으로 구현되어야 한다.
1. Talk inward with simple structures
app = FastAPI()
def get_payment_usecase():
return PaymentUseCase(CreditCardPaymentGateway(), KafkaEventPublisher())
class PaymentRequest(BaseModel):
user_id: str
amount: float
@app.post("/payment")
def process_payment(request: PaymentRequest, usecase: PaymentUseCase = Depends(get_payment_usecase)):
usecase.process_payment(request)
return {"message": "Payment processing started"}
process_payment 엔드포인트 함수는 단순한 구조인 PaymentRequest 모델을 통해 내부 PaymentUseCase로 데이터를 전달한다.
2. Talk outward through interface
# PaymentProcessor
class PaymentProcessor(ABC):
@abstractmethod
def process(self, user_id: str, amount: float):
pass
class CreditCardProcessor(PaymentProcessor):
def process(self, user_id: str, amount: float):
print(f"Processing credit card payment for {user_id}, amount: {amount}")
# EventPublisher
class EventPublisher(ABC):
@abstractmethod
def publish_event(self, event_type: str, data: dict):
pass
class KafkaEventPublisher(EventPublisher):
def publish_event(self, event_type: str, data: dict):
print(f"Publishing event {event_type} with data {data}")
# PaymentUseCase
class PaymentUseCase:
def __init__(self, processor: PaymentProcessor, event_publisher: EventPublisher):
self.processor = processor
self.event_publisher = event_publisher
def process_payment(self, request: PaymentRequest):
self.processor.process(request.user_id, request.amount)
self.event_publisher.publish("payment_completed", request.dict())
PaymentProcessor 인터페이스를 정의하고, CreditCardProcessor는 이를 구현한다. 새로운 결제 수단을 추가할 때 기존 코드 수정 없이 새로운 클래스를 추가하면 된다.
마찬가지로 EventPublisher 인터페이스를 정의하고, KafkaEventPublisher로 이를 구현한다. 새로운 메시지 큐가 추가하기 위해서도 기존 코드 수정할 필요 없이 새로운 클래스를 추가하면 된다.
마지막으로, PaymentUseCase에 DI를 적용하여 PaymentProcessor와 event_publisher를 주입받도록 하였다. 이렇게 하면 결제 수단 및 이벤트 처리 방식을 쉽게 확장 가능하다.
3. 신규 결제 수단 추가 시
앞서 말한 대로 의존성 역전 원칙으로 Clean Architecture를 설계하면 기능 추가 시에 기존 코드의 수정 없이 확장을 진행할 수 있다.
실제로 신규 결제 수단으로 카카오페이를 추가할 때, 기존 코드의 수정 없이 진행되는지 확인해 보자.
class KakaoPayProcessor(PayProcessor):
def process_payment(self, user_id: str, amount: float):
print(f"Processing KakaoPay payment for {user_id}, amount: {amount}")
위와 같이 카카오페이의 결제를 처리하는 클래스를 작성한다.
이제 PaymentUseCase의 PayProcessor로써 KakaoPayProcessor를 주입하기만 하면 기존 비즈니스 로직의 수정 없이 신용카드가 아닌 카카오페이 결제 기능을 지원할 수 있다.
4. 새로운 EventPublisher 추가 시
이번에는 새로운 메시지 브로커로 RabbitMQ를 추가할 때를 살펴보자.
class RabbitMQEventPublisher(EventPublisher):
def publish_event(self, event_type: str, data: dict):
print(f"Publishing event {event_type} with RabbitMQ: {event_type}, {data}")
위와 같이 RabbitMQ를 EventPublisher로 사용하는 클래스를 작성한다.
이제 PaymentUseCase의 EventPublisher로써 RabbitMQEventPublisher를 주입하기만 하면 기존 결제 처리 로직의 수정 없이 RabbitMQ로 event를 발생시킬 수 있다.
5. 왜 의존성 역전 원칙(DIP)이 중요한가?
위의 예시를 통해서 간단하게 왜 의존성 역전 원칙을 지키는 Architecture를 구축하는 것이 중요한지 정리해 보겠다.
- 변화에 강한 코드
새로운 결제 수단(KakaoPay)을 추가하거나 새로운 메시지 브로커(RabbitMQ)를 도입할 때 기존의 PaymentUseCase나 API 엔드포인트 함수를 변경할 필요가 없다. 이로 인해 코드 변경이 특정 부분에 국한되며, 전체 시스템이 예상치 못한 오류로 인해 무너질 가능성이 낮아진다. - 테스트 용이성
인터페이스를 사용하여 의존성을 주입하면, 실제 구현체를 사용하지 않고 테스트용 목(mock) 객체를 활용할 수 있다. 예를 들어, PaymentProcessor와 EventPublisher의 가짜(Mock) 구현체를 만들어서 단위 테스트를 수행하면, 실제 결제 시스템이나 메시지 브로커와 연결하지 않고도 결제 로직이 올바르게 동작하는지 검증할 수 있다. - 확장성 확보
의존성을 인터페이스를 통해 추상화하면 시스템이 변화에 유연하게 대응할 수 있다. 새로운 결제 수단, 메시지 브로커가 추가되는 확장이 필요하더라도 기존 로직의 수정 없이 단지 새로운 구현체를 추가하고 의존성을 주입하면 되기에 확장성이 뛰어난 시스템을 구축할 수 있다.
마무리
The Golden Rule은 개발자들이 소프트웨어를 설계할 때 유연성과 확장성을 극대화하는 강력한 가이드라인이 될 수 있다고 생각한다. 의존성 역전 원칙을 기반으로 하여 추상화와 단순화를 실천한다면, 변화에 강하고 유지보수하기 쉬운 Clean Architecture를 갖춘 시스템을 설계할 수 있을 것이다. 이를 꾸준하게 학습하고, 또 실무에 적용한다면 변화에 흔들리지 않는 강력한 코드를 작성하는 개발자가 될 수 있을 것이다.
'개발 > 개발 팁' 카테고리의 다른 글
Github autoSetupRemote 설정 (0) | 2025.01.17 |
---|---|
Docker 환경에서의 Spring Boot Auto Reload (Hot Reload) (0) | 2024.11.30 |
SSAFY 11기 전공자 합격 후기 (면접 스터디 X) (0) | 2024.01.13 |
페어 프로그래밍(Pair Programing) (0) | 2023.10.02 |
[부스트캠프 웹・모바일 8기] 네이버 부스트캠프 챌린지 회고 (3) | 2023.08.08 |