초기 코드
/* request 에 categoryOption 이 누락되었는지 검증 */
private void validateProductRequestContainsNeedOptions(CreateProductRequest request) throws CustomException {
List<CategoryOption> categoryOptions = categoryOptionService.getCategoryOption(request.getCategoryId());
Set<Long> needOptionIdSet = categoryOptions.stream()
.map(CategoryOption::getId)
.collect(Collectors.toSet());
request.getCategoryOptions()
.forEach(co -> needOptionIdSet.remove(co.getOptionId()));
if (!needOptionIdSet.isEmpty())
throw new CustomException(ErrorCode.OPTION_NOT_CONTAINS); // TODO 예외 구체화
}
/* request 에 variantOption 이 누락되었는지 검증 */
private void validateVariantRequestContainsNeedOptions(CreateProductVariantRequest request, Long categoryId) throws CustomException {
List<VariantOption> variantOptions = variantOptionService.getVariantOption(categoryId);
Set<Long> needOptionIdSet = variantOptions.stream()
.map(VariantOption::getId)
.collect(Collectors.toSet());
request.getVariantOptions()
.forEach(vo -> needOptionIdSet.remove(vo.getOptionId()));
if (!needOptionIdSet.isEmpty())
throw new CustomException(ErrorCode.OPTION_NOT_CONTAINS); // TODO 예외 구체화
}
처음 작성한 옵션 검증 로직이다
두개 메서드의 동작방식은 똑같으니
validateProductRequestContainsNeedOptions 기준으로 설명하면
먼저 해당 카테고리의 필요한 옵션 리스트를 가져온다
List<CategoryOption> categoryOptions = categoryOptionService.getCategoryOption(request.getCategoryId());
그리고 해당 Option의 ID의 집합을 만들고 needOptionIdSet request 정보의 optionId를 하나씩 빼준다
마지막으로 needOptionIdSet이 비워져있을경우 통과하는 간단한 로직이다
리팩토링 - 1
일단 CategoryOption과 VariantOption을 검증하는 코드의 공통된점이 많아 ProductUtils 클래스로 공통 부분을 뽑아보려했다
public class ProductUtils {
public static <N, R> void validateOptions(
List<N> needOptions,
Collection<R> requestOptions,
Function<N, Long> needOptionsIdMapper,
Function<R, Long> requestOptionsIdMapper) throws CustomException {
Set<Long> needOptionsIdSet = needOptions.stream()
.map(needOptionsIdMapper)
.collect(Collectors.toSet());
requestOptions.stream()
.map(requestOptionsIdMapper)
.forEach(needOptionsIdSet::remove);
if (!needOptionsIdSet.isEmpty())
throw new CustomException(ErrorCode.OPTION_NOT_CONTAINS); // TODO 예외 구체화
}
}
제네릭을 활용하여 validateOptions 메서드로 뽑아냈다
어떻게 작성해야 할지 감이 안잡혀 GPT의 도움을 받았다
Function이라는것을 처음보았는데 함수를 적어놓는거같다
Function<N, Long> 에서 N은 매개변수 Long은 반환타입으로 유추된다
needOptions엔 VariantOption과 CategoryOption 둘중 어떤 타입이 올지 모르니
getId()를 할수없어 따로 함수를 작성하는것으로 이해하고 넘어갔다
/* request 에 categoryOption 이 누락되었는지 검증 */
private void validateProductRequestContainsNeedOptions(CreateProductRequest request) throws CustomException {
List<CategoryOption> categoryOptions = categoryOptionService.getCategoryOption(request.getCategoryId());
ProductUtils.validateOptions(
categoryOptions,
request.getCategoryOptions(),
CategoryOption::getId,
CategoryOptionsRequest::getOptionId
);
}
/* request 에 variantOption 이 누락되었는지 검증 */
private void validateVariantRequestContainsNeedOptions(CreateProductVariantRequest request, Long categoryId) throws CustomException {
List<VariantOption> variantOptions = variantOptionService.getVariantOption(categoryId);
ProductUtils.validateOptions(
variantOptions,
request.getVariantOptions(),
VariantOption::getId,
VariantOptionRequest::getOptionId
);
}
이런식으로 리팩토링을 해보았다.
하지만 아직 뭔가 불편한 느낌이다
리팩토링 - 2
public static <N, R> void validateOptions()
해당 메서드 제네릭에 N과 R에 각각
VariantOption, CategoryOption
VariantOptionRequest, CategoryOptionRequest 타입만 제한하고싶은 마음이 들었다
하지만 자바에선 인터페이스가 아닌 클래스는 다중상속이 되지않기때문에
인터페이스를 만들어주어야한다
public interface Option {
Long getId();
}
Long getId()는 엔티티에서 @Getter로 이미 구현이 되어있어
이전의 Function<N, Long> needOptionsIdMapper를 삭제해도 된다
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class CategoryOption implements Option {
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class VariantOption implements Option {
이렇게 각각 Option 클래스에 Option interface를 상속시키고
public interface OptionRequest {}
@Getter
@AllArgsConstructor
public class CategoryOptionsRequest implements OptionRequest{
private final Long optionId;
private final Long optionValueId;
}
@Getter
@AllArgsConstructor
public class VariantOptionRequest implements OptionRequest{
private final Long optionId;
private final Long optionValueId;
}
request에도 interface를 상속하던중
CategoryOptionRequest와 VariantOptionRequest를 분리한 의미가 없다는 생각이들어
OptionRequest로 통합하기로하였다
@Getter
@AllArgsConstructor
public class OptionRequest {
private final Long optionId;
private final Long optionValueId;
}
최종
public class ProductUtils {
public static <N extends Option> void validateOptions(
List<N> needOptions,
List<OptionRequest> requestOptions) throws CustomException {
Set<Long> needOptionsIdSet = needOptions.stream()
.map(Option::getId)
.collect(Collectors.toSet());
requestOptions.stream()
.map(OptionRequest::getOptionId)
.forEach(needOptionsIdSet::remove);
if (!needOptionsIdSet.isEmpty())
throw new CustomException(ErrorCode.OPTION_NOT_CONTAINS); // TODO 예외 구체화
}
}
/* request 에 categoryOption 이 누락되었는지 검증 */
private void validateProductRequestContainsNeedOptions(CreateProductRequest request) throws CustomException {
List<CategoryOption> categoryOptions = categoryOptionService.getCategoryOption(request.getCategoryId());
ProductUtils.validateOptions(categoryOptions, request.getCategoryOptions());
}
/* request 에 variantOption 이 누락되었는지 검증 */
private void validateVariantRequestContainsNeedOptions(CreateProductVariantRequest request, Long categoryId) throws CustomException {
List<VariantOption> variantOptions = variantOptionService.getVariantOption(categoryId);
ProductUtils.validateOptions(variantOptions, request.getVariantOptions());
}
최종적으로 이렇게 마무리하였다
그리고 엔티티를 인터페이스에 상속하는것이 좋은 설계인지 잘모르겠고
뭔가 아직 부족한것 같지만 일단 여기서 만족하고 넘어가기로 하였다