Skip to content

Commit

Permalink
[#19] 세부정보 저장 비동기 처리 (#20)
Browse files Browse the repository at this point in the history
* feat: 도로명주소 API 추가 및 중간 작업 저장

- 비동기 작업을 위해 CompletableFuture 추가
- API에 있던 변환 작업 서비스 레이어로 이동

* feat: rate limiter 및 배치처리를 통한 세부정보 저장 추가

- TPS가 30으로 차후 rate limiter를 조절할 필요가 있음
- 코드 적용이 가능해지면 필요없는 코드 삭제 예정

* feat: 재시도 로직도 비동기로 처리

- 1시간 2분 소요
  • Loading branch information
JHwan96 authored Oct 2, 2024
1 parent 558142e commit 0c9f3be
Show file tree
Hide file tree
Showing 15 changed files with 462 additions and 73 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.healthmap.db.medicalfacility;

import jakarta.transaction.Transactional;
import org.locationtech.jts.geom.Point;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
Expand All @@ -18,6 +19,7 @@ public interface MedicalFacilityRepository extends JpaRepository<MedicalFacility
List<String> findAllId();

@Modifying
@Transactional
@Query("UPDATE MedicalFacilityEntity m SET " +
"m.parking = COALESCE(:parking, m.parking), " +
"m.parkingEtc = COALESCE(:parkingEtc, m.parkingEtc), " +
Expand Down
5 changes: 4 additions & 1 deletion openapi/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ dependencies {

implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.hibernate:hibernate-spatial:6.6.0.Final'
implementation 'com.google.code.gson:gson:2.11.0'
implementation 'com.google.code.gson:gson:2.11.0' // 차후 제거

//rate limiter
implementation 'com.bucket4j:bucket4j-core:8.10.1'

testImplementation platform('org.junit:junit-bom:5.10.0')
testImplementation 'org.junit.jupiter:junit-jupiter'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
package org.healthmap.openapi.api;

import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import lombok.extern.slf4j.Slf4j;
import org.healthmap.openapi.config.KeyProperties;
import org.healthmap.openapi.config.UrlProperties;
import org.healthmap.openapi.dto.FacilityDetailDto;
import org.healthmap.openapi.dto.FacilityDetailJsonDto;
import org.healthmap.openapi.dto.FacilityDetailUpdateDto;
import org.healthmap.openapi.error.OpenApiErrorCode;
import org.healthmap.openapi.exception.OpenApiProblemException;
import org.healthmap.openapi.pattern.PatternMatcherManager;
import org.healthmap.openapi.utility.RateLimitBucket;
import org.healthmap.openapi.utility.XmlUtils;
import org.springframework.stereotype.Component;
import org.w3c.dom.Document;
Expand All @@ -19,30 +24,45 @@
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.IOException;
import java.net.URI;
import java.net.URL;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

@Slf4j
@Component
public class FacilityDetailInfoApi {
private final KeyProperties keyProperties;
private final UrlProperties urlProperties;
private final PatternMatcherManager patternMatcherManager;
private final ObjectMapper objectMapper;
private final RateLimitBucket rateLimitBucket;
private final ExecutorService executorService;
private final HttpClient client;

public FacilityDetailInfoApi(KeyProperties keyProperties, UrlProperties urlProperties, PatternMatcherManager patternMatcherManager) {
public FacilityDetailInfoApi(KeyProperties keyProperties, UrlProperties urlProperties, PatternMatcherManager patternMatcherManager, RateLimitBucket rateLimitBucket) {
this.keyProperties = keyProperties;
this.urlProperties = urlProperties;
this.patternMatcherManager = patternMatcherManager;
this.rateLimitBucket = rateLimitBucket;
this.objectMapper = new ObjectMapper();
ExecutorService httpExecutorService = Executors.newFixedThreadPool(30);
this.executorService = Executors.newFixedThreadPool(100);
this.client = HttpClient.newBuilder().executor(httpExecutorService).build();
}

public FacilityDetailDto getFacilityDetailInfo(String code) {
public FacilityDetailUpdateDto getFacilityDetailInfo(String code) {

FacilityDetailDto facilityDetailDto = null;
FacilityDetailUpdateDto facilityDetailDto = null;
String url = urlProperties.getDetailUrl()
+ "?serviceKey=" + keyProperties.getServerKey() //Service Key
+ "&ykiho=" + code;
Expand Down Expand Up @@ -93,7 +113,7 @@ public FacilityDetailDto getFacilityDetailInfo(String code) {

String emergencyDay = XmlUtils.getStringFromElement("emyDayYn", element);
String emergencyNight = XmlUtils.getStringFromElement("emyNgtYn", element);
facilityDetailDto = FacilityDetailDto.of(
facilityDetailDto = FacilityDetailUpdateDto.of(
code, parking, parkingEtc, treatmentMon, treatmentTue, treatmentWed, treatmentThu, treatmentFri, treatmentSat, treatmentSun,
receiveWeek, receiveSat, lunchWeek, lunchSat, noTreatmentSun, noTreatmentHoliday, emergencyDay, emergencyNight
);
Expand All @@ -103,14 +123,64 @@ public FacilityDetailDto getFacilityDetailInfo(String code) {
throw new OpenApiProblemException(OpenApiErrorCode.INPUT_OUTPUT_ERROR);
} catch (Exception e) {
log.info(code);
e.printStackTrace(); //제거 예정
throw new OpenApiProblemException(OpenApiErrorCode.SERVER_ERROR);
}
return facilityDetailDto;
}

public FacilityDetailDto getFacilityDetailInfoFromJson(String code) {
FacilityDetailDto facilityDetailDto = null;
// OpenApi로부터 데이터 받아오는 역할만 부여
public CompletableFuture<FacilityDetailJsonDto> getFacilityDetailJsonDtoFromApi(String code) {
String apiUrl = urlProperties.getDetailUrl()
+ "?serviceKey=" + keyProperties.getServerKey() //Service Key
+ "&ykiho=" + code
+ "&_type=json";

HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(apiUrl))
.GET()
.build();

return CompletableFuture.supplyAsync(() -> {
try {
rateLimitBucket.consumeWithBlock(1);
return client.sendAsync(request, HttpResponse.BodyHandlers.ofString());
} catch (InterruptedException e) {
log.error(e.getMessage());
throw new RuntimeException("Rate limit exceeded", e);
}
}, executorService).thenCompose(future -> future)
.thenApply(HttpResponse::body)
.thenApply(this::getFacilityDetailJsonDto)
.thenApply(x -> {
if (x != null) {
x.saveCodeIntoDto(code);
}
return x;
})
.exceptionally(ex -> {
if (ex.getMessage().contains("LIMITED_NUMBER_OF_SERVICE_REQUESTS_PER_SECOND_EXCEEDS_ERROR")) {
log.warn("초당 요청 한도 초과, 재시도...");
// 재시도 로직 또는 지연 후 재시도
return CompletableFuture.supplyAsync(() -> {
try {
return getFacilityDetailJsonDtoFromApi(code).get();
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(e);
}
},
CompletableFuture.delayedExecutor(1, TimeUnit.SECONDS)).join();
}
log.error("error 발생, null 반환: {}", ex.getMessage());
return null;
});
}


// Json으로 OpenAPI 가져오기
// Sync
//TODO: 변경 예정
public FacilityDetailUpdateDto getFacilityDetailInfoFromJson(String code) {
FacilityDetailUpdateDto facilityDetailDto = null;
String apiUrl = urlProperties.getDetailUrl()
+ "?serviceKey=" + keyProperties.getServerKey() //Service Key
+ "&ykiho=" + code
Expand All @@ -123,6 +193,7 @@ public FacilityDetailDto getFacilityDetailInfoFromJson(String code) {

HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
String body = response.body();
log.info("body : {}", body);

JsonObject jsonObject = JsonParser.parseString(body).getAsJsonObject();
JsonObject jsonArray = jsonObject.getAsJsonObject("response")
Expand Down Expand Up @@ -164,7 +235,7 @@ public FacilityDetailDto getFacilityDetailInfoFromJson(String code) {
String emergencyDay = getJsonElement(item, "emyDayYn");
String emergencyNight = getJsonElement(item, "emyNgtYn");

facilityDetailDto = FacilityDetailDto.of(
facilityDetailDto = FacilityDetailUpdateDto.of(
code, parking, parkingEtc, treatmentMon, treatmentTue, treatmentWed, treatmentThu, treatmentFri, treatmentSat, treatmentSun,
receiveWeek, receiveSat, lunchWeek, lunchSat, noTreatmentSun, noTreatmentHoliday, emergencyDay, emergencyNight
);
Expand All @@ -173,12 +244,32 @@ public FacilityDetailDto getFacilityDetailInfoFromJson(String code) {
}

} catch (Exception e) {
e.printStackTrace(); //제거 예정
throw new OpenApiProblemException(OpenApiErrorCode.SERVER_ERROR);
}
return facilityDetailDto;
}

private FacilityDetailJsonDto getFacilityDetailJsonDto(String jsonBody) {
FacilityDetailJsonDto facilityDetailJsonDto = null;
try {
JsonNode path = objectMapper.readTree(jsonBody)
.path("response")
.path("body")
.path("items");
if (!path.isEmpty()) {
JsonNode itemNode = path.path("item");
facilityDetailJsonDto = objectMapper.treeToValue(itemNode, FacilityDetailJsonDto.class);
}
} catch (JsonParseException je) {
log.error(je.getMessage(),je);
throw new OpenApiProblemException(OpenApiErrorCode.SERVER_ERROR, "LIMITED_NUMBER_OF_SERVICE_REQUESTS_PER_SECOND_EXCEEDS_ERROR");
} catch (Exception e) { // return null 할지 생각
log.error("getFacilityDetailJson error : {}", e.getMessage(), e);
throw new OpenApiProblemException(OpenApiErrorCode.SERVER_ERROR, e.getMessage());
}
return facilityDetailJsonDto;
}

private String getTreatmentTimeFromElement(JsonObject obj, String startTimeKey, String endTimeKey) {
String startTime = getJsonElement(obj, startTimeKey);
if (startTime == null || startTime.length() != 4) {
Expand Down
11 changes: 0 additions & 11 deletions openapi/src/main/java/org/healthmap/openapi/config/JpaConfig.java

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package org.healthmap.openapi.converter;

import org.healthmap.db.medicalfacility.MedicalFacilityEntity;
import org.healthmap.openapi.dto.FacilityDetailDto;
import org.healthmap.openapi.dto.FacilityDetailUpdateDto;
import org.healthmap.openapi.error.OpenApiErrorCode;
import org.healthmap.openapi.exception.OpenApiProblemException;

Expand All @@ -10,7 +10,7 @@
import java.util.stream.Collectors;

public class FacilityDetailConverter {
public static MedicalFacilityEntity toEntity(FacilityDetailDto dto) {
public static MedicalFacilityEntity toEntity(FacilityDetailUpdateDto dto) {
return Optional.ofNullable(dto)
.map(x -> MedicalFacilityEntity.of(
dto.getCode(), null, null, null, null, null, null, null, null,
Expand All @@ -21,7 +21,7 @@ public static MedicalFacilityEntity toEntity(FacilityDetailDto dto) {
)).orElseThrow(() -> new OpenApiProblemException(OpenApiErrorCode.NULL_POINT));
}

public static List<MedicalFacilityEntity> toEntityList(List<FacilityDetailDto> dtoList) {
public static List<MedicalFacilityEntity> toEntityList(List<FacilityDetailUpdateDto> dtoList) {
return Optional.ofNullable(dtoList)
.map(x -> x.stream()
.map(FacilityDetailConverter::toEntity)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package org.healthmap.openapi.dto;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;

@Getter
@NoArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
@ToString
public class FacilityDetailJsonDto {
private String code;
private String parkXpnsYn;
private String parkEtc;
private String trmtMonStart; // 진료시간_월_시작
private String trmtMonEnd; // 진료시간_월_시작
private String trmtTueStart;
private String trmtTueEnd;
private String trmtWedStart;
private String trmtWedEnd;
private String trmtThuStart;
private String trmtThuEnd;
private String trmtFriStart;
private String trmtFriEnd;
private String trmtSatStart;
private String trmtSatEnd;
private String trmtSunStart;
private String trmtSunEnd;
private String rcvWeek; // 접수시간_평일
private String rcvSat; // 접수시간_토요일
private String lunchWeek; // 점심시간_평일
private String lunchSat; // 점심시간_토
private String noTrmtSun; // 일요일 휴진
private String noTrmtHoli; // 공휴일 휴진
private String emyDayYn; // 주간 응급실 운영 여부
private String emyNgtYn; // 야간 응급실 운영 여부

public void saveCodeIntoDto(String code) {
this.code = code;
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
package org.healthmap.openapi.dto;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;

@Getter
@NoArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
@ToString // 차후 필요없게될 시 삭제
public class FacilityDetailDto {
public class FacilityDetailUpdateDto {
private String code; // ykiho(암호 요양 기호)
private String parking;
private String parkingEtc;
Expand All @@ -28,7 +30,7 @@ public class FacilityDetailDto {
private String emergencyNight;


private FacilityDetailDto(String code, String parking, String parkingEtc, String treatmentMon, String treatmentTue, String treatmentWed, String treatmentThu, String treatmentFri, String treatmentSat, String treatmentSun, String receiveWeek, String receiveSat, String lunchWeek, String lunchSat, String noTreatmentSun, String noTreatmentHoliday, String emergencyDay, String emergencyNight) {
private FacilityDetailUpdateDto(String code, String parking, String parkingEtc, String treatmentMon, String treatmentTue, String treatmentWed, String treatmentThu, String treatmentFri, String treatmentSat, String treatmentSun, String receiveWeek, String receiveSat, String lunchWeek, String lunchSat, String noTreatmentSun, String noTreatmentHoliday, String emergencyDay, String emergencyNight) {
this.code = code;
this.parking = parking;
this.parkingEtc = parkingEtc;
Expand All @@ -49,17 +51,18 @@ private FacilityDetailDto(String code, String parking, String parkingEtc, String
this.emergencyNight = emergencyNight;
}

public static FacilityDetailDto of(
public static FacilityDetailUpdateDto of(
String code, String parking, String parkingEtc, String treatmentMon, String treatmentTue, String treatmentWed,
String treatmentThu, String treatmentFri, String treatmentSat, String treatmentSun, String receiveWeek,
String receiveSat, String lunchWeek, String lunchSat, String noTreatmentSun, String noTreatmentHoliday,
String emergencyDay, String emergencyNight
) {
return new FacilityDetailDto(
return new FacilityDetailUpdateDto(
code, parking, parkingEtc, treatmentMon, treatmentTue, treatmentWed,
treatmentThu, treatmentFri, treatmentSat, treatmentSun, receiveWeek,
receiveSat, lunchWeek, lunchSat, noTreatmentSun,noTreatmentHoliday,
emergencyDay, emergencyNight
);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

@RestControllerAdvice
@Slf4j
public class OpenApiExceptionHandler {
public class ApiExceptionHandler {
@ExceptionHandler(value = OpenApiProblemException.class)
public ResponseEntity<Object> openApiProblemException(OpenApiProblemException e) {
OpenApiErrorCode errorCode = e.getOpenApiErrorCode();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@
@Getter
public class OpenApiProblemException extends RuntimeException {
private final OpenApiErrorCode openApiErrorCode;
private final String errorMessage;
private final String message;

public OpenApiProblemException(OpenApiErrorCode openApiErrorCode) {
this.openApiErrorCode = openApiErrorCode;
this.errorMessage = openApiErrorCode.getMessage();
this.message = openApiErrorCode.getMessage();
}

public OpenApiProblemException(OpenApiErrorCode openApiErrorCode, String errorMessage) {
this.openApiErrorCode = openApiErrorCode;
this.errorMessage = errorMessage;
this.message = errorMessage;
}
}
Loading

0 comments on commit 0c9f3be

Please sign in to comment.