dasd412 / RemakeDiabetesDiaryAPI

혈당일지 api 리메이크
https://www.diabetes-diary.tk/
1 stars 0 forks source link

Querydsl BooleanBuilder로 중복 코드 제거하기 #51

Closed dasd412 closed 2 years ago

dasd412 commented 2 years ago

예를 들어, DietRepositoryImpl 내의 다음 코드들은 BooleanBuilder를 이용하여 중복을 제거할 수 있다.

    List<Diet> findHigherThanBloodSugarBetweenTime(Long writerId, int bloodSugar, LocalDateTime startDate, LocalDateTime endDate);

    List<Diet> findLowerThanBloodSugarBetweenTime(Long writerId, int bloodSugar, LocalDateTime startDate, LocalDateTime endDate);

    List<Diet> findHigherThanBloodSugarInEatTime(Long writerId, int bloodSugar, EatTime eatTime);

    List<Diet> findLowerThanBloodSugarInEatTime(Long writerId, int bloodSugar, EatTime eatTime);
dasd412 commented 2 years ago

FoodRepositoryImpl에선 BooleanBuilder를 사용하여 쿼리를 만들었다. 그런데 해당 쿼리들은 Diet, DiabetesDiary 모두에 쓰일 수 있는 where 조건문들이다. 따라서 PredicateMaker라는 Predicate를 만들어주는 정적 클래스를 작성했다.

public class PredicateMaker {

    private PredicateMaker() {
    }

    /**
     * where 문을 작성할 때, 특히 파라미터의 종류 등에 따라 조건 분기를 하고 싶을 때 Predicate 객체를 사용한다.
     * 작성자 id는 on 절에서 사용되므로 파라미터에서 생략.
     *
     * @param sign       부등호 enum
     * @param bloodSugar 식사 혈당 수치 (만약 식사 혈당에 공백으로 기입되어 있었다면, default 0.
     * @return where 절에 들어가는 조건문 (해당 기간 동안에 식사 혈당과 음식 이름이 함께 작성되었던 일지 중, 부등호 관계와 일치하는 식사 혈당에 기재된 음식들을 리턴할 때 사용한다.)
     */
    public static Predicate decideEqualitySign(InequalitySign sign, int bloodSugar) {
        BooleanBuilder booleanBuilder = new BooleanBuilder();

        switch (sign) {
            case GREATER:
                booleanBuilder.and(QDiet.diet.bloodSugar.gt(bloodSugar));
                break;

            case LESSER:
                booleanBuilder.and(QDiet.diet.bloodSugar.lt(bloodSugar));
                break;

            case EQUAL:
                booleanBuilder.and(QDiet.diet.bloodSugar.eq(bloodSugar));
                break;

            case GREAT_OR_EQUAL:
                booleanBuilder.and(QDiet.diet.bloodSugar.goe(bloodSugar));
                break;

            case LESSER_OR_EQUAL:
                booleanBuilder.and(QDiet.diet.bloodSugar.loe(bloodSugar));
                break;
        }

        return booleanBuilder;
    }

    /**
     * where 문을 작성할 때, 특히 파라미터의 종류 등에 따라 조건 분기를 하고 싶을 때 Predicate 객체를 사용한다.
     *
     * @param startDate 시작 날짜
     * @param endDate   도착 날짜
     * @return where 절에 들어가는 조건문 (해당 기간 사이에 있는가)
     */
    public static Predicate decideBetween(LocalDateTime startDate, LocalDateTime endDate) {
        BooleanBuilder booleanBuilder = new BooleanBuilder();
        booleanBuilder.and(QDiabetesDiary.diabetesDiary.writtenTime.between(startDate, endDate));
        return booleanBuilder;
    }

    /**
     * inner join diet on 절 이후에 쓰인다.
     *
     * @param sign 부등호 (Equal이면 안된다. double에 대해선 ==을 쓸 수 없기 때문)
     * @return 식단의 평균 혈당 값. 단, join 된 것에 한해서다.
     */
    public static Predicate decideAverageOfDiet(InequalitySign sign) {
        checkArgument(sign != InequalitySign.EQUAL && sign != InequalitySign.NONE, "평균을 구할 땐 '=='과 'none' 은 사용할 수 없다.");
        BooleanBuilder booleanBuilder = new BooleanBuilder();
        switch (sign) {
            case GREATER:
                booleanBuilder.and(QDiet.diet.bloodSugar.gt(JPAExpressions.select(QDiet.diet.bloodSugar.avg())
                        .from(QDiet.diet)));
                break;

            case LESSER:
                booleanBuilder.and(QDiet.diet.bloodSugar.lt(JPAExpressions.select(QDiet.diet.bloodSugar.avg())
                        .from(QDiet.diet)));
                break;

            case GREAT_OR_EQUAL:
                booleanBuilder.and(QDiet.diet.bloodSugar.goe(JPAExpressions.select(QDiet.diet.bloodSugar.avg())
                        .from(QDiet.diet)));
                break;

            case LESSER_OR_EQUAL:
                booleanBuilder.and(QDiet.diet.bloodSugar.loe(JPAExpressions.select(QDiet.diet.bloodSugar.avg())
                        .from(QDiet.diet)));
                break;
        }
        return booleanBuilder;
    }

}
dasd412 commented 2 years ago

그리고 DietRepositoryImpl 내에 추상화된 메서드를 작성하였다.

    @Override
    public List<Diet> findDietsWithWhereClause(Long writerId, List<Predicate> predicates) {
        return jpaQueryFactory.select(QDiet.diet)
                .innerJoin(QDiet.diet.diary.writer, QWriter.writer)
                .on(QDiet.diet.diary.writer.writerId.eq(writerId))
                .where(ExpressionUtils.allOf(predicates))
                .fetch();
    }

이렇게 하면 predicates에 적힌 조건대로 where 절 조건문이 추가된다. 따라서 쓸데 없는 중복 코드를 많이 줄일 수 있고, predicates의 조합에 따라 여러가지 조건문을 만들어 낼 수 있어서 훨씬 유연하다.

dasd412 commented 2 years ago

맨 위에 작성되있던 예시 코드들을 차례차례 삭제하면서 findDietsWithWhereClause()로 대체한다. Find Usage를 이용해서 사용했던 부분들을 추적하면 된다. 그리고 기존 테스트 코드들이 있기 때문에 리팩토링 이후에도 정상적으로 작동하는지 빠르게 확인할 수 있다.

dasd412 commented 2 years ago

하다보니 문제가 생겼다. PredicateMaker에 기능이 같은데, 엔티티 참조 관계 때문에 거의 비슷한 코드를 작성해야 한다.

    public static Predicate decideBetween(LocalDateTime startDate, LocalDateTime endDate) {
        BooleanBuilder booleanBuilder = new BooleanBuilder();
        booleanBuilder.and(QDiabetesDiary.diabetesDiary.writtenTime.between(startDate, endDate));
        return booleanBuilder;
    }

    public static Predicate decideBetweenInDiet(LocalDateTime startDate, LocalDateTime endDate) {
        BooleanBuilder booleanBuilder = new BooleanBuilder();
        booleanBuilder.and(QDiet.diet.diary.writtenTime.between(startDate, endDate));
        return booleanBuilder;
    }

리팩토링 해보니까 Predicate문은 각각의 RepositoryImpl에서 처리해주는 게 적합해 보인다. 아니면, 패턴을 이용해서 처리해줘야 할 것 같다. 어떻게 해야 할지 애매하다 흠.. 구현을 숨겨야 좋은데, 이 클래스의 위 두개 메서드를 사용하려면 구현을 봐야 이해가 되는 것이 문제다....