우리의 가계부는 월별 조회 기능을 제공하고 있다.
다음과 프론트에서 요청을 하면
- 하루의 총 지출
- 하루의 총수입
- 월별 총지출
- 월별 총수입
의 데이터를 담아 return 해주어야 한다.
이를 위해서 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
'Floney' 카테고리의 다른 글
[플로니] 서버야 왜 자꾸 죽니 (3) | 2023.10.03 |
---|---|
[플로니] Spring Batch로 이월 설정 구현하기 (0) | 2023.07.12 |
[플로니] default 설정을 했는데 null이 나와요 (0) | 2023.05.09 |
플로니 - 끝나지 않은 카테고리 삽질기(상속관계 @Builder) (0) | 2023.04.30 |
플로니 - 카테고리 조회하기 시행착오 (2) | 2023.04.26 |