판매 시스템 설계와 관련하여, 다양한 도메인 및 역할 간의 협력 및 의존성에 대한 고민을 하고 있어요. 고민을 한 이유는 더 나은 구조를 만들어 코드의 응집성을 향상시키고 역할 및 책임을 명확하게 구분하며, 향후 유지보수와 확장성을 개선이 가능하도록 만들고 싶기 때문이에요. 제 목표는 판매 시스템의 모든 구성 요소가 서로 협력하여 원활한 작동을 보장하는 중요한 역할을 수행하도록 만드는 것이에요 !!
(제가 하고 있는 프로젝트의 주제는 기프티콘 중고거래에요.)
판매를 하기 위해 다음과 같은 검증 항목이 필요해요.
(현재 저는 데이터 액세스 계층에서 Mapper를 사용하고 있어요.)
각 객체의 역할과 책임을 설명한 후에 고민했던 부분들이 무엇이었는지 소개할게요.
두 가지 고민을 말하기 전에 각각의 코드가 처음에 어떻게 되어있는지에 대해 공유할게요.
@Service
@RequiredArgsConstructor
public class SaleCreationFacade {
private final ItemService itemService;
private final UserService userService;
private final SaleService saleService;
private final SaleCreateValidator saleCreateValidator;
public int createSale(int itemId, int sellerId, SaleCreateRequest request) {
// 아이템 존재 확인 로직 ...
// 유저 존재 유무 확인 로직 ...
// 해당 유저가 판매자 인지 확인하는 로직 ...
saleCreateValidator.validate(request);
return saleService.save(itemId, sellerId, request);
}
}
@Component
@RequiredArgsConstructor
public class SaleCreateValidator {
private final SaleMapper saleMapper;
public void validate(SaleCreateRequest request) {
validateExpirationDate(request.expirationDate());
validate(request);
}
private void validateExpirationDate(LocalDate expirationDate) {
LocalDate currentDate = LocalDate.now();
if (expirationDate.isBefore(currentDate)) {
throw new ExpiredSaleException();
}
}
private void checkDuplicateBarcode(String barcord) {
if (saleMapper.existsByBarcode(barcord)) {
throw new DuplicatedBarcodeException();
}
}
}
@Service
@RequiredArgsConstructor
public class SaleService {
private final SaleMapper saleMapper;
public int save(int itemId, int sellerId, SaleCreateRequest request) {
Sale sale = request.toEntity();
sale.updateItemId(itemId);
sale.updateSellerId(sellerId);
saleMapper.save(sale);
return sale.getId();
}
// 기타 로직
}
SaleCreateValidator가 Mapper를 의존하게 되면 Service 레이어를 건너뛰는 것은 기본적으로 좋지 않아요. Mapper는 데이터 액세스 레이어에 속하며, 일반적으로 Service 레이어가 데이터 액세스를 처리하는 것이 일반적인 규칙이에요.
그렇다면, Service 레이어를 건너 뛰어 SaleCreateValidator가 Mapper를 의존하는게 규칙을 깬 것보다 더 나은 이점이 있는지 물음을 던져보았어요.
SaleCreateValidator에서 Mapper를 사용하고 Facade 레이어에서 여러 도메인의 Service와 SaleCreateValidator를 의존할 거에요. Facade 에서 판매 생성을 위한 검증 로직을 한 곳에 모아 두니 좀 더 응집성 있는 구조를 유지할 수 있다는 장점이 있어요.
하지만 다음과 같은 문제점이 있어요.
레이어 간의 순환 참조를 피하려면 Mapper를 Service 레이어에서만 사용하고, Validator 레이어에서는 데이터 액세스 로직을 처리하지 않기로 했어요. 이제 SRP를 준수하며 역할 분리 원칙을 지킬 수 있게 됐어요.
(바코드가 이미 존재하는지에 대한 검증하는 것은 Service 에서 로직을 작성했어요.)
코드는 다음과 같이 바뀌었어요.
@Service
@RequiredArgsConstructor
public class SaleCreationFacade {
private final ItemService itemService;
private final UserService userService;
private final SaleService saleService;
private final SaleCreateValidator saleCreateValidator;
public int createSale(int itemId, int sellerId, SaleCreateRequest request) {
// 아이템 존재 확인 로직 ...
// 유저 존재 유무 확인 로직 ...
// 해당 유저가 판매자 인지 확인하는 로직 ...
saleCreateValidator.validate(request);
saleService.validateDuplicateBarcode(request.barcode());
return itemVariantService.save(itemId, sellerId, request);
}
}
@Component
public class SaleCreateValidator {
// Mapper 의존성 삭제
public void validate(SaleCreateRequest request) {
validateExpirationDate(request.expirationDate());
}
private void validateExpirationDate(LocalDate expirationDate) {
LocalDate currentDate = LocalDate.now();
if (expirationDate.isBefore(currentDate)) {
throw new ExpiredSaleException();
}
}
// 바코드 존재 유무 로직 삭제
}
@Service
@RequiredArgsConstructor
public class SaleService {
private final SaleMapper saleMapper;
public int save(int itemId, int sellerId, SaleCreateRequest request) {
// 바코드 검증 로직 사용지점
validateDuplicateBarcode(request.barcode);
Sale sale = request.toEntity();
sale.updateItemId(itemId);
sale.updateSellerId(sellerId);
saleMapper.save(itemVariant);
return sale.getId();
}
// 바코드 존재 유무 검증 로직 추가
public void validateDuplicateBarcode(String barcode) {
if (saleMapper.existsByBarcode(barcode)) {
throw new DuplicatedBarcodeException();
}
}
}
CreateSaleFacade에서 SaleCreateValidator를 의존하는 이유
SaleService에서 SaleCreateValidator를 의존하는 이유
저는 다음과 같이 결론을 내렸어요.
SaleService와 CreateSaleFacade 중 SaleCreateValidator를 의존해야 하는 객체는 CreateSaleFacade에요. 이유는 CreateSaleFacade가 판매 시스템의 퍼사드 역할을 하고, 검증 및 비지니스 로직의 일부를 처리해야하기 때문이에요. SaleCreateValidator는 Facade 레이어 내에서 검증 작업을 수행하며, SaleService는 판매와 관련된 비지니스 로직을 처리하는 데 중점을 둬야 해요.
판매 시스템 설계에 대한 고민을 통해, 서비스, 퍼사드 및 유효성 검사기의 역할과 상호작용을 고민한 부분들을 정리해보았어요. 유효성 검사기가 Mapper에 의존하는 것과 객체 간 의존성을 어떻게 관리해야 하는지에 대한 고민을 풀어봤고, 역할 분리와 순환 의존성을 피하기 위한 방법을 제안했어요.
결론적으로, CreateSaleFacade와 SaleService 중 SaleCreateValidator를 의존해야 하는 객체는 CreateSaleFacade이고, Validator에서 Mapper를 의존하지 않는 것으로 결론 지었어요. 이를 통해 시스템의 각 요소가 명확한 책임을 지며, 응집성 있고 유지보수 가능한 설계를 만들어 보았어요. 감사합니다.