레이어드 아키텍처로 생각하면
소프트웨어 아키텍처란
아키텍처
: 어떤 대상의 구성과 동작 원리 , 구성 요소간의 관계 및 시스템 외부 환경과의 관계를 설명하는 하나의 설명서
ex) 시스템 아키텍처는 시스템이 전반적으로 어떻게 구성되어있는지, 각각의 요소들은 무엇이 있는지에 대해 설명한다.
ex ) 소프트웨어 아키텍처 : 소프트웨어 구성요소들 사이 관계를 표현
레이어드 아키텍처
관심사가 같은 코드들을 계층으로 그룹화
레이어드 아키텍처 특징
1. 계층화로 인한 분리된 책임
2. 편의에 따라 여러 계층을 추가 가능하다
Ex) presentation, application, domain, persistence
3. 구조가 쉽고 단순하고 익숙하다
4. 데이터베이스 주도 설계가 될 수 있다.
레이어드 아키텍처 흐름
상위계층이 하위계층을 의존하게된다.
상위계층이 하위계층을 의존하기 때문에
하위계층의 변화가 발생하면 해당 계층을 의존하고있는 상위계층에 영향을 주게된다.
이렇게되면 하위계층의 변화는 상위계층의 수정을 야기할 수 있고 테스트또한 쉽지않다.
[ 비즈니스 로직을 검증하기위해 데이터가 필요할 수 있다. == 상위계층 테스트를 위해 하위계층이 필요할 수 있다. ]
예를들어 persistence Layer의 변화는 application Layer 의 변화를 야기하고
application Layer 의 변화는 presentation Layer 의 변화를 야기하므로
한 계층의 변화는 프로젝트 전체에 영향을 끼칠수있는 상황이다.
[ 이런 상황은 레이어드 아키텍처를 사용할 시 프로젝트 전체가 DB에 의존하게 되기 때문에 발생한다. ]
이런 상황을 해결하기 위해 클린 아키텍처를 알아보자
[ 프로젝트 전체가 DB에 의존하지 않도록 구성하면 되지않을까]
클린 아키텍처에서는 의존성 역전을 통해서
프로젝트 전체가 DB가 아닌 도메인에 의존하게 하는 방법으로 이런 문제를 해결한다.
개선된 레이어드 아키텍처 구조
레이어드 아키텍처에서 인터페이스를 사용하여
상위 계층이 하위계층의 인터페이스를 의존하게 된다면
결합도가 낮아져서 하위계층의 변경이 상위계층에 미치는 영향을 최소하는 방법도 존재한다.
이점
- 유연성 증가: 하위 계층의 구현을 쉽게 변경하거나 교체할 수 있습니다.
- 테스트 용이성: 목(mock) 객체를 사용하여 각 계층을 독립적으로 테스트할 수 있습니다.
- 확장성: 새로운 구현을 쉽게 추가할 수 있습니다.
클린 아키텍처
클린아키텍처 또한 레이어드 아키텍처 처럼 의존성 방향이 안쪽으로만 향한다 [ 한 방향으로만 진행한다. ]
클린 아키텍처 특징
1. 핵심 규칙을 담고 있는 도메인이 중심
2. 도메인이 세부 사항에 의존하지 않는다.
- 세부사항 또는 외부요소란 우리가 사용하는 라이브러리나 DB등을 의미한다.
ex) 세부사항 예시 -> 입출력 장치, 디비 ,웹시스템, 서버 ,프레임워크, 통신 프로토콜 등이 있다.
ex) 웹 어플리케이션이라면 스프링과 Mysql같은 코드를 제외한 모든부분이 세부사항 또는 외부요소라고 볼 수 있다.
[ "스프링과 MySQL 같은 코드를 제외한 모든 부분이 세부 사항"이라는 표현은 다소 모호하게 해석될 여지가 있습니다. 정확히 말하면, 스프링과 MySQL 역시 세부 사항에 해당합니다.
아마도 그 표현은 "스프링과 MySQL 같은 특정 프레임워크와 데이터베이스가 도입된다고 해도, 핵심 도메인 로직은 그 외부 기술에 의존하지 않도록 설계해야 한다"는 의미로 이해할 수 있습니다. 즉, 스프링이나 MySQL을 사용하더라도 도메인 계층은 그들에 종속되지 않도록 구조화하라는 강조점이 있었을 가능성이 큽니다.
다시 정리하면, 클린 아키텍처에서는 스프링이나 MySQL 역시 외부 요소로 간주됩니다. 도메인 로직은 이들 외부 요소에 의존하지 않고 독립적으로 설계되어야 합니다.]
3. 익숙하지 않을 수 있고 레퍼런스가 작다.
클린 아키텍처 레이어
1. 엔티티 레이어
가장 핵심적인 비즈니스 로직이나 규칙을 담고 있는 레이어이다.
대부분의 프로젝트에서 도메인 패키지에 들어있는 것들이라고 보면된다.
외부요소에 대한 어떠한 의존성도 가지고 있으면 안된다.
2. 유즈 케이스 레이어
비즈니스 로직을 포함하고 있고 보통 Repository에서 객체를 받아와서 특정한 행위를 하고 업데이트 하는 부분을 처리하고 있다.
유즈케이스 : 유스케이스는 시스템이 사용자나 다른 시스템(액터)의 요구를 충족시키기 위해 수행하는 일련의 행동이나 단계를 설명합니다.
3. 어댑터 레이어
데이터를 Use Case에서 사용하는 형태로 변환하거나 내부에서 사용하는 형태를 외부에 적합한 형태로 변환한다.
4. 인프라스트럭처 레이어
DB와 프레임워크 같은 외부와 통신 작업을 해주는것들이다.
클린 아키텍처에서 인프라스트럭처 레이어는 애플리케이션의 핵심 비즈니스 로직을 지원하기 위해 외부 시스템과 상호작용하는 세부 사항들을 포함하는 계층입니다. 인프라스트럭처 레이어는 데이터베이스, 네트워크, 메시지 브로커, 프레임워크 등 애플리케이션의 도메인 및 유스케이스 계층이 독립적으로 작동할 수 있도록 구체적인 구현체를 제공합니다.
인프라스트럭처 레이어의 주요 예시는 다음과 같습니다:
- 데이터베이스 접근 코드
- 데이터베이스 연결, 데이터 저장, 조회, 업데이트, 삭제와 같은 기능을 수행하는 코드입니다.
- 예: MySQL, MongoDB, PostgreSQL 등과 상호작용하는 Repository나 DAO 클래스, ORM을 활용한 JPA 구현체 등.
- 파일 시스템 접근
- 로컬 또는 클라우드 기반의 파일 시스템에 대한 접근을 제공하는 코드입니다.
- 예: 파일 업로드/다운로드 코드, AWS S3와의 파일 저장 연동 등.
- API 클라이언트
- 외부 시스템과의 통신을 위한 HTTP 클라이언트 또는 API 클라이언트를 포함합니다.
- 예: REST API 통신을 위한 Retrofit, OkHttp, Axios 같은 라이브러리, 또는 SOAP 클라이언트.
- 메시지 브로커와의 통신
- 이벤트나 메시지를 발행하거나 구독하는 코드로, 비동기 처리에 활용됩니다.
- 예: Kafka, RabbitMQ와 같은 메시지 브로커와 상호작용하는 코드.
- 이메일 또는 SMS 전송 서비스
- 외부 서비스와의 통합을 통해 이메일이나 SMS를 전송하는 코드입니다.
- 예: SMTP 프로토콜을 사용하는 이메일 전송 코드, Twilio와 같은 SMS 전송 서비스.
- 로깅 및 모니터링
- 애플리케이션 로그를 수집하고, 모니터링 및 분석 도구에 데이터를 전송하는 코드입니다.
- 예: ELK 스택과 연동된 로깅 모듈, Prometheus와 Grafana를 사용하는 모니터링 코드.
- 프레임워크 코드 및 설정
- 스프링, ASP.NET과 같은 프레임워크에 대한 구체적인 설정 및 활용 코드.
- 예: 스프링 부트의 설정 클래스, 의존성 주입(Dependency Injection) 설정, 보안 및 인증 설정.
- 캐시와의 통신
- 캐시 시스템과의 상호작용을 담당하는 코드입니다.
- 예: Redis나 Memcached와 연동하는 캐시 관리 코드.
인프라스트럭처 레이어는 구체적인 기술과 외부 요소에 대한 세부 사항을 캡슐화하여, 도메인 계층과 유스케이스 계층이 외부 의존성을 최소화할 수 있도록 해줍니다.
클린 아키텍처는 어디서부터 어디까지를 Entity에 넣어야하고 어디까지를 Use Case에 넣어야할지 같은 세부사항을 알기가 쉽지않은것이 문제이다. 그래서 클린 아키텍처를 프로젝트에 도입하기 어렵다
헥사고날 아키텍처
클린 아키텍처와는 약간 다르지만 클린 아키텍처를 적용하기에 아주 좋은 레퍼런스가 될 수 있는 아키텍처로
헥사고날 아키텍처가 있다.
핵사고날 아키텍처의 가장 중요한 핵심은
외부 요소와 핵심 비즈니스 로직이 소통할 때 포트를 통해서 간접적으로 통신해야 한다는 점이다.
헥사고날 아키텍처 특징
헥사고날 아키텍처의 특징은 클린 아키텍처의 특징과 대부분 일치하게 된다.
1. 큰 비즈니스 가치를 가지고 있는 도메인 모델에 큰 관심
2. 레퍼런스가 클린 아키텍처에 비해 많다.
3. 포트와 어댑터를 구성하고 관리하는데 복잡성이 따른다.
4. 도메인에 라이브러리를 직접 활용하기 어렵다.
POJO로 이루어진 Entity와 Use Case는 클린 아키텍처의 Entity와 Use Case랑 유사하다.
어플리케이션의 핵심기능 , 비즈니스 로직 규칙등을 캡슐화 한다.
포트는 도메인 모델과 외부 통신 인터페이스를 정의한다.
Use case에서 포트를 구현하거나 Use case에서 포트를 호출해 외부 요소와 통신을 할 수 있다.
의존성 역전
포트는 인터페이스입니다. 두 가지 종류의 포트가 있는데, 예시와 함께 설명드리겠습니다:
- Inbound Port , Input Port (Driving Port, Primary Port)
// 인바운드 포트 - 외부에서 애플리케이션을 사용하는 방법 정의
public interface OrderUseCase { // 인바운드 포트
void placeOrder(PlaceOrderCommand command);
OrderDetails getOrder(OrderId orderId);
}
// UseCase에서 구현
public class PlaceOrderUseCase implements OrderUseCase { // 포트 구현
public void placeOrder(PlaceOrderCommand command) {
// 주문 처리 로직
}
public OrderDetails getOrder(OrderId orderId) {
// 주문 조회 로직
}
}
// 외부(컨트롤러)에서 사용
@RestController
public class OrderController {
private final OrderUseCase orderUseCase; // 포트 사용
public void createOrder(@RequestBody OrderRequest request) {
orderUseCase.placeOrder(new PlaceOrderCommand(request));
}
}
외부(컨트롤러) => web adapter
2. Outbound Port , Output Port (Driven Port, Secondary Port)
// 아웃바운드 포트 - 애플리케이션이 외부 시스템을 사용하는 방법 정의
public interface OrderRepository { // 아웃바운드 포트
void save(Order order);
Order findById(OrderId id);
}
// UseCase에서 사용
public class PlaceOrderUseCase {
private final OrderRepository orderRepository; // 포트 사용
public void execute(PlaceOrderCommand command) {
Order order = new Order(command.getItems());
orderRepository.save(order); // 포트를 통해 외부 시스템과 통신
}
}
// 외부(어댑터)에서 구현
@Repository
public class JpaOrderRepository implements OrderRepository { // 포트 구현
private final JpaOrderEntityRepository jpaRepository;
public void save(Order order) {
// JPA를 사용해 실제로 DB에 저장
OrderEntity entity = OrderMapper.toEntity(order);
jpaRepository.save(entity);
}
}
jpaOrderRePository -> Persistence Adapter
포트의 장점:
1. 의존성 역전
// UseCase는 구체적인 구현(MySQL, MongoDB 등)을 모름
public class PlaceOrderUseCase {
private final OrderRepository repository; // 인터페이스에만 의존
}
2. 테스트 용이성
// 테스트할 때 실제 DB 대신 가짜 구현 사용 가능
public class TestOrderRepository implements OrderRepository {
private Map<OrderId, Order> orders = new HashMap<>();
public void save(Order order) {
orders.put(order.getId(), order);
}
}
3. 변경 유연성
// MySQL에서 MongoDB로 변경해도 UseCase 코드는 변경 불필요
public class MongoOrderRepository implements OrderRepository {
private final MongoTemplate mongoTemplate;
public void save(Order order) {
// MongoDB에 저장하는 코드
}
}
정리하면:
- 포트 = 인터페이스
- Inbound Port: 외부 → 애플리케이션 진입점
- Outbound Port: 애플리케이션 → 외부 시스템 사용점
도메인 모델은 애플리케이션이 해결하려는 문제 영역의 개념과 규칙을 표현한 모델로, 비즈니스 로직의 핵심 요소들을 담고 있습니다. 즉, 애플리케이션이 다루는 데이터와 그 데이터를 처리하는 규칙을 정의한 구조체라고 할 수 있습니다.
도메인 모델은 주로 엔티티(Entity), 값 객체(Value Object), 서비스(Service) 등을 통해 정의됩니다. 이러한 요소들은 시스템의 핵심 비즈니스 개념과 그 개념 간의 관계를 반영하며, 애플리케이션이 제공해야 하는 기능과 요구사항을 충족하는 데 필수적입니다.
도메인 모델의 주요 구성 요소
- 엔티티(Entity)
- 고유 식별자를 가지며, 도메인 내에서 중요한 데이터를 표현하는 객체입니다.
- 예를 들어, 전자 상거래 애플리케이션의 User, Order, Product 같은 클래스는 각각 고유한 아이디로 식별되는 엔티티입니다.
- 값 객체(Value Object)
- 고유 식별자가 없고, 속성 값으로만 동일성을 판단할 수 있는 객체입니다.
- 예를 들어, Money, Address 같은 클래스는 두 개의 값 객체가 동일한 속성 값을 가질 경우 동일한 것으로 간주됩니다.
- 도메인 서비스(Domain Service)
- 특정 엔티티나 값 객체에 속하지 않는 비즈니스 규칙을 구현한 서비스입니다. 일반적으로 여러 엔티티나 값 객체가 연관된 복잡한 비즈니스 로직이 여기에 포함됩니다.
- 예: 결제 처리를 담당하는 PaymentService, 주문 상태 변경을 담당하는 OrderStatusService 등.
- 애그리거트(Aggregate)
- 서로 연관된 여러 엔티티와 값 객체를 하나의 단위로 묶어 일관성 있는 상태를 유지하게 하는 개념입니다. 주로 하나의 루트 엔티티를 중심으로 구성되며, 애그리거트는 외부에서 루트 엔티티를 통해서만 접근이 가능합니다.
도메인 모델의 역할
- 비즈니스 규칙과 제약 사항을 구현: 도메인 모델은 특정 애플리케이션의 비즈니스 규칙을 코드로 표현하여, 외부 시스템이 아닌 애플리케이션 내에서 독립적으로 유지될 수 있게 합니다.
- 외부 요소와 독립성 유지: 도메인 모델은 데이터베이스나 프레임워크 같은 외부 요소와 직접 연관되지 않으며, 포트 및 어댑터 패턴을 통해 외부와 간접적으로 통신합니다.
포트와 도메인 모델의 관계
클린 아키텍처에서 포트는 도메인 모델이 외부와 상호작용할 수 있도록 정의된 인터페이스입니다. 포트는 도메인 모델이 외부의 세부 사항을 알 필요 없이 비즈니스 로직을 수행하도록 지원하며, 주로 유스케이스에서 이를 구현하거나 호출하여 외부 시스템과의 통신을 수행합니다.
도메인 모델 예시
// 쇼핑몰의 도메인 모델 예시
public class Order {
private OrderId id;
private List<OrderItem> items;
private OrderStatus status;
private Money totalAmount;
public void addItem(Product product, int quantity) {
// 비즈니스 규칙: 재고 확인
if (!product.hasEnoughStock(quantity)) {
throw new OutOfStockException();
}
// 주문 항목 추가 로직
OrderItem item = new OrderItem(product, quantity);
items.add(item);
recalculateTotalAmount();
}
public void cancel() {
// 비즈니스 규칙: 배송 전에만 취소 가능
if (status.isShipped()) {
throw new CannotCancelException("이미 배송된 주문은 취소할 수 없습니다");
}
status = OrderStatus.CANCELLED;
}
}
도메인 모델에는 Entity나 Value Object뿐만 아니라 Domain Service도 포함됩니다.
그 이유를 설명드리면:
- 엔티티나 값 객체에 넣기 애매한 비즈니스 로직이 존재
// 이런 로직은 어느 엔티티에 넣어야 할까요?
public class PaymentService { // Domain Service
public void processPayment(Order order, Payment payment) {
// 주문 금액 검증
if (!order.getTotalAmount().equals(payment.getAmount())) {
throw new InvalidPaymentException();
}
// 결제 처리
payment.process();
// 주문 상태 업데이트
order.markAsPaid();
}
}
- Order에 넣기에는 Payment 관련 로직이 너무 많음
- Payment에 넣기에는 Order 관련 로직이 너무 많음
- 둘 다에 넣으면 책임이 분산됨
2. 여러 엔티티/값 객체가 관여하는 복잡한 비즈니스 로직
public class DiscountService { // Domain Service
public Money calculateDiscount(Order order, Customer customer, Coupon coupon) {
// 주문, 고객, 쿠폰 정보를 모두 고려한 할인 계산
Money discount = Money.ZERO;
// 고객 등급 할인
if (customer.isVIP()) {
discount = discount.add(order.getTotalAmount().multiply(0.1));
}
// 쿠폰 할인
if (coupon.isValid()) {
discount = discount.add(coupon.getDiscountAmount());
}
return discount;
}
}
이런 로직은 특정 엔티티 하나에 속하기 어려움
따라서:
- Entity/Value Object: 개별 비즈니스 개념과 그에 직접 관련된 로직
- Domain Service: 여러 개념이 관여하는 복잡한 비즈니스 로직
이 모두가 도메인 모델을 구성하는 요소가 됩니다. 이들은 모두 비즈니스 규칙을 표현하는 다른 방식일 뿐입니다.
단, Domain Service와 Application Service(Use Case)는 다릅니다:
// Domain Service: 순수한 비즈니스 로직
public class DiscountService {
public Money calculateDiscount(Order order, Customer customer) {
// 비즈니스 규칙에 따른 할인 계산
}
}
// Application Service (Use Case): 트랜잭션, 외부 시스템 통신 등
public class OrderCheckoutUseCase {
public void checkout(OrderId orderId) {
Order order = orderRepository.findById(orderId); // 외부 시스템(DB) 통신
Customer customer = customerRepository.findById(order.getCustomerId());
Money discount = discountService.calculateDiscount(order, customer); // 도메인 서비스 호출
paymentGateway.process(order.getTotalAmount().subtract(discount)); // 외부 시스템(결제) 통신
}
}
Domain Service와 Use Case는 다른 개념입니다. 클린 아키텍처의 계층 관점에서 보면:
- Domain Service (Entities 계층)
- 순수한 비즈니스 규칙만 포함
- 외부 시스템에 대해 전혀 모름
public class PricingService { // Domain Service
public Money calculateOrderPrice(Order order, DiscountPolicy discountPolicy) {
// 순수하게 가격 계산 로직만 수행
Money basePrice = order.calculateBasePrice();
Money discount = discountPolicy.calculateDiscount(order);
return basePrice.subtract(discount);
}
}
2. Use Case (Use Cases 계층)
- 비즈니스 로직의 흐름 제어
- 포트를 통해 외부 시스템과 통신
- Domain Service 사용
public class PlaceOrderUseCase {
private final OrderRepository orderRepository; // 포트
private final PaymentGateway paymentGateway; // 포트
private final PricingService pricingService; // Domain Service
public void execute(PlaceOrderCommand command) {
// 1. 주문 생성
Order order = new Order(command.getItems());
// 2. 가격 계산 (Domain Service 사용)
Money price = pricingService.calculateOrderPrice(
order,
new RegularDiscountPolicy()
);
// 3. 결제 처리 (외부 시스템과 통신)
paymentGateway.process(price);
// 4. 주문 저장 (외부 시스템과 통신)
orderRepository.save(order);
}
}
주요 차이점:
- 책임의 범위
- Domain Service: 순수 비즈니스 규칙만
- Use Case: 비즈니스 흐름 전체 조정
- 의존성
- Domain Service: 외부 시스템 모름
- Use Case: 포트를 통해 외부 시스템과 통신
- 위치
- Domain Service: Entities 계층 (가장 안쪽)
- Use Case: Use Cases 계층 (중간)
정리하면:
- Domain Service는 순수한 비즈니스 규칙을 표현하는 도메인 모델의 일부
- Use Case는 이러한 도메인 모델을 사용해 실제 애플리케이션의 기능을 구현하는 계층
레이어드 아키텍처의 Service
// 레이어드 아키텍처의 Service - 모든 것이 한 곳에
@Service
public class OrderService {
private final OrderRepository orderRepository;
private final PaymentGateway paymentGateway;
public void placeOrder(OrderRequest request) {
// 1. DTO → Entity 변환
Order order = new Order(request.getItems());
// 2. 비즈니스 로직
Money totalPrice = order.calculateTotalPrice();
if (totalPrice.isGreaterThan(Money.of(1000000))) {
throw new OrderLimitExceededException();
}
// 3. 결제 처리
paymentGateway.process(totalPrice);
// 4. 데이터베이스 저장
orderRepository.save(order);
// 5. 이메일 발송
emailService.sendOrderConfirmation(order);
}
}
클린 아키텍처의 도메인 서비스와 use case
// 1. Domain Service - 순수 비즈니스 로직만
public class OrderValidationService {
public void validateOrder(Order order) {
Money totalPrice = order.calculateTotalPrice();
if (totalPrice.isGreaterThan(Money.of(1000000))) {
throw new OrderLimitExceededException();
}
}
}
// 2. Use Case - 흐름 제어
public class PlaceOrderUseCase {
private final OrderRepository orderRepository;
private final PaymentGateway paymentGateway;
private final OrderValidationService validationService;
private final NotificationPort notificationPort;
public void execute(PlaceOrderCommand command) {
// 주문 생성
Order order = new Order(command.getItems());
// 비즈니스 규칙 검증 (Domain Service 사용)
validationService.validateOrder(order);
// 외부 시스템과 통신 (Port 사용)
paymentGateway.process(order.calculateTotalPrice());
orderRepository.save(order);
notificationPort.sendOrderConfirmation(order);
}
}
주요 차이점:
- 책임 분리
- 레이어드: 하나의 Service 클래스가 모든 책임
- 클린: Domain Service(비즈니스 규칙)와 Use Case(흐름 제어)로 분리
- 의존성 방향
- 레이어드: Service가 직접 Repository, 외부 서비스에 의존
- 클린: Use Case가 Port(인터페이스)에 의존, 실제 구현은 바깥쪽에
- 비즈니스 로직의 위치
- 레이어드: Service 클래스 안에 모든 로직
- 클린: Domain Model과 Domain Service에 비즈니스 로직, Use Case는 조정만
이렇게 분리함으로써:
- 비즈니스 로직이 더 명확해짐
- 테스트가 쉬워짐
- 변경에 더 유연해짐
어댑터는 외부 요소를 직접 다루는 부분이다.
라이브러리나 프레임워크에 종속적인 코드는 모두 여기에 들어가게 된다.
포트를 구현하기도 하고[PersistenceAdapter 처럼]
포트를 통해 Use case를 직접 호출하기도 한다. [Web Adapter 처럼]
어댑터는 애플리케이션을 사용하는 driving adapter와 애플리케이션이 사용하는 driven adapter로 구분할 수 있다.
헥사고날 아키텍처를 읽기 위해 알아야하는 지식
UML의 기초는 has-a와 is-a입니다.
has-a는 ->로 표시합니다 [ 색칠된 화살표 ]
이 문맥에서는 “사용한다” 라고 읽어도 된다.
is-a는 속이 빈 화살표, [ 색칠되지않은 화살표 ]
-|>로 표시합니다 “구현한다” 라고 읽는다. [ =상속받는다.]
화살표를 해석해보면 다음과 같이 해석 할 수 있다.
- ① WebAdapter는 InputPort를 사용합니다
[ 컨트롤러는 InputPort [=UseCase 인터페이스]를 사용한다.
InputPort에는 요구사항을 처리하는 메소드들이 있고 그 메소드는 비즈니스 로직의 흐름 제어를 하는데
그것을 구현하는 구현체는 UseCase이다.] - ② Usecase는 InputPort를 구현합니다 [ UseCase는 구현체다. ]
- ③ Usecase는 Entity를 사용합니다
- ④ Usecase는 OutputPort를 사용합니다 [ OutputPort는 리포지토리 인터페이스 같은것이다. UseCase는 리포지토리 인터페이스를 사용하고 PersistenceAdapter[= 리포지토리 구현체]같은 외부요소가 OutputPort의 구현체이다. ]
- ⑤ PersistenceAdapter는 OutputPort를 구현합니다
Usecase를 호출하는 쪽을 입력 포트 , 입력 어댑터라고 볼 수 있고
Use case로부터 호출되어 외부에 결과를 전달하는것들을 출력 포트, 출력 어댑터라고 볼 수 있다.
Controller = Input Adapter
분홍색 동그라미 = Input Port
RepositoryImpl = Output Adapter
JpaMember가 있는 이유는
domain 하위에 있는 Member.java는 POJO이기 때문이다.
프로젝트에 헥사고날 아키텍처를 적용한다면
코드,파일,패키지 수가 일반적인 프로젝트보다 증가하게 된다.
그래서 어떤 아키텍처도 완벽하지않다.
헥사고날 아키텍처를 고려해야할때
1. 대규모의 프로젝트를 진행할때
2. 프로젝트 일원 모두 클린 아키텍처를 이해하고 있을 때
3. 외부 요소의 변화가 잦을때
헥사고날 아키텍처 정리
간단하게 설명
Web Adapter : 컨트롤러
Input Port : 레이어드 아키텍처로 생각하면 서비스 인터페이스이다
. [ = 구현해야하는 비즈니스 로직 흐름을 가지는 메소드들이 적혀있는 인터페이스]
Use case : 레이어드 아키텍처로 생각하면 서비스 인터페이스 구현체 [= 구현해야하는 비즈니스 로직 흐름을 가지는 메소드를 구현한 구현체 , 즉 Input Port의 구현체]
Output Port : 레이어드 아키텍처로 생각하면 리포지토리 인터페이스가 하나의 예시가 된다. [ 사용할 외부 기술 로직 정의 ,
persistenceAdapter와 연결된 OutputPort라면 리포지토리 인터페이스이다.]
Persistence Adapter : 리포지토리 인터페이스 구현체, 외부 기술을 사용한 것들 [ ex) JpaRepository]
화살표대로 해석
- Use Case 는 Input Port를 구현한다. -> 유즈케이스는 서비스 인터페이스를 구현한 구현체이다.
- Web Adapter는 Input Port를 사용한다. -> 컨트롤러는 서비스 인터페이스를 사용한다.
[컨트롤러는 서비스 인터페이스를 사용하고, Spring 프레임워크가 서비스 구현체를 주입해준다. ]
- Use Case는 Persistence Adapter를 사용한다. -> 서비스 구현체는 리포지토리 인터페이스를 사용한다.
- Use Case는 Entity를 사용한다. -> 서비스 구현체는 엔티티를 사용한다.
input , output 방향성의 의미
헥사고날 아키텍처에서 "input port"와 "output port"라는 이름은
애플리케이션의 핵심(core)을 기준으로 데이터나 제어의 흐름 방향을 나타낸다.
Input Port (입력 포트):
- "Input"이라는 이름은 애플리케이션 코어로 들어오는 데이터나 요청을 의미합니다.
- 외부 세계(예: 웹 인터페이스, CLI, 다른 애플리케이션 등)로부터 애플리케이션 코어로 들어오는 진입점입니다.
- 애플리케이션의 사용 사례(use case)를 정의하는 인터페이스입니다.
- 외부 세계(Web Adapter)가 애플리케이션과 상호작용하는 방법을 정의합니다. [ 컨트롤러가 사용할 서비스의 메소드를 정의]
- 주로 애플리케이션의 유스케이스나 기능을 정의하는 인터페이스입니다.
- 예: 우리의 OrderUseCase 인터페이스 [ == 서비스 인터페이스 ]
Output Port (출력 포트):
- "Output"이라는 이름은 애플리케이션 코어에서 나가는 데이터나 요청을 의미합니다.
- 애플리케이션 코어가 외부 시스템(예: 데이터베이스, 외부 서비스 등)과 상호작용하기 위한 인터페이스입니다.
- 주로 데이터 접근이나 외부 서비스 호출을 추상화합니다.
- 예: 우리의 OrderRepository 인터페이스
UseCase (OrderManagementService):
- Input Port(OrderUseCase)를 구현한 클래스입니다.
- 실제 비즈니스 로직을 포함하고 있습니다.
- Output Port(OrderRepository)를 사용하여 데이터 접근을 수행합니다.
Web Adapter (OrderController):
- 외부 요청(HTTP)을 받아 Input Port(OrderUseCase)로 전달합니다.
- OrderUseCase 인터페이스에 의존하며, 구체적인 구현(OrderManagementService)을 알지 못합니다.
Persistence Adapter (JpaOrderRepository):
- Output Port(OrderRepository)를 구현한 클래스입니다.
- 실제 데이터베이스 작업을 수행합니다 (예: JPA를 사용).
이러한 명명의 이점
- 방향성 명확화:
- "Input"과 "Output"이라는 용어는 데이터와 제어 흐름의 방향을 명확하게 나타냅니다.
- 애플리케이션 코어를 중심으로 생각할 때, 무엇이 들어오고(input) 무엇이 나가는지(output) 쉽게 이해할 수 있습니다.
- 관심사 분리:
- Input 포트는 애플리케이션이 제공하는 기능에 집중합니다.
- Output 포트는 애플리케이션이 필요로 하는 외부 리소스에 집중합니다.
- 의존성 방향 제어:
- Input 포트를 통해 외부 요소가 애플리케이션 코어에 의존하게 만듭니다.
[= 외부요소가 input 포트를 의존하니까 결과적으로 어플리케이션 코어에 의존한다는 말이다.] - Output 포트를 통해 애플리케이션 코어가 구체적인 구현이 아닌 추상화에 의존하게 만듭니다.
- Input 포트를 통해 외부 요소가 애플리케이션 코어에 의존하게 만듭니다.
- 유연성과 테스트 용이성:
- 포트를 통한 상호작용은 실제 구현을 쉽게 교체하거나 모의 객체(mock)로 대체할 수 있게 해줍니다.
예를 들어, 우리의 주문 시스템에서:
- OrderUseCase (Input Port)는 외부에서 주문 생성, 조회, 상태 업데이트 요청이 들어오는 진입점입니다.
- OrderRepository (Output Port)는 주문 데이터를 저장하고 조회하기 위해 애플리케이션 코어에서 외부 저장소로 나가는 요청을 정의합니다
이 구조의 이점
- 관심사 분리: 각 컴포넌트가 명확한 책임을 가집니다.
- 의존성 역전: 핵심 비즈니스 로직(UseCase)이 구체적인 구현에 의존하지 않고, 인터페이스(Port)에 의존합니다.
- 테스트 용이성: 각 컴포넌트를 독립적으로 테스트할 수 있습니다.
- 유연성: 구현체를 쉽게 교체할 수 있습니다 (예: 다른 데이터베이스 기술로 변경)
이러한 구조를 통해 애플리케이션의 핵심 비즈니스 로직을
외부 요소(웹, 데이터베이스 등)로부터 분리하고 보호할 수 있습니다.
개선된 레이어드 아키텍처 vs 헥사고날 아키텍처 [ 클린 아키텍처 ]
인터페이스를 이용하여 하위계층의 결합도를 낮춘 개선된 레이어드 아키텍처를 사용한다면
기존의 레이어드 아키텍처의 단점을 해결하고 헥사고날 아키텍처가 가지고 있던 장점 또한 가질 수 있다.
대규모라도 개선된 레이어드 아키텍처를 사용할 수 있다.
두 아키텍처 모두 확장성, 유지보수성, 테스트 용이성 등에서 뛰어난 성능을 보일 수 있기 때문
그래서 개선된 레이어드 아키텍처와 헥사고날 아키텍처 차이는 다음으로 볼 수 있다.
- 용어와 개념적 모델: 레이어 vs 포트와 어댑터
- 시각적 표현: 계층형 다이어그램 vs 육각형 다이어그램
- 외부 시스템과의 상호작용 강조 정도
그래서 개선된 레이어드 아키텍처, 헥사고날 아키텍처를 선택하는 기준이 될 수 있는건 다음과 같다.
- 개념적 모델과 용어:
- 레이어드 아키텍처의 개념에 더 익숙하고 그 용어를 선호한다면 개선된 레이어드 아키텍처를 선택
- 포트와 어댑터의 개념을 명시적으로 사용하고 싶다면 헥사고날 아키텍처를 선택
- 아키텍처 표현:
- 전통적인 계층 다이어그램으로 시스템을 표현하고 싶다면 개선된 레이어드 아키텍처
- 육각형 다이어그램으로 시스템을 시각화하고 싶다면 헥사고날 아키텍처
- 팀의 선호도와 경험:
- 팀이 어떤 아키텍처 스타일에 더 익숙한지에 따라 선택
- 기존 시스템과의 연속성:
- 기존 레이어드 아키텍처에서 진화하는 경우 개선된 레이어드 아키텍처가 더 자연스러울 수 있음
- 외부 시스템과의 상호작용 강조:
- 외부 시스템과의 상호작용을 더 명시적으로 모델링하고 싶다면 헥사고날 아키텍처가 약간 더 적합할 수 있음
출처
https://www.youtube.com/watch?v=Ql7CoQminoM&ab_channel=%EC%9A%B0%EC%95%84%ED%95%9C%ED%85%8C%ED%81%AC
실무사용사례를 볼수있는 출처
https://blog.appkr.dev/work-n-play/learn-n-think/ddd-hexagonal/
댓글