JustDoEat
기능이 많다면 퍼사드 패턴(Facade)으로 관리를 해보자, 내돈내산 퍼사드 리팩토링 본문
개요
ProductService에 너무 많은 기능, 메서드들이 들어오게 되었고,
이로 인해 ProductService 혼자서 감당하기에는 책임들이 너무 무거워진다고 생각함.
추가로 다른 서비스에서 ProductService를 의존받아 사용하는 경우에도 상대적으로 가벼운 기능만 필요한데
너무 투머치로 모든 기능들이 보이는 거 같아서 분리를 하고 싶었고,
하나의 클래스가 너무 많은 책임을 가지고 있는 게 싫어서 퍼사드 패턴을 적용해 보기로 했다.
퍼사드를 왜 적용을 하려고 했는가?
퍼사드의 정의처럼, 시스템의 복잡성을 감추고, 필요한 기능만 접근할 수 있도록 분리를 해보려고 한다.
퍼사드 패턴과 책임분리가 적절하게 이루어지지 않던 클래스는 아래와 같다.
ProductService
@Service
@RequiredArgsConstructor
public class ProductServiceImpl implements ProductService {
private final ProductRepository productRepository;
private final ImageService imageService;
private final CategoryService categoryService;
private final ImageService imageService;
private final UserService userService;
@Override
public void registerProduct(ProductRegisterRequest product, List<MultipartFile> images) {}
@Override
public ProductDetailResponse getProductDetail(Long productId) {}
@Override
public List<ProductOverViewResponse> searchProductOverview(ProductSearchRequestDto productSearchRequestDto);
@Override
public void registrationProduct(ProductRegisterRequestDto productRegisterRequestDto, ProductImageCreateRequestDto productImageCreateRequestDto);
@Override
public ProductDetailResponseDto getProductDetail(Long productId);
@Override
public void updateProductViews(Long productId);
@Override
public void updateProduct(ProductUpdateRequestDto productUpdateRequestDto, ProductCategoryUpdateRequestDto productCategoryUpdateRequestDto, ProductImageDeleteRequestDto productImageDeleteRequestDto,ProductImageCreateRequestDto productImageCreateRequestDto);
@Override
public Product createWithRegisterRequestDto
@Override
public void saveProduct(Product product)
@Override
public List<ReviewWithAvgProjection> getProductReviewsByProductId
@Override
public ProductDto getProductById(Long productId)
private User resolveUser(){}
private Category resolveCategory(){}
private Image resolveImage(){}
private ProductImageDtoCollection getProductImagesByProductId
private ImageUrlCollection getImageUrlCollection
private void saveImage(ImageUrlCollection imageUrlCollection, Product product)
}
코드를 이해한다기보다, 직관적으로 봤을 때 너저분하고 내가 무엇을 하려는지 그 누구도 한 번에 알기 힘들 것이다.
일단 코드를 보면, 한 클래스(ProductService)에서
상품 등록, 수정, 삭제, 조회 등의 메서드들이 한 클래스에 있다,
단순히 repository에서 조회를 해오는 게 아닌 말 그대로 비즈니스적인 요소들이 들어간 등록, 삭제, 조회 등의 메서드들이다.
기존 ProductService의 문제점
- 코드가 난잡하다, 메서드들만 나열을 했는데도 정신이 사납다.
- 단일책임의 원칙을 위반한다.
- 여러 서비스들끼리 의존을 난잡하게 한다.
- 다른 도메인, ProfileService에서 단순한 상품조회를 하기 위해 난잡한 ProductService를 의존받아야 한다.
=> ProductRepository를 의존받으면 되잖아? 할 수도 있지만, 나의 경우는 다른 서비스에서 다른 도메인의 Repository 이용하기 위해서는 Repository를 바로 의존받지 말고 앞단에 Service를 두어서 간단한 작업이라도 Service를 의존받아서 Repository에 접근을 하는 게 일관성을 위해서도 좋을 거라 생각했다. - 클래스에서 기능을 수정하면 ProductService를 의존받고 있는 모든 클래스들이 영향을 받을 수 있다고 생각함, 즉 유연성이 현저히 저하되고 결합도가 높은 코드라고 생각.
단일책임을 견고하게 하고, 퍼사드를 적용했을 때의 기대효과(장점)
- 단일책임으로 클래스를 분리함으로 결합도는 떨어지고 응집도가 높아지게 된다.
- 다른 도메인에서 단순히 Product의 조회만 필요하다면 ProductService만 의존받아 쓰면 됨. 즉 기존에 무거운 ProductService를 의존을 받지 않아도 된다.
- 클라이언트 서비스(컨트롤러)에서는 세부적인 구현 말고 ProductService만 의존받아 필요한 메서드만 즉시 실행하면 됨.
- 응집도가 높아지고 가독성이 향상되고 결합도가 느슨해지므로 유지보수력이 향상!
단일책임을 견고하게 하고, 퍼사드를 적용했을 때의 기대효과(단점)
- 클래스를 너무 나누어 오버헤드가 발생할 수 있음.
- 세부적인 구현을 숨김으로, 유연성 저하 가능
=> 이에 대한 해결책으로 단순 crud 혹은 crud작업을 위한 공통되는 메서드를 ProductCrudService를 둠으로 완화.
어떻게 적용을 했는가?
기존 ProductService에 있는 메서드, private메서드들을 기능별로 클래스를 만들어 분리했다.
검색, 등록, 조회, 갱신등 각 기능마다 연관이 있는 메서드는 공통되는 기능으로 나누어 각각 다른 클래스로 몰아버렸다.
공통된 로직 혹은 ProductRepository에 접근이 필요하다면 productCrudService를 따로 두어 다른 도메인 서비스 혹은 다른 상품 서비스에서도 접근이 가능하도록 했다.
ProductService
@Service
@RequiredArgsConstructor
public class ProductServiceImpl implements ProductService {
private final ProductRegistrationService productRegistrationService;
private final ProductSearchService productSearchService;
private final ProductUpdatingService productUpdatingService;
@Override
public List<ProductOverViewResponse> searchProductOverview(ProductSearchRequestDto productSearchRequestDto) { }
@Override
public void registrationProduct(ProductRegisterRequestDto productRegisterRequestDto, ProductImageCreateRequestDto productImageCreateRequestDto){ }
@Override
public ProductDetailResponseDto getProductDetail(Long productId){ }
@Override
public void updateProductViews(Long productId) { }
@Override
public void updateProduct(ProductUpdateRequestDto productUpdateRequestDto, ProductCategoryUpdateRequestDto productCategoryUpdateRequestDto, ProductImageDeleteRequestDto productImageDeleteRequestDto,ProductImageCreateRequestDto productImageCreateRequestDto){ }
}
ProductService는 각 기능별로 분리된 클래스들만 의존성으로 받아와
클라이언트가 필요한 메서드만 실행하도록 해주는 구조로 변경이 되었다.
ProductCrudService
@Service
@RequiredArgsConstructor
public class ProductCrudServiceImpl implements ProductCrudService{
private final ProductRepository productRepository;
@Override
public void saveProduct(Product product) { }
@Override
public ProductDto getProductDtoById(Long productId) { }
@Override
public Product getProductById(Long productId) { }
@Override
public List<ProductOverViewResponse> getProductOverViewByTitle(String title) { }
@Override
public List<ProductOverViewResponse> getProductOverViewByTag(String tag, String tag2, String tag3) { }
@Override
public void updateProductViews(Long productId){ }
@Override
public void updateProductInfo(ProductUpdateRequestDto productUpdateRequestDto) { }
}
ProductRepository와 상호작용을 해야 하는 부분은 ProductCrudService를 따로 두어 관리를 했다.
productRegistrationService
@Service
@RequiredArgsConstructor
public class ProductRegistrationServiceImpl implements ProductRegistrationService{
private final ImageService imageService;
private final ProductCrudService productCrudService;
@Override
public void registerProduct(ProductRegisterRequestDto productRegisterRequestDto, ProductImageCreateRequestDto productImageCreateRequestDto){}
private ImageUrlCollection getImageUrlCollection(ProductImageCreateRequestDto productImageCreateRequestDto){}
private Product createWithRegisterRequestDto(ProductRegisterRequestDto productRegisterRequestDto){}
private void saveProduct(Product product){}
private void saveImage(ImageUrlCollection imageUrlCollection, Product product){}
}
productUpdateService
@Service
@RequiredArgsConstructor
public class ProductUpdateServiceImpl implements ProductUpdatingService{
private final ProductCrudService productCrudService;
private final CategoryBusinessService categoryBusinessService;
private final ImageService imageService;
@Override
public void updateProductViews(Long productId){ }
@Override
public void updateProductInfo(ProductUpdateRequestDto productUpdateRequestDto, ProductCategoryUpdateRequestDto productCategoryUpdateRequestDto, ProductImageDeleteRequestDto productImageDeleteRequestDto, ProductImageCreateRequestDto productImageCreateRequestDto){ }
private Product resolveProduct(Long productId){ }
}
ProductSearchService
@Service
public class ProductSearchServiceImpl implements ProductSearchService {
private final ProductCrudService productCrudService;
private final ImageService imageService;
private final ReviewService reviewService;
private final Map<String, ProductSearchStrategy> productSearchStrategies;
@Autowired
public ProductSearchServiceImpl(ProductCrudService productCrudService, ImageService imageService, ReviewService reviewService, Map<String, ProductSearchStrategy> productSearchStrategies) {
this.productCrudService = productCrudService;
this.imageService = imageService;
this.reviewService = reviewService;
this.productSearchStrategies = productSearchStrategies;
}
@Override
public List<ProductOverViewResponse> searchProductByQuery(ProductSearchRequestDto productSearchRequestDto) {}
@Override
public ProductDetailResponseDto getProductDetail(Long productId) {}
private ProductDto getProductById(Long productId) { }
private ProductImageDtoCollection getProductImagesByProductId(Long productId) { }
private List<ReviewWithAvgProjection> getProductReviewsByProductId(Long productId) { }
}
그 외 상대적으로 복잡한 검색, 수정, 삭제, 등록등의 기능들은 각 기능에 맞게 클래스로 분리를 하였다.
고민했던 점들과 나만의 기준을 새운 부분
"그럼 모든 도메인의 서비스를 이렇게 분리를 하면 오히려 복잡해지는 게 아닌가?"
이 부분이 가장 큰 고민이었다! 여러 블로그 글들을 보고 나와 같은 고민을 하시는 분의 글을 보고 확신이 생겼다.
마지막 구절에 정답은 없다, 큰 프로젝트에 기능에 따른 분리가 없이 구현을 한다면 유지보수에 힘이 들것이고, 작은 프로젝트에 퍼사드를 적용한다면 배보다 배꼽이 커질 수 있다.라는 말을 보고 확신을 가진 거 같았다.
그래서 Category나 Image처럼 큰 기능이 없다면 굳이 분리를 하지 않았다, Category의 경우는 상대적으로 복잡한 서비스 로직이 조금 있기에 CategoryBusinessService로 분리를 했다.
하지만 Product는 큼직한 기능들이 많아서 분리를 했다.
"오히려 전체적으로 봤을 때 private 메서드들이 많아지는 거 같은데 테스트할 때 괜찮을까?"
단위 테스트 시 테스트의 용이성을 떨어뜨리긴 하지만,
내가 쓰고 있는 private 메서드 들은 단순히 다른 서비스의 public 메서드들을 호출하기 때문에 다른 서비스들을 테스트 하면서 동작 확인이 가능하기 때문에 괜찮을 거 같다고 판단을 하였다.
위 코드처럼 다른 서비스의 호출을 하는 역할 밖에 없다.