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 12 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,59 @@
package com.parkingcomestrue.external.api;

public class ApiCounter {

private final double MIN_TOTAL_COUNT;

private double totalCount;
private double errorCount;
private boolean isClosed;

Choose a reason for hiding this comment

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

double 인 이유가 있을까요? 정수형 같아보여서용!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

그렇네여.. 수정했습니다!


public ApiCounter() {
this.MIN_TOTAL_COUNT = 10;
this.totalCount = 0;
this.errorCount = 0;
this.isClosed = false;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

얘네는 private double totalCount = 0; 이렇게 선언해도 무방할듯하네요. 이렇게 가도 좋습니다~


public ApiCounter(double minTotalCount) {
this.MIN_TOTAL_COUNT = minTotalCount;
this.totalCount = 0;
this.errorCount = 0;
this.isClosed = false;
}

public synchronized void countUp() {
totalCount++;
}

public synchronized void errorCountUp() {
totalCount++;
errorCount++;
}

Choose a reason for hiding this comment

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

synchronized keyword vs AtomicInteger 어떤 차이가 있고 성능적으로 차이가 날까요?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

서버 한대 상황에서 synchronized를 사용하면 간단할거 같아서 이렇게 적용했는데
찾아보니 AtomicInteger를 사용하면 락 없이도 여러 스레드에서 사용할 수 있네요!
AtomicInteger로 수정했습니다~!


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 = 0;
errorCount = 0;
isClosed = false;
}

public boolean isClosed() {
return isClosed;
}

public void close() {
isClosed = true;
}

public boolean isErrorRateOverThan(double errorRate) {
if (totalCount < MIN_TOTAL_COUNT) {
return false;
}
double currentErrorRate = errorCount / totalCount;
return currentErrorRate >= errorRate;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

요 부분에서 사용하는 totalCount 를 다른 메서드에서 조작하기 때문에 syncronized 하게 실행해도 괜찮을거같네요.

예를 들어, if 문을 통과하고 return false; 를 하기 전에 다른 스레드에서 countUp 을 호출하는 경우??

Copy link
Contributor Author

Choose a reason for hiding this comment

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

해당 케이스를 생각 못했네유..
synchronized 키워드 대신 AtomicInteger 이점이 많은 거 같아서 요걸로 수정했습니다.


public double getTotalCount() {
return totalCount;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.parkingcomestrue.external.api;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.function.Supplier;

public class AsyncApiExecutor {

private static final ExecutorService executorService = Executors.newFixedThreadPool(100, (Runnable r) -> {
Thread thread = new Thread(r);
thread.setDaemon(true);
return thread;
}
);

public static <U> CompletableFuture<U> executeAsync(Supplier<U> supplier) {
return CompletableFuture.supplyAsync(supplier::get, executorService);
}
}

Choose a reason for hiding this comment

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

해당 부분 configuration으로 정의하고 bean으로 써도 되지 않을까요?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

좋습니다~!

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.isClosed()) {
Copy link
Contributor

Choose a reason for hiding this comment

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

제가 알기론 close 가 정상상태로 알고 있습니다. 그래서 isOpen() 이 일반적인 서킷브레이커 패턴에서 사용하는 네이밍일거같슴다

Copy link
Contributor Author

Choose a reason for hiding this comment

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

오홍 그렇네요! 수정했습니다~

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.close();
scheduler.schedule(apiCounter::reset, annotation.resetTime(), annotation.timeUnit());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.parkingcomestrue.external.api;

import com.parkingcomestrue.external.api.parkingapi.HealthCheckResponse;

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.parkingapi.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,15 @@
package com.parkingcomestrue.external.api.parkingapi;

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,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,4 @@
package com.parkingcomestrue.external.parkingapi.korea;
package com.parkingcomestrue.external.api.parkingapi.korea;

import com.parkingcomestrue.common.domain.parking.BaseInformation;
import com.parkingcomestrue.common.domain.parking.Fee;
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.parkingapi.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
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.parkingcomestrue.external.parkingapi.pusan;
package com.parkingcomestrue.external.api.parkingapi.pusan;

import com.parkingcomestrue.common.domain.parking.BaseInformation;
import com.parkingcomestrue.common.domain.parking.Fee;
Expand Down
Loading
Loading