Skip to content

영양성분 계산 로직 리팩토링

koo995 edited this page Sep 21, 2024 · 12 revisions

요구사항 분석

위의 요구사항에 따르면 다이어리를 작성할때, 섭취식품의 정보를 무게(gram)로 선택하거나 식품의 기본단위((1)개, (1)컵, (1)잔, (1)인분)를 선택하냐에 따라서 영양정보를 알맞게 계산해줘야 한다. 예를 들어, 사용자가 사과를 섭취했을때, 사과 1개를 섭취할 수 있거나 257g을 섭취할 수 있다.
또한 영양성분의 알맞은 계산을 위해서 소수점 2자리 까지 표현하더라도 불필요한 소수점표현은 없애야할 필요가 있다.
예를 들어 칼로리는 245.55kcal, 250kcal 와 같이 표현되어야지 250.10kcal 처럼 불필요한 소수점 자리가 있으면 보기 안좋다.

기존의 클래스 정의 V1

식품의 영양성분은 NutritionFacts 라는 클래스가 나타낸다. 그리고 기본단위로 선택했을때 영양성분을 나타내는 NutritionFactsPerOneServing클래스, gram단위로 섭취했을 때의 영양성분을 나타내는 NutritionFactsPerGram클래스를 정의하였다.

@ToString
@Getter
@NoArgsConstructor
public class NutritionFacts {
    private BigDecimal productTotalCalories; // 제품의 총 칼로리
    private BigDecimal productTotalCarbohydrate;
    private BigDecimal productTotalProtein;
    private BigDecimal productTotalFat;
    private BigDecimal productServingSize; // 서빙사이즈
    private String productServingUnit; // 식품의 기본 섭취 단위
    private BigDecimal productTotalWeightGram; // 식품의 총 중량

    @Builder
    public NutritionFacts(BigDecimal productTotalCalories, BigDecimal productTotalCarbohydrate, BigDecimal productTotalProtein, BigDecimal productTotalFat, BigDecimal productServingSize, String productServingUnit, BigDecimal productTotalWeightGram) {
        this.productTotalCalories = productTotalCalories;
        this.productTotalCarbohydrate = productTotalCarbohydrate;
        this.productTotalProtein = productTotalProtein;
        this.productTotalFat = productTotalFat;
        this.productServingSize = productServingSize;
        this.productServingUnit = productServingUnit;
        this.productTotalWeightGram = productTotalWeightGram;
    }
    
    // 단위선택을 gram 으로 선택했을때 사용할 메서드
    public NutritionFactsPerGram calculateNutritionFactsPerGram() {
        return NutritionFactsPerGram.builder()
                .productCaloriesPerGram(stripIfNecessary(
                        productTotalCalories.divide(productTotalWeightGram, SCALE, ROUNDING_MODE)))
                .productCarbohydratePerGram(stripIfNecessary(
                        productTotalCarbohydrate.divide(productTotalWeightGram, SCALE, ROUNDING_MODE)))
                .productProteinPerGram(stripIfNecessary(
                        productTotalProtein.divide(productTotalWeightGram, SCALE, ROUNDING_MODE)))
                .productFatPerGram(stripIfNecessary(
                        productTotalFat.divide(productTotalWeightGram, SCALE, ROUNDING_MODE)))
                .build();
    }

    // 기본단위를 선택했을때 사용할 메서드
    public NutritionFactsPerOneServing calculateNutritionFactsPerOneServingUnit() {
        return NutritionFactsPerOneServing.builder()
                .productCaloriesPerOneServing(stripIfNecessary(
                        productTotalCalories.divide(productServingSize, SCALE, ROUNDING_MODE)))
                .productCarbohydratePerOneServing(stripIfNecessary(
                        productTotalCarbohydrate.divide(productServingSize, SCALE, ROUNDING_MODE)))
                .productProteinPerOneServing(stripIfNecessary(
                        productTotalProtein.divide(productServingSize, SCALE, ROUNDING_MODE)))
                .productFatPerOneServing(stripIfNecessary(
                        productTotalFat.divide(productServingSize, SCALE, ROUNDING_MODE)))
                .build();
    }

    // 불필요한 소수점을 제거하기 위한 메서드
    private BigDecimal stripIfNecessary(BigDecimal value) {
        BigDecimal strippedValue = value.stripTrailingZeros();
        return strippedValue.scale() <= 0 ? strippedValue.setScale(0) : strippedValue;
    }
}
@Getter
@ToString
public class NutritionFactsPerGram {
    private BigDecimal productCaloriesPerGram;

    private BigDecimal productCarbohydratePerGram;

    private BigDecimal productProteinPerGram;

    private BigDecimal productFatPerGram;

    @Builder
    private NutritionFactsPerGram(BigDecimal productCaloriesPerGram, BigDecimal productCarbohydratePerGram, BigDecimal productProteinPerGram, BigDecimal productFatPerGram) {
        this.productCaloriesPerGram = productCaloriesPerGram;
        this.productCarbohydratePerGram = productCarbohydratePerGram;
        this.productProteinPerGram = productProteinPerGram;
        this.productFatPerGram = productFatPerGram;
    }
}
@Getter
@ToString
public class NutritionFactsPerOneServing {
    private BigDecimal productCaloriesPerOneServing;

    private BigDecimal productCarbohydratePerOneServing;

    private BigDecimal productProteinPerOneServing;

    private BigDecimal productFatPerOneServing;

    @Builder
    private NutritionFactsPerOneServing(BigDecimal productCaloriesPerOneServing, BigDecimal productCarbohydratePerOneServing, BigDecimal productProteinPerOneServing, BigDecimal productFatPerOneServing) {
        this.productCaloriesPerOneServing = productCaloriesPerOneServing;
        this.productCarbohydratePerOneServing = productCarbohydratePerOneServing;
        this.productProteinPerOneServing = productProteinPerOneServing;
        this.productFatPerOneServing = productFatPerOneServing;
    }
}

이제 다이어리 작성시 섭취한 단위와 양에 따른 영양성분을 계산한다.

먼저, 각 단위에 맞는 연산을 위한 전략 인터페이스를 정의한다.

public interface NutritionCalculationStrategy {
    CalculatedNutrition calculate(NutritionFacts nutritionFacts, ProductIntakeInfo productIntakeInfo);
}
@RequiredArgsConstructor
@Component
public class NutritionCalculator {
    private final ProductRepository productRepository;
    private final NutritionCalculationStrategyFactory strategyFactory; // 사용자가 선택한 단위의 연산에 해당하는 전략을 가져오는 팩토리

    public CalculatedNutrition calculate(ProductIntakeInfo productIntakeInfo) {
        Product product = productRepository.findById(productIntakeInfo.getProductId())
                .orElseThrow(() -> new BusinessException(INVALID_PRODUCT_ID));
        NutritionFacts nutritionFacts = product.getNutritionFacts();
        
        // productIntakeInfo 안에 있는 사용자가 선택한 단위를 가져온다.
        NutritionCalculationStrategy strategy = strategyFactory.getStrategy(productIntakeInfo.getServingUnit());
        return strategy.calculate(nutritionFacts, productIntakeInfo);
    }
}
Clone this wiki locally