Floney

[플로니] 초기화된 캘린더 응답 방식 포함 시켜 만들기

딤섬뮨 2023. 5. 30. 11:11
728x90

우리의 가계부는 월별 조회 기능을 제공하고 있다.

다음과 프론트에서 요청을 하면 

- 하루의 총 지출

- 하루의 총수입

- 월별 총지출

- 월별 총수입

 

의 데이터를 담아 return 해주어야 한다.

 

이를 위해서 API명세서를 다음과 같이 작성해 주었다.

API명세서

 

예를 들어 2023-05-01이 들어오면, DB에서 5월에 등록된 가계부 내역들을 모두 조회해야 한다.

 

1. 기간 정하기

이를 위해 2023-05-01부터 2023-05-31이라는 기간을 지정해야 했다.

LocalDate의 기능 중에는 해당 년-월의 첫날과 끝날을 알려주는 기능이 있다.

 

DateFormatter라는 util 기능을 정의하여, dates라는 map리스트를 만들어주었다.

public class DateFormatter {

    public static final String START = "start";
    public static final String END = "end";

    public static Map<String, LocalDate> getDate(String targetDate) {
        LocalDate date = LocalDate.parse(targetDate, DateTimeFormatter.ISO_DATE);
        Map<String, LocalDate> dates = new HashMap<>();
        YearMonth yearMonth = YearMonth.from(date);
        LocalDate lastCurrentDate = yearMonth.atEndOfMonth();

        dates.put(START, date);
        dates.put(END, lastCurrentDate);
        return dates;
    }
}

key : value = "start" : 2023-05-01

key : value = "end" : 2023-05-31

 

이런 식으로 들어갈 것이다.

 

    @Override
    @Transactional(readOnly = true)
    public MonthLinesResponse showByMonth(String bookKey, String date) {
        Map<String, LocalDate> dates = DateFormatter.getDate(date);
        LocalDate start = dates.get(START);
        LocalDate end = dates.get(END);

        return MonthLinesResponse.of(daysExpense(bookKey, start, end)
            , totalExpense(bookKey, start, end));
    }

 

2. 각 날짜별 총수입, 총지출

그렇게 구해진 기간을 쿼리문에 넣어서 던져준다.

 

쿼리문은 다음과 같은 데이터를 뽑아야 한다.

1. 해당 기간 내의 각 날짜별 총수입. 총지출

2. 가계부는 bookKey에 대응되는 특정 가계부

 

그래서, lineDate.between을 통해 기간을 지정해 주고 수입과 지출 중에 하나라면 수입과 지출 group별로 return을 하게끔 했다. 

 @Override
    public List<BookLineExpense> dayIncomeAndOutcome(String bookKey, LocalDate start, LocalDate end) {
        return jpaQueryFactory.select(
                new QBookLineExpense(
                    bookLine.lineDate,
                    bookLine.money.sum(), //합계를 구하기
                    bookLineCategory.name
                )
            )
            .from(bookLine)
            .innerJoin(bookLine.book, book)
            .innerJoin(bookLine.bookLineCategories, bookLineCategory)
            .where(
                bookLine.status.eq(ACTIVE),
                book.status.eq(ACTIVE),
                bookLine.lineDate.between(start, end),
                bookLineCategory.name.in(INCOME.getKind(), //수입과 지출의 내역만 뽑기
                    OUTCOME.getKind()),
                book.bookKey.eq(bookKey)
            )
            .groupBy(bookLine.lineDate, bookLineCategory.name) // 수입과 지출 그리고 날짜별로 그룹화
            .fetch();
    }

3. 월 별 총지출, 총수입

밑에 월별 총지출, 총수입에 대한 쿼리는 메서드를 별도로 만들었다

 

   @Override
    public List<TotalExpense> totalExpense(String bookKey, LocalDate start, LocalDate end) {
        return jpaQueryFactory.select(
                new QTotalExpense(
                    bookLine.money.sum(),
                    bookLineCategory.name
                )
            )
            .from(bookLine)
            .innerJoin(bookLine.book, book)
            .innerJoin(bookLine.bookLineCategories, bookLineCategory)
            .where(
                bookLine.lineDate.between(start, end),
                bookLineCategory.name.in(INCOME.getKind(), OUTCOME.getKind()),
                book.bookKey.eq(bookKey),
                book.status.eq(ACTIVE),
                bookLine.status.eq(ACTIVE)
            )
            .groupBy(bookLineCategory.name)
            .orderBy(bookLineCategory.name.asc())
            .fetch();
    }

 

그렇게 되면 이런 식으로 요청이 온다


하지만, 프런트에서 수정을 원했다.

현재는 가계부 내역에 있는 데이터만 뽑아서 준다.

즉, 2023-05-01의 데이터가 없다면, response body에는 2023-05-01 관련 데이터가 안 들어간다.

또한, 2023-05-04에 수입 내역만 있고 지출 내역이 없다면 수입 내역만 보내진다.

 

프런트에서는 다음과 같은 response를 원했다.

- 모든 날짜를 다 보내준다

- 모든 날짜의 총수입/  총지출을 보내준다

- 만약 해당 날짜에 내역이 없다면 money를 0으로 표시한다.

 

그렇게 나는 리팩토링을 시작했다.

 

우선, 생각해 본 사항으로는

 

미리 money가 0원인 데이터를 월의 요일만큼 만든다.

예를 들어 5월은 31일까지 존재하니 1일부터 31일까지의 INCOME / OUTCOME을 담은 BookLineResponse를 만들어 놓는다.

