Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEAT] 버스위치정보 수집 스케줄러 및 버스 통계 서비스 구현 #114

Merged
merged 32 commits into from
Aug 28, 2024

Conversation

Gyaak
Copy link
Collaborator

@Gyaak Gyaak commented Aug 23, 2024

버스위치정보 수집 스케줄러 기능 추가

  • api call 리트라이 로직 추가
  • api call 병렬로 처리

버스 통계 서비스 구현

  • 버스 위치정보 가공 로직 구현 (BusLocationProcessor)
  • 버스 노선-정거장 통계 조회 구현 (BusViewService)

Todo

  • bus 패키지 구조 리팩토링 : 정적 데이터(정거장 노선 정보), 버스 위치 정보, 버스 통계 등 분리 필요

close #89
close #113

@Gyaak Gyaak added ⌨️ BE Backend 작업 ✨ feat 기능 개발과 관련된 라벨입니다. labels Aug 23, 2024
@Gyaak Gyaak self-assigned this Aug 23, 2024
@Gyaak Gyaak closed this Aug 23, 2024
@Gyaak Gyaak reopened this Aug 23, 2024
@Gyaak Gyaak closed this Aug 23, 2024
@Gyaak Gyaak reopened this Aug 23, 2024
@Gyaak Gyaak changed the title [FEAT] 버스 통계 서비스 구현 [FEAT] 버스위치정보 수집 스케줄러 및 버스 통계 서비스 구현 Aug 23, 2024
server/build.gradle Outdated Show resolved Hide resolved
@@ -7,7 +7,7 @@
@Getter
public enum EndBus implements EnumCodeInterface {
// "0 = RUNNING" or "1 = END"
RUNNING("0"), END("1"), LENT_BUS_END("4");
RUNNING("0"), END("1"), END_BUS_CODE2("2"), LENT_BUS_END("4");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

눈물나는데 답이 따로 없네요. 나중에 찾아보고 변경해봅시다

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

월요일에 전화한번 해보죠...
대체 이게 뭐냐고...

@@ -58,14 +57,10 @@ public List<BusRouteInfoBodyDto> getRouteInfo(String apiRouteId) throws ApiClien
params.add("routeId", apiRouteId);
try {
URI uri = this.getOpenApiUri(path, params);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아래 로직으로 변경됨에 따라 삭제 필요

Comment on lines 16 to 18
@Query(value = "select count(*) from bus_location l")
Integer getRowNum();

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 쿼리에서 l 별칭이 필요 없어 보입니다!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수정하겠습니다.

import com.talkka.server.bus.dao.BusStatEntity;
import com.talkka.server.bus.dao.BusStatRepository;

import jakarta.transaction.Transactional;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 어노테이션도 jakarta로 되어있어서 수정 필요

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수정하겠습니다.

@@ -121,4 +100,31 @@ private URI getOpenApiUri(String path, MultiValueMap<String, String> params) {
.queryParams(params)
.build();
}

// 리트라이 로직을 포함한 api call
private <T> ResponseEntity<T> apiCallWithRetry(String path, MultiValueMap<String, String> params,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 친구 Abstract class도 빼도 좋을 것 같아요.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

리팩토링 진행 하면서 별도 클래스로 분리해 빈으로 등록하는것 고려중입니다.

Comment on lines +108 to +109
final int MAX_ATTEMPTS = 10;
final int RETRY_INTERVAL = 200;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MAX_ATTEMPS 3회정도면 충분해보입니다! 다른 파트에서 주입할 수 있으면 더 좋을듯...?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이건 이번주말에 분리하도록 리팩토링할 예정이라 그때 수정하도록 하겠습니다.

Comment on lines 78 to 102
// old logic
// @Deprecated
// @Transactional
// public void process() {
// List<Integer> apiCallNoList = busLocationRepository.getDistinctApiCallNoList();
// apiCallNoList.sort(Comparator.naturalOrder());
// Map<String, BusLocationEntity> prev = new HashMap<>();
// System.out.println(apiCallNoList);
// for (Integer apiCallNo : apiCallNoList) {
// Map<String, BusLocationEntity> cur = new HashMap<>();
// List<BusLocationEntity> locationList = busLocationRepository.findByApiCallNo(apiCallNo);
// for (BusLocationEntity location : locationList) {
// BusLocationEntity beforeLocation;
// if ((beforeLocation = prev.get(location.getPlateNo())) != null
// // 두정거장 이상 넘어가면 체크하지 않음
// && location.getStationSeq() - beforeLocation.getStationSeq() <= 1 && (
// location.getRemainSeatCount() != -1 && beforeLocation.getRemainSeatCount() != -1)) {
// BusStatEntity statEntity = toBusStatEntity(beforeLocation, location);
// busStatRepository.save(statEntity);
// }
// cur.put(location.getPlateNo(), location);
// }
// prev = cur;
// }
// }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

보존 안해도되면 삭제해도 될 듯 싶습니다.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

혹시 몰라서 냅두겠습니다.

Comment on lines 21 to 23
Integer dayOfWeek,
Integer hour,
Integer minute,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분들은 나중에 Refactoring 할 때 조금 더 엄밀하게 VO 로 체크되면 좋을 것 같아요.

Comment on lines 9 to 23
Long statId,
Long routeId,
Long stationId,
String apiRouteId,
String apiStationId,
Integer beforeSeat,
Integer afterSeat,
Integer seatDiff,
LocalDateTime beforeTime,
LocalDateTime afterTime,
String plateNo,
PlateType plateType,
Integer dayOfWeek,
Integer hour,
Integer minute,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

너무 다 던지는 느낌이 있어서 (일단은 이것도 쓸수도...?) 나중에 필요한 부분만 빼다가 보여주는게 필요합니다

예를 들어,
7800 - 성균관대 - 현재시각(07:00) 의 "들어오는 좌석수"를 요청하면
06:30 ~ 07:30 의 "들어오는 좌석수" - "시간" 의 배열 반환이면 충분합니다.

이런 식으로 해당 정류장에서 몇 명이 타는지... 이런 데이터들을 표현해야합니다.

private final BusLocationProcessor busLocationProcessor;
private final BusLocationRepository busLocationRepository;

@PostMapping("/process/all")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

admin 권한 필요 (bus쪽은 공개되어있습니다.)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

관리자페이지 완성 후 adminController로 옮길 예정입니다.

Comment on lines +19 to +22
public class BusViewController {
private final BusViewService busViewService;

@GetMapping("/now")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

View 라는 이름이 모호하다고 생각합니다.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 부분은 다른 controller 를 통하게 될 것임. 테스트용 임시 컨트롤러로 유지하고 이후 작업에서 변경하도록 할 것임.

Comment on lines +23 to +28
public ResponseEntity<BusViewDto> getNow(
@RequestParam(name = "routeStationId", required = false, defaultValue = "17499") Long routeStationId,
@RequestParam(name = "stationNum", required = false, defaultValue = "5") Integer stationNum,
@RequestParam(name = "timeRange", required = false, defaultValue = "45") Integer timeRange,
@RequestParam(name = "week", required = false, defaultValue = "2") Long week
) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 controller 임시로 둔 것으로 이해해도 될까요? (추후에 제가 수정하게 되는 컨트롤러)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네 맞습니다. 프론트에 맞춰서 수정하시면 됩니다.

@@ -60,7 +60,7 @@ public class BusLocationEntity {
private PlateType plateType;

@Column(name = "remain_seat_count", nullable = false)
private Short remainSeatCount;
private Integer remainSeatCount;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

변경되면서 다른 쪽에서 터질 수 있을 것 같은데 염두에 두겠습니다!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

추후 리팩토링 예정

Comment on lines 37 to 69
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "route_id", nullable = false)
private BusRouteEntity route;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "station_id", nullable = false)
private BusStationEntity station;

@Column(name = "station_seq", nullable = false)
private Integer stationSeq;

@Column(name = "empty_seat", nullable = false)
private Integer emptySeat;

@Column(name = "plate_no", nullable = false, length = 32)
private String plateNo;

@Column(name = "plate_type", nullable = false, length = 1)
@Convert(converter = PlateTypeConverter.class)
private PlateType plateType;

@Column(name = "created_at", nullable = false)
private LocalDateTime createdAt;

@Column(name = "epoch_day", nullable = false)
private Long epochDay;

@Column(name = "time", nullable = false)
private Integer time;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "route_info_id")
private BusPlateStatisticEntity routeInfo;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

실제로 많이 가져올 일이 없어 LAZY 뒀다고 생각해도 될까요

Copy link
Collaborator Author

@Gyaak Gyaak Aug 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네 맞습니다. 보통 BusRemainSeatEntity에서 BusPlateStatisticEntity를 조회하는 경우가 별로 없어 LAZY로 설정했습니다.

Comment on lines 16 to 21
@Getter
@AllArgsConstructor
private static class SeatInfo implements Serializable {
LocalDateTime arrivedTime;
Integer remainSeat;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

따로 이렇게 두신 이유가 궁금합니다..!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Serializable은 삭제했습니다.
arrivedTime과 remainSeat을 한쌍으로 묶어 json으로 내보내기 위해 inner class를 추가했습니다.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이후에 open api docs로 잘 빠지는지 확인 필요...

remainSeatList.add(new SeatInfo(seat.getCreatedAt(), seat.getEmptySeat()));
}
if (!seats.isEmpty()) {
standardTime = seats.get(seats.size() >> 1).getCreatedAt();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요기는 어떤 의미를 가지고 있는지 궁금합니다.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요청한 정거장 기준으로 앞뒤로 빈좌석과 도착시간 등을 조회하게 되는데,
이때 기준이 된 정거장에 도착한 시간을 얻는 코드입니다.
총 정거장의 개수가 항상 홀수이고, 기준이 된 정거장이 중앙에 위치하기 때문에 size/2 중앙 정거장의 인덱스를 가져올 수 있습니다.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. seats -> 변수명으로부터 list 임이 잘 와닿지 않아, 37번 라인의 의미 해석이 어렵습니다.
  2. 비슷한 문제로 seats.size() 부분이 해당 배열의 크기를 구한다는 의미임을 처음에 인지하지 못하는 문제가 있었습니다.
  3. shift 연산자로 표현하는 것이 가독성이 굉장히 좋지 않았습니다.
  4. 결과적으로 seats.get(seats.size() >> 1) 부분은 배열의 중간 요소를 구한다 라는 의미의 method로 분리하는 것이 조금 더 잘 읽힐 것 같습니다.
  5. seats.size() >> 1 또한 왜 해당 행위를 해야하는 지에 대해서 의미적으로 잘 전달되지 않아, private method 화 하는 것이 어떨까 제안드립니다.

Comment on lines +52 to +76
if (location.getRemainSeatCount() == -1) {
continue;
}
try {
if (locationMap.containsKey(location.getPlateNo())) {
var dq = locationMap.get(location.getPlateNo());
if (!dq.isEmpty() && location.getStationSeq() < dq.peekLast().getStationSeq()) {
process(dq);
dq = new ArrayDeque<>();
locationMap.put(location.getPlateNo(), dq);
}
dq.add(location);
} else {
Deque<BusLocationEntity> dq = new ArrayDeque<>();
dq.add(location);
locationMap.put(location.getPlateNo(), dq);
}
} catch (InvalidLocationNotFoundException e) {
log.warn("버스 위치정보 가공 실패 locationId : {}, apiRouteId : {}, apiStationId : {}, plateNo : {} ",
location.getBusLocationId(),
location.getApiRouteId(),
location.getApiStationId(),
location.getPlateNo());
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

나중에 리팩토링 꼭 필요합니다. depth가 너무 깊네요.

* 이 정보는 특정 버스에 대한 다양한 정거장에서의 위치 데이터를 포함합니다.
*/
private void process(Deque<BusLocationEntity> locations) {
assert locations.peek() != null;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분 확실한 부분인가요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네 process 호출 전 locations가 empty인지 확인합니다.

Comment on lines 7 to 12
@Converter(autoApply = true)
public class TimeSlotTypeConverter extends EnumCodeConverter<TimeSlot> {
public TimeSlotTypeConverter() {
super(TimeSlot.class);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

사용하는 Entity에서 왜 이렇게 저장하는지를 명시해주시면 좋을 것 같습니다.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TimeSlot 별로 집계 할 때 사용했었는데 집계 방식이 변경되어 사용하지 않게 되었습니다.
Converter삭제 및 TimeSlot enum 롤백 했습니다.

remainSeatList.add(new SeatInfo(seat.getCreatedAt(), seat.getEmptySeat()));
}
if (!seats.isEmpty()) {
standardTime = seats.get(seats.size() >> 1).getCreatedAt();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. seats -> 변수명으로부터 list 임이 잘 와닿지 않아, 37번 라인의 의미 해석이 어렵습니다.
  2. 비슷한 문제로 seats.size() 부분이 해당 배열의 크기를 구한다는 의미임을 처음에 인지하지 못하는 문제가 있었습니다.
  3. shift 연산자로 표현하는 것이 가독성이 굉장히 좋지 않았습니다.
  4. 결과적으로 seats.get(seats.size() >> 1) 부분은 배열의 중간 요소를 구한다 라는 의미의 method로 분리하는 것이 조금 더 잘 읽힐 것 같습니다.
  5. seats.size() >> 1 또한 왜 해당 행위를 해야하는 지에 대해서 의미적으로 잘 전달되지 않아, private method 화 하는 것이 어떨까 제안드립니다.

Copy link
Contributor

@JuneParkCode JuneParkCode left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@Gyaak Gyaak merged commit 39c6b44 into develop Aug 28, 2024
1 check passed
@JuneParkCode JuneParkCode deleted the feature/#113 branch August 29, 2024 09:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
⌨️ BE Backend 작업 ✨ feat 기능 개발과 관련된 라벨입니다.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[FEAT] 버스 통계 서비스 구현 [FEAT] 버스 위치 스케줄링작업 구현
3 participants