초기 코드

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

최종적으로 이렇게 마무리하였다
그리고 엔티티를 인터페이스에 상속하는것이 좋은 설계인지 잘모르겠고
뭔가 아직 부족한것 같지만 일단 여기서 만족하고 넘어가기로 하였다

Junyoung.dev