public class BookLineExpense {
    private final LocalDate date;
    private final Long money;
    private final AssetType assetType;

이를 Map형태로 저장해놓는다.

key : value = 2023-05-01 : List <BookLineResponse>

List <BookLineResponse>에는 각 날짜의 INCOME/OUTCOME 두 개씩 담길 것이다. 

예를 들어 다음과 같이 말이다.

{2023-05-31=[BookLineExpense(date=2023-05-31, money=0, assetType=INCOME),
BookLineExpense(date=2023-05-31, money=0, assetType=OUTCOME)],

2023-05-30=[BookLineExpense(date=2023-05-30, money=0, assetType=INCOME),
BookLineExpense(date=2023-05-30, money=0, assetType=OUTCOME)],

그 후 DB에서 DB데이터를 가지고 오면,  DB의 날짜를 key로 이용해 해당하는 default List <BookResponse>를 가지고 온다

그 후 DB의 데이터가 INCOME 데이터라면 List <BookResponse>의 INCOME 대신 DB의 INCOME 데이터를 넣어준다.


그런데 문제가 있다.

List의 특성상 INCOME의 데이터를  특정할 수 없다. INCOME이 몇 번째에 있는지 알아서 이를 DB데이터로 바꿔주어야 하는데 , 그냥 0 아니면 1이라는 인덱스 밖에 정보가 없다.

즉 LocalDate를 key로 이용해 찾는다고 하더라도 몇 번째가 INCOME인지 O(1)로 알아내기 힘들다.

 

아예 Map에서 value가 List가 아니라 원자값이면, 좋을 것 같았다.

 

흠 그렇다면 복합 키를 이용해 LocalDate, INCOME 이런 식으로 key를 만들면 좋겠지만 Map은 복합키가 없다

하지만 우리는 DTO가 있다! 아 key에 DTO를 새로 정의해서 넣어볼까?

다음과 같은 MonthKey는 Map에서 key로 들어갈 것이다. 이렇게 하면 두 개의 데이터를 Key에 넣을 수 있게 된다

@AllArgsConstructor(access = AccessLevel.PRIVATE)
@EqualsAndHashCode
public class MonthKey {
    private LocalDate date;
    private AssetType assetType;

    public static MonthKey of(LocalDate date, AssetType assetType) {
        return new MonthKey(date, assetType);
    }

    public static MonthKey toMonthKey(BookLineExpense expense) {
        return new MonthKey(expense.getDate(),expense.getAssetType());
    }

}

2023-05-01부터 2023-05-31까지 0원으로 초기화시켜주는 함수를 다음과 같이 정의한다.

MonthKey안에 LocalDate와 AssetType을 가지게 함으로써, Map을 사용할 수 있었다.

List의 인덱싱으로 찾아야 하는 번거로움을 극복했다.

public static Map<MonthKey, BookLineExpense> initDates(String targetDate) {
        Map<String, LocalDate> dates = getDate(targetDate);
        Map<MonthKey, BookLineExpense> initDates = new LinkedHashMap<>();

        LocalDate currentDate = dates.get(START);
        while (!currentDate.isAfter(dates.get(END))) {
            initDates.put(MonthKey.of(currentDate, INCOME),
                BookLineExpense.initExpense(currentDate, INCOME));

            initDates.put(MonthKey.of(currentDate, OUTCOME),
                BookLineExpense.initExpense(currentDate, OUTCOME));

            currentDate = currentDate.plusDays(NEXT_DAY);
        }

        return initDates;
    }

 

이 초기화된 Map과 DB에서 가져온 데이터를 토대로, 마지막에 원하는 응답을 만들어주는 MonthLinesResponseDto 내에 reflectDB라는 함수를 정의한다.

 

이는 DB에 내역이 있으면 -> MonthKey를 만들어 -> 초기화된 Map에서 key값으로 value를 찾을 수 있다.

이 초기화된 value대신에 DB의 내역을 갱신시켜주면 된다.

 

단, 객체의 참조값이 아닌 내용을 참조하기 위해 MonthKey에 @EqualsAndHash 정의 필수

 public static List<BookLineExpense> reflectDB(String monthDate, List<BookLineExpense> dayExpenses) {
        Map<MonthKey, BookLineExpense> dates = DateFactory.initDates(monthDate); //0으로 초기화된 Map을 가져온다
        for (BookLineExpense dbExpense : dayExpenses) {
            dates.replace(MonthKey.toMonthKey(dbExpense), dbExpense);
        }
        return dates.values()
            .stream()
            .toList();
    }

이 함수를 호출해서, expenses에 넣어주면 된다

  public static MonthLinesResponse of(String monthDate, List<BookLineExpense> dayExpenses, Map<String, Long> totalExpenses) {
        return MonthLinesResponse.builder()
            .expenses(reflectDB(monthDate, dayExpenses))
            .totalIncome(totalExpenses.getOrDefault(INCOME, DEFAULT_MONEY))
            .totalOutcome(totalExpenses.getOrDefault(OUTCOME, DEFAULT_MONEY))
            .build();
    }

원하는 바로 잘 나온다.

48 refactor 캘린더 조회 응답 수정 by sienna011022 · Pull Request #49 · Floney-2023/Floney-Server (github.com)

 

48 refactor 캘린더 조회 응답 수정 by sienna011022 · Pull Request #49 · Floney-2023/Floney-Server

📚 이슈 번호 #48 💬 기타 사항 의논할 내용 포스팅 마무리 하고, 여기에 링크 달아 놓을게요!

github.com

 

728x90