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

Refactor/#34 주차장 데이터 삽입 성능개선 및 외부 api 장애 발생시 처리 로직 추가 #86

Merged
merged 19 commits into from
Jun 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
06f795f
fix: 회원가입 및 비밀번호 조회시에 로그인 필요 없도록 인증코드도 추가
This2sho May 17, 2024
e906096
feat: 주차장 batch로 insert 구현
This2sho May 30, 2024
fde58fa
feat: 주차장 데이터 비동기로 읽어오도록 변경
This2sho May 30, 2024
2c41564
feat: 커낵션 타임 아웃, 리드 타임 아웃 및 실패시 재시도 로직 추가
This2sho Jun 9, 2024
9d06db0
feat: 헬스 체크 추가
This2sho Jun 9, 2024
472db69
feat: 서킷 브레이커 패턴 구현
This2sho Jun 9, 2024
d3391e6
feat: 헬스 체크 구현 및 api 패키지로 이동
This2sho Jun 9, 2024
7e783ef
feat: 서킷 브레이커 어노테이션 적용
This2sho Jun 9, 2024
ff521a7
Merge remote-tracking branch 'origin/main' into Refactor/#34_주차장_데이터_…
This2sho Jun 9, 2024
59b7bfd
refactor: 테스트용 log 삭제
This2sho Jun 9, 2024
90d5d67
test: thread sleep 대신 future get 사용
This2sho Jun 9, 2024
78ababc
fix: 테스트시 flyway 안돌도록 수정
This2sho Jun 9, 2024
81e989e
refactor: ExecutorService Bean으로 사용하도록 변경
This2sho Jun 10, 2024
dc4ae8e
refactor: synchronized 키워드 대신 AtomicInteger 사용하도록 변경
This2sho Jun 10, 2024
017a847
refactor: HealthCheckResponse 패키지 이동
This2sho Jun 10, 2024
7ba4298
refactor: HealthCheckResponse 패키지 이동
This2sho Jun 10, 2024
270e6a2
fix: 좌표계 지정
This2sho Jun 13, 2024
4b85021
refactor: 생성, 수정일 직접 넣도록 수정
This2sho Jun 13, 2024
879a941
refactor: 운영 시간 표현 방식 변경
This2sho Jun 13, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions app-scheduler/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,10 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-actuator'
// 프로메테우스 추가
implementation 'io.micrometer:micrometer-registry-prometheus'

implementation group: 'org.hibernate.orm', name: 'hibernate-spatial', version: '6.3.1.Final'

implementation 'org.springframework.boot:spring-boot-starter-aop'

implementation 'org.springframework.retry:spring-retry:2.0.6'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package com.parkingcomestrue.external.api;

import java.util.concurrent.atomic.AtomicInteger;

