효과적인 판매 시스템 설계: Service, Facade 및 Validator의 역할 및 상호작용

허진혁·2023년 11월 9일
0

givemeticon 프로젝트

목록 보기
7/10

📚 개요

판매 시스템 설계와 관련하여, 다양한 도메인 및 역할 간의 협력 및 의존성에 대한 고민을 하고 있어요. 고민을 한 이유는 더 나은 구조를 만들어 코드의 응집성을 향상시키고 역할 및 책임을 명확하게 구분하며, 향후 유지보수와 확장성을 개선이 가능하도록 만들고 싶기 때문이에요. 제 목표는 판매 시스템의 모든 구성 요소가 서로 협력하여 원활한 작동을 보장하는 중요한 역할을 수행하도록 만드는 것이에요 !!

🕵️‍♂️ 구현 고려 사항

(제가 하고 있는 프로젝트의 주제는 기프티콘 중고거래에요.)

판매를 하기 위해 다음과 같은 검증 항목이 필요해요.

  1. 판매할 아이템이 존재하는가?
  2. 유저가 존재하는가?
  3. 유저의 계좌가 존재하는가?(계좌가 존재해야 판매자로 판매 기능을 사용할 수 있어요)
  4. 이미 존재하는 바코드인가?
  5. 유효기간이 지나지 않았는가?

☀︎ 역할 및 책임

(현재 저는 데이터 액세스 계층에서 Mapper를 사용하고 있어요.)

각 객체의 역할과 책임을 설명한 후에 고민했던 부분들이 무엇이었는지 소개할게요.

  • CreateSaleFacade: 각 도메인의 서비스를 의존하여 필요한 메서드를 가져올 거에요. Controller는 이 파사드를 의존하게 되요.
  • SaleService: 판매 도메인과 관련된 비지니스 로직을 작성하는 곳이에요.
  • SaleCreateValidator: 판매 아이템 생성을 위해 필요한 검증을 위한 곳이에요.

🤔 두 가지 고민

두 가지 고민을 말하기 전에 각각의 코드가 처음에 어떻게 되어있는지에 대해 공유할게요.

SaleCreationFacade

@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);
    }
}

SaleCreateValidator

@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();
        }
    }
}

SaleService

@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();
    }

    // 기타 로직
}

논점 1️⃣ SaleCreateValidator가 Mapper를 의존해도 괜찮은가?

SaleCreateValidator가 Mapper를 의존하게 되면 Service 레이어를 건너뛰는 것은 기본적으로 좋지 않아요. Mapper는 데이터 액세스 레이어에 속하며, 일반적으로 Service 레이어가 데이터 액세스를 처리하는 것이 일반적인 규칙이에요.

그렇다면, Service 레이어를 건너 뛰어 SaleCreateValidator가 Mapper를 의존하는게 규칙을 깬 것보다 더 나은 이점이 있는지 물음을 던져보았어요.

SaleCreateValidator에서 Mapper를 사용하고 Facade 레이어에서 여러 도메인의 Service와 SaleCreateValidator를 의존할 거에요. Facade 에서 판매 생성을 위한 검증 로직을 한 곳에 모아 두니 좀 더 응집성 있는 구조를 유지할 수 있다는 장점이 있어요.

하지만 다음과 같은 문제점이 있어요.

  • 역할 분리 원칙 위반: Mapper를 Validator 내에서 직접 사용하는 것은 Validator가 데이터 액세스 논리를 처리하게 만드는 것으로 역할 분리 원칙을 위반하게 되요. (SRP 위반)
    • Validator 클래스는 주로 입력 데이터의 유효성을 검사하고 유효하지 않은 데이터를 거부하는 역할이에요.
    • Mapper 클래스는 데이터베이스와 같은 데이터 저장소와 상호 작용하며 데이터 액세스와 조작을 담당 하는 역할이에요.
  • 순환 의존성 가능성: SaleCreateValidator가 Mapper에 의존하고 SaleService도 Mapper에 의존한다면, 순환 의존성 문제가 발생할 수 있습니다. 이러한 상황은 코드의 복잡성을 높이고 유지 보수를 어렵게 만들 수 있습니다.

레이어 간의 순환 참조를 피하려면 Mapper를 Service 레이어에서만 사용하고, Validator 레이어에서는 데이터 액세스 로직을 처리하지 않기로 했어요. 이제 SRP를 준수하며 역할 분리 원칙을 지킬 수 있게 됐어요.
(바코드가 이미 존재하는지에 대한 검증하는 것은 Service 에서 로직을 작성했어요.)

코드는 다음과 같이 바뀌었어요.

SaleCreationFacade (변화 없음)

@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);
    }
}

SaleCreationValidator

@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();
        }
    }
	// 바코드 존재 유무 로직 삭제
}

SalerService

@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();
        }
    }
}

논점 2️⃣ CreateSaleFacade와 SaleService 중 SaleCreateValidator를 의존해야 하는 객체는 누구일까?

CreateSaleFacade에서 SaleCreateValidator를 의존하는 이유

  • CreateSaleFacade는 클라이언트와의 상호작용을 처리하며, Sale 생성에 필요한 입력 데이터의 유효성을 검사해야 해요. SaleCreateValidator는 이러한 입력 데이터의 유효성 검사를 수행하는데 적합한 역할을 수행해
  • CreateSaleFacade가 SaleCreateValidator에 의존하면, 클라이언트로부터 받은 데이터를 검증하고, Sale 생성 요청을 SaleService로 전달하기 전에 데이터의 유효성을 확인할 수 있어요.

SaleService에서 SaleCreateValidator를 의존하는 이유

  • SaleService는 Sale 도메인과 관련된 비즈니스 로직을 처리하며, Sale 생성 시에도 입력 데이터의 유효성을 검사가 이루어져야 해요. SaleCreateValidator는 입력 데이터의 유효성을 검사하는 역할을 수행하여 SaleService에서 유효성 검사와 관련된 로직을 분리하고 모듈화할 수 있게 되요.

저는 다음과 같이 결론을 내렸어요.

SaleService와 CreateSaleFacade 중 SaleCreateValidator를 의존해야 하는 객체는 CreateSaleFacade에요. 이유는 CreateSaleFacade가 판매 시스템의 퍼사드 역할을 하고, 검증 및 비지니스 로직의 일부를 처리해야하기 때문이에요. SaleCreateValidator는 Facade 레이어 내에서 검증 작업을 수행하며, SaleService는 판매와 관련된 비지니스 로직을 처리하는 데 중점을 둬야 해요.

마무리

판매 시스템 설계에 대한 고민을 통해, 서비스, 퍼사드 및 유효성 검사기의 역할과 상호작용을 고민한 부분들을 정리해보았어요. 유효성 검사기가 Mapper에 의존하는 것과 객체 간 의존성을 어떻게 관리해야 하는지에 대한 고민을 풀어봤고, 역할 분리와 순환 의존성을 피하기 위한 방법을 제안했어요.

결론적으로, CreateSaleFacade와 SaleService 중 SaleCreateValidator를 의존해야 하는 객체는 CreateSaleFacade이고, Validator에서 Mapper를 의존하지 않는 것으로 결론 지었어요. 이를 통해 시스템의 각 요소가 명확한 책임을 지며, 응집성 있고 유지보수 가능한 설계를 만들어 보았어요. 감사합니다.

profile
Don't ever say it's over if I'm breathing

0개의 댓글