멋쟁이사자처럼 백엔드 플러스 과정에서 앞으로 프로젝트를 진행하면서 사용할 아키텍처에 대해 배웠다.

그 내용을 간단히 정리해본다.

도메인?

개발을 배우고, 강의를 들으면서 도메인이라는 이야기를 정말 많이 들었는데, 사이트 도메인 말고는.. 딱히 들어본 적이 없는 개념이었다.

도메인은 ‘해결하려는 비즈니스 문제 영역’ 이라고 하는데, 이렇게 보면 너무 추상적이어서,

수업에서는 주로 코드를 통해 익히고 배우는 과정을 진행했다.

음식 주문 앱을 만든다고 해보자

음식 주문 앱을 만든다고 예를 들어보자.

이 앱의 핵심 비즈니스 영역은 어떤게 있을까?

가게에 주문을 하고, 리뷰를 남기는 서비스라고 한다면..

Order : 주문

Store: 가게

Payment: 결제

Review: 리뷰

이런 도메인이 나올 수 있다.

왜 DDD (Domain-Driven Design) 도메인 주도 설계를 해야할까?

1) 서비스 분리

서비스가 커지면, 각 서비스 별로 잘게 나눠서.. 유지 보수를 편하게 할 수 있다.

도메인 주도 설계를 하면 책임을 명확하게 할 수 있다.

이 부분은 DDD와 함께 자주 쓰이는 헥사고날 아키텍처를 통해 더 잘 이해할 수 있다.

2) 테스트 가능한 코드

도메인 주도 설계에서는 핵심 비즈니스 로직만을 담는 것이 핵심이다.

따라서 프레임워크와 독립된 코드로 작성하는데, 이렇게 하면 단위 테스트가 용이해진다.

** 순수 자바로 코드를 만든다고 해서 POJO (Plain Old Java Object) 라고도 한다.

그동안 개발하면서 테스트 코드가 스프링 의존성 때문에 어려웠다면, DDD를 적용해보는 것도 좋을 것 같다.

헥사고날 아키텍처

DDD와 함께 많이 언급되는 아키텍처로.. 비즈니스 로직을 외부 인프라와 분리해 의존성을 낮춘다.

개인적인 생각은 각 도메인간의 책임을 명확히 할 수 있어 자주 쓰이는 아키텍처라고 생각한다.




헥사고날 아키텍처는

도메인을 중심으로, 해당 도메인을 소통하기 위한 Port 인터페이스가 존재한다.

이 Port인터페이스를 상속 받은 Adapter를 구현해 도메인이 실제 외부와 통신하게 된다.

코드로 이해하는게 더 쉬우니, 코드로 이해해보자.

만약 주문을 조회할 때 가게가 열려있을 때만 가능하다고, 가정해보자.

이를 도메인을 헥사고날 아키텍처로 설계하면 아래 코드와 같이 할 수 있다.

📁 order.presentation.controller

컨트롤러를 통해 주문 ID로 주문 조회 요청을 받는다.

@RequiredArgsConstructor @RequestMapping(path = "/order") @RestController public class OrderController { private final OrderService orderUsecase; @GetMapping public OrderResponse getOrder(@RequestParam Long orderId) { return orderUsecase.getOrderById(orderId); } }

📁 order.application.service

주문 ID 요청을 받으면, 주문 ID 서비스 요청을 받는다.

서비스 요청을 받으면,

OrderRepository를 통해 DB 접근을 하고, Entity를 받아와 도메인 객체로 응답해준다.

이때 서비스는 받아온 도메인 객체를 Response 객체로 바꿔 응답해준다.

@Service public class OrderService { private final OrderRepository orderRepository; public OrderService(OrderRepository orderRepository) { this.orderRepository = orderRepository; } public OrderResponse getOrderById(Long orderId) { return OrderFactory.createOrderResponse(orderRepository.getOrderById(orderId)); } }

📁 order.application.port.out

다른 도메인으로부터 데이터를 받아오는 역할을 하므로 out 패키지에 port 인터페이스를 만들었다.

public interface StoreStatusPort { Boolean getStoreOpenStatus(Long storeId); }

📁 order.domain

@Repository public interface OrderRepository { Order getOrderById(Long orderId); }

📁 store.domain.adapter

@Component public class StoreApiAdapter implements StoreStatusPort { @Override public Boolean getStoreOpenStatus(Long storeId) { /* 가게 오픈 상태를 확인하는 비즈니스 로직 예제 코드로.. 가게는 늘 열려있다. */ return true; } }

📁 order.infrastructure.adpter

@RequiredArgsConstructor @Repository public class OrderRepositoryAdapter implements OrderRepository { private final OrderMemoryRepository orderMemoryRepository; private final StoreStatusPort statusPort; @Override public Order getOrderById(Long orderId) { if (statusPort.getStoreOpenStatus(orderId)) { // 가게가 열려있으면, return OrderFactory.createOrder(orderMemoryRepository.findById(orderId)); } throw new OrderNotExistException(); } }