not-a-gardener의 메인인 Plant, Garden 기능을 소개하자면 다음과 같다.
즉, 계산할 게 많다!
계산 과정에서 필요한 값을 DB에서 불러와야 하는데(보유한 비료 목록과 각 비료의 마지막 시비 날짜),
이는 Entity가 아니다.
고로 이번 포스트에서는 JPA를 사용해 Entity가 아닌 값을 조회하는 방법과 관주/시비 주기 계산 로직을 중점으로 다룬다.
서비스를 기준으로 DB에 다녀오는 Dao/Repository,
계산 로직을 담당하는 GardenResponseProvider/각종 Util 클래스를 분리하여 진행했다.
@Getter
@AllArgsConstructor
@NoArgsConstructor
@ToString
public static class Response {
// 식물 정보 (DB에서 바로 나옴)
private PlantDto.Response plant;
// 서버단에서 계산해서 돌려줘야할 데이터들
private Detail gardenDetail;
}
@AllArgsConstructor
@Builder
@Getter
@ToString
public static class Detail {
// 마지막 관수
private WateringDto.Response latestWateringDate;
// 이하 계산해서 넣는 정보
private String anniversary; // 키운지 며칠 지났는지
private int wateringDDay;
// 물주기 정보
private int wateringCode;
// 비료 주기 정보
ChemicalCode chemicalCode;
}
//WateringDto.Response
@AllArgsConstructor
@Builder
@Getter
public static class Response {
private Long id;
private String plantName;
private String chemicalName;
private LocalDate wateringDate;
private Message msg;
}
// PlantDto.Response
@AllArgsConstructor
@Builder
@Getter
@ToString
public static class Response{
private Long id;
private String name;
private String species;
private int recentWateringPeriod;
private int earlyWateringPeriod;
private String medium;
private Long placeId;
private String placeName;
private LocalDate createDate;
private LocalDate birthday;
private LocalDate postponeDate;
private LocalDate conditionDate;
}
기존에 사용하고 있던 기본 Response용 DTO를 재활용하여 만든 DTO라 복잡해보일 수 있다. GardenDto를 도식화하면 다음과 같다.
Plant | Detail |
---|---|
PlantDto | WateringDto |
기타 정보(ex. nn일째 반려중) | 관수 코드, 시비 코드 |
클래스 | 하는 일 |
---|---|
GardenResponseProvider | GardenUtil등으로 DB에서 받아온 데이터를 넘기고, Response용 DTO 조합 |
GardenUtil | GardenResponseProvider에서 넘겨준 데이터로 각종 계산을 수행하고, 계산된 값을 돌려줌 |
식물에 관한 각종 계산 로직이 들어있는 클래스다.
public GardenDto.Detail getGardenDetail(Plant plant, List<ChemicalUsage> latestChemicalUsages) {
// ##### 1. nn일째 반려중
String anniversary = getAnniversary(plant.getBirthday());
// 물주기 기록이 없으면
if (plant.getWaterings() == null || plant.getWaterings().size() == 0) {
// 물주기 정보가 부족해요
return GardenDto.Detail.from(null, anniversary, -1, WateringCode.NO_RECORD.getCode(), null);
}
// 가장 최근 물주기 불러오기
WateringDto.Response latestWatering = WateringDto.Response.from(plant.getWaterings().get(0));
// ##### 2. 비료든 물이든 뭐라도 준지 며칠이나 지났는지 계산
int wateringDDay = getWateringDDay(plant.getRecentWateringPeriod(), plant.getWaterings().get(0).getWateringDate());
// ##### 3. 관수 코드 계산
// 이 식물은 목이 말라요, 흙이 말랐는지 확인해보세요 ... 등의 watering code를 계산
int wateringCode = getWateringCode(plant.getRecentWateringPeriod(), wateringDDay);
// ##### 4. 시비 코드 계산
// chemicalCode: 물을 줄 식물에 대해서 맹물을 줄지 비료/약품 희석액을 줄지 알려주는 용도
// 어떤 비료를 줘야하는지 알려준다
GardenDto.ChemicalCode chemicalCode = getChemicalCode(latestChemicalUsages);
return GardenDto.Detail.from(latestWatering, anniversary, wateringDDay, wateringCode, chemicalCode);
}
// # 1. nn일째 반려중
public String getAnniversary(LocalDate birthday) {
if(birthday == null){
return "";
}
LocalDate today = LocalDate.now();
// 생일이면
if ((today.getMonth() == birthday.getMonth()) && (today.getDayOfMonth() == birthday.getDayOfMonth())) {
return "생일 축하해요";
}
return Duration.between(birthday.atStartOfDay(), today.atStartOfDay()).toDays() + "일 째 반려중";
}
// ##### 2. 비료든 물이든 뭐라도 준지 며칠이나 지났는지 계산
public int getWateringDDay(int recentWateringPeriod, LocalDate lastDrinkingDay) {
// 비료든 물이든 뭐라도 준지 며칠이나 지났는지 계산
int period = (int) Duration.between(lastDrinkingDay.atStartOfDay(), LocalDate.now().atStartOfDay()).toDays();
return recentWateringPeriod - period;
}
// #### 3. 관수 코드 계산
public int getWateringCode(int recentWateringPeriod, int wateringDDay) {
if (recentWateringPeriod == wateringDDay) {
// 오늘 물 줌
return WateringCode.WATERED_TODAY.getCode();
} else if (recentWateringPeriod == 0) {
// 물주기 정보 부족
return WateringCode.NO_RECORD.getCode();
} else if (wateringDDay == 0) {
// 물주기
return WateringCode.THIRSTY.getCode();
} else if (wateringDDay == 1) {
// 물주기 하루 전
// 체크하세요
return WateringCode.CHECK.getCode();
} else if (wateringDDay >= 2) { // 얘가 wateringCode == 4 보다 먼저 걸린다
// 물주기까지 이틀 이상 남음
// 놔두세요
return WateringCode.LEAVE_HER_ALONE.getCode();
} else {
// 음수가 나왔으면 물주기 놓침
// 며칠 늦었는지 알려줌
return wateringDDay;
}
}
물을 줘야할 식물의 경우, 오늘 맹물을 줘야할지 비료를 섞어 줘야할지도 계산해준다.
// -1 0 1
// 비료 사용 안함 맹물 주기 비료주기
public GardenDto.ChemicalCode getChemicalCode(List<ChemicalUsage> latestChemicalUsages) {
// index 필요
// chemical list index에 맞춰 해당 chemical을 줘야하는지 말아야하는지 산출
for (int i = 0; i < latestChemicalUsages.size(); i++) {
ChemicalUsage latestFertilizingInfo = latestChemicalUsages.get(i);
LocalDate latestFertilizedDate = latestFertilizingInfo.getLatestWateringDate();
if (latestFertilizedDate == null) {
// 해당 비료를 준 기록이 아예 없으면
continue;
}
// 해당 비료를 준지 얼마나 지났는지 계산
int period = (int) Duration.between(latestFertilizedDate.atStartOfDay(), LocalDate.now().atStartOfDay()).toDays();
// 시비 날짜와 같거나 더 지났으면
if (period >= (int) latestFertilizingInfo.getPeriod()) {
return new GardenDto.ChemicalCode(latestFertilizingInfo.getChemicalId(), latestFertilizingInfo.getName());
}
}
return null;
}
이때, DB에서 엔티티가 아닌 값을 조회해와야 한다.
@Query 어노테이션을 사용해여 nativeQuery 메소드를 만든다.
// ChemicalRepository
@Query(value = "select chemical_id chemicalId, period, name," +
" (select MAX(watering_date) from watering w where w.plant_id = :plantId and w.chemical_id = c.chemical_id) latestWateringDate" +
" from chemical c where c.gardener_id = :gardenerId" +
" order by period desc", nativeQuery = true)
List<ChemicalUsage> findLatestChemicalizedDayList(@Param("gardenerId") Long gardenerId, @Param("plantId") Long plantId);
chemicalId(PK), 시비 주기, 비료 이름, 그리고 스칼라 서브쿼리를 사용해 해당 비료의 마지막 시비 주기를 가져오는 간단한 SQL이다.
이때 리턴 값으로 사용한 ChemicalUsage는 다음과 같다.
public interface ChemicalUsage {
Long getChemicalId();
int getPeriod();
String getName();
LocalDate getLatestWateringDate();
}
getter만을 이용한 인터페이스를 구현해주면 된다.
이번 기능을 구현하며 JPA가 자꾸 나를 괴롭혔다. (어떻게 하는지 모르겠음, 자꾸 안된다구 함)
하지만 그만큼 나도 JPA를 괴롭혔기 때문에 (안된다는데 계속 시킴) 쌤쌤으로 치기로 한다.