public class ApiCounter {

private final int MIN_TOTAL_COUNT;

private AtomicInteger totalCount;
private AtomicInteger errorCount;
private boolean isOpened;

public ApiCounter() {
this.MIN_TOTAL_COUNT = 10;
this.totalCount = new AtomicInteger(0);
this.errorCount = new AtomicInteger(0);
this.isOpened = false;
}

public ApiCounter(int minTotalCount) {
this.MIN_TOTAL_COUNT = minTotalCount;
this.totalCount = new AtomicInteger(0);
this.errorCount = new AtomicInteger(0);
this.isOpened = false;
}

public void countUp() {
while (true) {
int expected = getTotalCount();
int newValue = expected + 1;
if (totalCount.compareAndSet(expected, newValue)) {
return;
}
}
}

public void errorCountUp() {
countUp();
while (true) {
int expected = getErrorCount();
int newValue = expected + 1;
if (errorCount.compareAndSet(expected, newValue)) {
return;
}
}
}

public void reset() {
Copy link
Contributor

Choose a reason for hiding this comment

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

요게 서킷 open 하는 역할인가요??

Copy link
Contributor Author

@This2sho This2sho Jun 10, 2024

Choose a reason for hiding this comment

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

넴 초기(정상) 상태로 돌리는 용도입니다

totalCount = new AtomicInteger(0);
errorCount = new AtomicInteger(0);
isOpened = false;
}

public boolean isOpened() {
return isOpened;
}

public void open() {
isOpened = true;
}

public boolean isErrorRateOverThan(double errorRate) {
int currentTotalCount = getTotalCount();
int currentErrorCount = getErrorCount();
if (currentTotalCount < MIN_TOTAL_COUNT) {
return false;
}
double currentErrorRate = (double) currentErrorCount / currentTotalCount;
return currentErrorRate >= errorRate;
}

public int getTotalCount() {
return totalCount.get();
}
public int getErrorCount() { return errorCount.get(); }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.parkingcomestrue.external.api;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AsyncApiExecutorConfig {

@Bean
public ExecutorService executorService() {
return Executors.newFixedThreadPool(100, (Runnable r) -> {
Thread thread = new Thread(r);
thread.setDaemon(true);
return thread;
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.parkingcomestrue.external.api;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.TimeUnit;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface CircuitBreaker {

int minTotalCount() default 10;
double errorRate() default 0.2;
long resetTime() default 30;
TimeUnit timeUnit() default TimeUnit.MINUTES;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.parkingcomestrue.external.api;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Slf4j
@Aspect
@Component
public class CircuitBreakerAspect {

private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(10);
private final Map<Object, ApiCounter> map = new ConcurrentHashMap<>();

@Around("@annotation(annotation)")
public Object around(ProceedingJoinPoint proceedingJoinPoint, CircuitBreaker annotation) {
ApiCounter apiCounter = getApiCounter(proceedingJoinPoint, annotation.minTotalCount());
if (apiCounter.isOpened()) {
log.warn("현재 해당 {} API는 오류로 인해 중지되었습니다.", proceedingJoinPoint.getTarget());
return null;
}
try {
Object result = proceedingJoinPoint.proceed();
apiCounter.countUp();
return result;
} catch (Throwable e) {
handleError(annotation, apiCounter);
return null;
}
}

private ApiCounter getApiCounter(ProceedingJoinPoint proceedingJoinPoint, int minTotalCount) {
Object target = proceedingJoinPoint.getTarget();
if (!map.containsKey(target)) {
map.put(target, new ApiCounter(minTotalCount));
}
return map.get(target);
}

private void handleError(CircuitBreaker annotation, ApiCounter apiCounter) {
apiCounter.errorCountUp();
if (apiCounter.isErrorRateOverThan(annotation.errorRate())) {
apiCounter.open();
scheduler.schedule(apiCounter::reset, annotation.resetTime(), annotation.timeUnit());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.parkingcomestrue.external.api;

import lombok.Getter;

@Getter
public class HealthCheckResponse {

boolean isHealthy;
int totalSize;

public HealthCheckResponse(boolean isHealthy, int totalSize) {
this.isHealthy = isHealthy;
this.totalSize = totalSize;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.parkingcomestrue.external.api;

public interface HealthChecker {

HealthCheckResponse check();
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
package com.parkingcomestrue.external.coordinate;
package com.parkingcomestrue.external.api.coordinate;

import com.parkingcomestrue.common.domain.parking.Location;
import com.parkingcomestrue.external.coordinate.dto.CoordinateResponse;
import com.parkingcomestrue.external.coordinate.dto.CoordinateResponse.ExactLocation;
import com.parkingcomestrue.external.api.HealthChecker;
import com.parkingcomestrue.external.api.coordinate.dto.CoordinateResponse;
import com.parkingcomestrue.external.api.HealthCheckResponse;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
Expand All @@ -13,7 +14,7 @@
import org.springframework.web.util.UriComponentsBuilder;

@Component
public class CoordinateApiService {
public class CoordinateApiService implements HealthChecker {

private static final String KAKAO_URL = "https://dapi.kakao.com/v2/local/search/address.json";

Expand All @@ -32,12 +33,12 @@ public Location extractLocationByAddress(String address, Location location) {
return location;
}

ExactLocation exactLocation = getExactLocation(result);
CoordinateResponse.ExactLocation exactLocation = getExactLocation(result);
return Location.of(exactLocation.getLongitude(), exactLocation.getLatitude());
}

private ExactLocation getExactLocation(ResponseEntity<CoordinateResponse> result) {
List<ExactLocation> exactLocations = result.getBody().getExactLocations();
private CoordinateResponse.ExactLocation getExactLocation(ResponseEntity<CoordinateResponse> result) {
List<CoordinateResponse.ExactLocation> exactLocations = result.getBody().getExactLocations();
return exactLocations.get(0);
}

Expand All @@ -59,4 +60,15 @@ private boolean isEmptyResultData(ResponseEntity<CoordinateResponse> result) {
Integer matchingDataCount = result.getBody().getMeta().getTotalCount();
return matchingDataCount == 0;
}

@Override
public HealthCheckResponse check() {
UriComponents uriComponents = makeCompleteUri("health check");
ResponseEntity<CoordinateResponse> response = connect(uriComponents);
return new HealthCheckResponse(isHealthy(response), 1);
}

private boolean isHealthy(ResponseEntity<CoordinateResponse> response) {
return response.getStatusCode().is2xxSuccessful();
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.parkingcomestrue.external.coordinate;
package com.parkingcomestrue.external.api.coordinate;

import com.parkingcomestrue.external.support.exception.SchedulerException;
import com.parkingcomestrue.external.support.exception.SchedulerExceptionInformation;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.parkingcomestrue.external.coordinate.dto;
package com.parkingcomestrue.external.api.coordinate.dto;

import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.parkingcomestrue.external.api.parkingapi;

import com.parkingcomestrue.common.domain.parking.Parking;
import com.parkingcomestrue.external.api.HealthChecker;
import java.util.List;

public interface ParkingApiService extends HealthChecker {

default boolean offerCurrentParking() {
return false;
}

List<Parking> read(int pageNumber, int size);

int getReadSize();
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
package com.parkingcomestrue.external.parkingapi.korea;
package com.parkingcomestrue.external.api.parkingapi.korea;

import static com.parkingcomestrue.common.domain.parking.TimeInfo.MAX_END_TIME;

import com.parkingcomestrue.common.domain.parking.BaseInformation;
import com.parkingcomestrue.common.domain.parking.Fee;
Expand Down Expand Up @@ -114,7 +116,7 @@ private TimeInfo toTimeInfo(String beginTime, String endTime) {

private LocalTime parsingOperationTime(String time) {
if (time.equals(HOURS_24)) {
return LocalTime.MAX;
return MAX_END_TIME;
}
try {
return LocalTime.parse(time, TIME_FORMATTER);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package com.parkingcomestrue.external.parkingapi.korea;
package com.parkingcomestrue.external.api.parkingapi.korea;

import com.parkingcomestrue.common.domain.parking.Parking;
import com.parkingcomestrue.external.parkingapi.ParkingApiService;
import com.parkingcomestrue.external.api.CircuitBreaker;
import com.parkingcomestrue.external.api.HealthCheckResponse;
import com.parkingcomestrue.external.api.parkingapi.ParkingApiService;
import java.net.URI;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
Expand Down Expand Up @@ -34,26 +34,20 @@ public KoreaParkingApiService(KoreaParkingAdapter adapter,
}

@Override
public List<Parking> read() throws Exception {
Set<KoreaParkingResponse> result = new HashSet<>();
for (int pageNumber = 1; ; pageNumber++) {
KoreaParkingResponse response = call(pageNumber, SIZE);
String resultCode = response.getResponse().getHeader().getResultCode();
if (NORMAL_RESULT_CODE.equals(resultCode)) {
result.add(response);
continue;
}
break;
}
return result.stream()
.flatMap(response -> adapter.convert(response).stream())
.toList();
@CircuitBreaker
public List<Parking> read(int pageNumber, int size) {
ResponseEntity<KoreaParkingResponse> response = call(pageNumber, size);
return adapter.convert(response.getBody());
}

private KoreaParkingResponse call(int startIndex, int size) {
URI uri = makeUri(startIndex, size);
ResponseEntity<KoreaParkingResponse> response = restTemplate.getForEntity(uri, KoreaParkingResponse.class);
return response.getBody();
@Override
public int getReadSize() {
return SIZE;
}

private ResponseEntity<KoreaParkingResponse> call(int pageNumber, int size) {
URI uri = makeUri(pageNumber, size);
return restTemplate.getForEntity(uri, KoreaParkingResponse.class);
}

private URI makeUri(int startIndex, int size) {
Expand All @@ -66,4 +60,15 @@ private URI makeUri(int startIndex, int size) {
.queryParam("type", RESULT_TYPE)
.build();
}

@Override
public HealthCheckResponse check() {
ResponseEntity<KoreaParkingResponse> response = call(1, 1);
return new HealthCheckResponse(isHealthy(response), response.getBody().getResponse().getBody().getTotalCount());
}

private boolean isHealthy(ResponseEntity<KoreaParkingResponse> response) {
return response.getStatusCode().is2xxSuccessful() && response.getBody().getResponse().getHeader()
.getResultCode().equals(NORMAL_RESULT_CODE);
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.parkingcomestrue.external.parkingapi.korea;
package com.parkingcomestrue.external.api.parkingapi.korea;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
Expand Down Expand Up @@ -29,6 +29,7 @@ public static class Header {
public static class Body {

private List<Item> items;
private int totalCount;

@Getter
@JsonIgnoreProperties(ignoreUnknown = true)
Expand Down
Loading
Loading