서비스 참조
클린 아키텍처를 적용하며 가장 헷갈렸던 부분 중 하나가 service간 참조가되는 상황에서의 코드 작성이다. 외부와 연결이 되지 않아 Repository로 만들수는 없고, 이미 다른 서비스에 구현되어 있는 코드를 다시 새로운 서비스에 구현하는 것도 비효율적인 것 같았다.
구글링을 해보니 Spring에서는 이런 경우
- 컨트롤러에서 각 서비스들을 호출
- 서비스들을 호출하는 UseCase 활용 / 상위 Service를 만들어 통합
이 가장 많이 고려되는 방법이었다.
하지만 컨트롤러에서 각 서비스들을 호출하는 방법은 clean architecture의 철학과 맞지 않다고 생각된다. Controller에서 여러 Service를 호출하게 되면, 여러 서비스 호출 시 한 서비스에서 오류가 발생 했을 때, 다른 서비스의 작업들은 롤백이 되지않아 데이터가 일관성을 잃게 된다. 또한, 컨트롤러의 역할은 클라이언트의 요청을 받고, 적절한 Service(단일)을 호출하여 응답을 반환하는 것이다. 여기서 Service들을 조합하여 사용하게 되면 너무 많은 책임을 가지게 된다.
무엇보다, 비즈니스 로직의 수정이 필요한 경우 Controller까지 검토 및 수정해야되는 경우가 생기기 때문에 유지보수에도 악영향을 줄 것이라고 생각된다.
상위서비스(혹은 UseCase)를 생성하는 경우를 살펴보자.
Application
├── Orders
│ └── OrderService.cs
├── Payments
│ └── PaymentService.cs
├── Users
│ └── UserService.cs
└── Coordinators
└── PurchaseCoordinator.cs ← 상위 통합 서비스
using CleanArch.Application.Users;
using CleanArch.Application.Orders;
using CleanArch.Application.Payments;
namespace CleanArch.Application.Coordinators
{
public class PurchaseCoordinator
{
private readonly IUserService _userService;
private readonly IOrderService _orderService;
private readonly IPaymentService _paymentService;
public PurchaseCoordinator(
IUserService userService,
IOrderService orderService,
IPaymentService paymentService)
{
_userService = userService;
_orderService = orderService;
_paymentService = paymentService;
}
public async Task<bool> PurchaseAsync(int userId, string product, decimal price)
{
// 1️⃣ 주문 생성
var order = await _orderService.CreateOrderAsync(product, price);
// 2️⃣ 포인트 차감
var pointResult = await _userService.DeductPointsAsync(userId, (int)price);
if (!pointResult) return false;
// 3️⃣ 외부 결제 처리
var paymentResult = await _paymentService.ProcessPaymentAsync(price);
if (!paymentResult) return false;
// 4️⃣ 결제 완료 → 주문 상태 업데이트
order.MarkPaid();
await _orderService.UpdateAsync(order);
return true;
}
}
}
이렇게 코드를 작성한 경우 각 서비스가 자신의 도메인만 처리할 수 있도록 하고 결과만 return 할 수 있게 된다. 하지만 Service내부에서 계층이 나뉘어지게 되기 때문에 갈수록 관리가 어려워질 가능성이 있다.(계층이 2개에서 끝나지 않을 가능성도 발생)
또한 1번과 마찬가지로 트랜잭션 관리가 어려워지게 될 수도 있다. ex.1.주문생성 (성공) => 2. 포인트 차감 (실패)
그렇기 때문에 사용한다면 조회 쿼리를 사용하는 서비스들만 사용하는것이 안전한 구조가 된다.
.NET에서 DIP를 위배하지 않고 서비스를 호출하는 대표적인 방법은 구현체가 아닌 인터페이스를 참조하는 방법이다.
예시를 보면,
INotificationService.cs (Notification이 내부로직만 있어 Applciation layer에 속해 있다고 가정)
namespace TodoList.Application.Interfaces
{
public interface INotificationService
{
Task NotifyTodoCreation(int todoId);
}
}
TodoService.cs
public class TodoService : ITodoService
{
private readonly ITodoRepository _repository;
private readonly IAppLogger<TodoService> _logger;
private readonly INotificationService _notificationService; <- 인터페이스를 주입
public TodoService(ITodoRepository repository, IAppLogger<TodoService> logger, INotificationService notificationService)
{
_repository = repository;
_logger = logger;
_notificationService = notificationService;
}
public async Task<TodoDto> CreateAsync(CreateTodoRequest request)
{
var todo = new TodoItem(request.Title, new DueDate(request.DueDate));
await _repository.AddAsync(todo);
await _notificationService.NotifyTodoCreation(todo.Id);
return ToDto(todo);
}
// ...
}
이렇게 직접참조가 아닌 인터페이스를 참조하면 서비스 간 결합도를 낮출 수 있고(TodoService는 notificationService가 어떻게 구현되어 있는지 알 필요가 없음), 서비스 간 독립성과 순환 참조 방지의 역할을 할 수 있다.
특히나, NotificationService가 SMS, 이메일 등의 외부 인프라와 연동이 되어있다면 NotificationService는 Infrastructure레이어에 구현되는 것이 맞기 때문에 상위 모듈은 하위 모듈에 의존하지 말고, 둘 다 추상화(인터페이스)에 의존해야 한다는 DIP원칙을 지키기 위해 더욱 Interface를 참조해야만 한다.
EX)
| 계층 | 의존대상 | 설명 |
| Application Layer (TodoService) | INotificationService (추상화) | 구현체에 관심 없음 |
| Infrastructure Layer (EmailNotificationService) | INotificationService (추상화) | 인터페이스를 구현함 |
이렇게 인터페이스만 참조해서 Service를 만든다고 하더라도, 한 서비스에서 다른 서비스의 함수를 호출한다면 서비스 간 결합도가 상승할 수 밖에 없다. 이 결합도를 느슨하게 만드는 방법은 공용으로 사용되는 로직을 도메인 서비스 혹은 도메인 이벤트로 옮기는 방법이 있다.
도메인 서비스를 만드는 방법은
https://www.milanjovanovic.tech/blog/building-a-custom-domain-events-dispatcher-in-dotnet
Building a Custom Domain Events Dispatcher in .NET
Learn how to build a lightweight, in-process domain events dispatcher in .NET without external dependencies. We'll explore the trade-offs between immediate consistency and coupling while implementing a strongly-typed solution from scratch.
www.milanjovanovic.tech
이 아티클을 보면 자세하게 알 수 있다.