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 1 commit
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
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,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,91 @@
package com.parkingcomestrue.external.api;

import static org.assertj.core.api.Assertions.assertThat;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.junit.jupiter.api.Test;

class ApiCounterTest {

@Test
void 전체_요청이_최소_횟수를_넘고_예외가_특정_지수를_넘어가면_true를_반환한다() {
//given
ApiCounter apiCounter = new ApiCounter();

for (int i = 0; i < 8; i++) {
apiCounter.countUp();
}
for (int i = 0; i < 2; i++) {
apiCounter.errorCountUp();
}

//when
boolean actual = apiCounter.isErrorRateOverThan(0.2);

//then
assertThat(actual).isTrue();
}

@Test
void 여러_스레드에서도_전체_요청이_최소_횟수를_넘고_예외가_특정_지수를_넘어가면_true를_반환한다() throws InterruptedException {
//given
ExecutorService executorService = Executors.newFixedThreadPool(30);
ApiCounter apiCounter = new ApiCounter();
int threadCount = 1000;
CountDownLatch latch = new CountDownLatch(threadCount);

//when
for (int i = 0; i < threadCount; i++) {
if (i % 10 == 0 || i % 10 == 1) {
executorService.submit(() -> {
try {
apiCounter.errorCountUp();
} finally {
latch.countDown();
}
});
continue;
}
executorService.submit(() -> {
try {
apiCounter.countUp();
} finally {
latch.countDown();
}
});
}

latch.await();
boolean actual = apiCounter.isErrorRateOverThan(0.2);

//then
assertThat(actual).isTrue();
}

@Test
void 여러_스레드에서_카운트를_증가시킬수있다() throws InterruptedException {
//given
ExecutorService executorService = Executors.newFixedThreadPool(30);
ApiCounter apiCounter = new ApiCounter();
int threadCount = 1000;
CountDownLatch latch = new CountDownLatch(threadCount);

//when
for (int i = 0; i < threadCount; i++) {
executorService.submit(() -> {
try {
apiCounter.countUp();
} finally {
latch.countDown();
}
});
}

latch.await();

//then
assertThat(apiCounter.getTotalCount()).isEqualTo(threadCount);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.parkingcomestrue.external.api;

import com.parkingcomestrue.fake.CircuitBreakerTestService;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class CircuitBreakerAspectTest {

/**
* 요청 중 20%의 예외가 발생하면 api 요청 잠김
* 잠긴 후, 2초 후에 다시 요청보내지도록 reset
*/
@Autowired
private CircuitBreakerTestService service;

private boolean[] isExecuted = {false, false};
private final long SECOND = 1000;

@Test
void 서비스에_에러가_특정_지수를_넘으면_요청이_잠긴다() {
//given
for (int i = 0; i < 8; i++) {
service.call(() -> {});
}
for (int i = 0; i < 2; i++) {
service.call(() -> {throw new RuntimeException();});
}

//when
service.call(() -> {isExecuted[0] = true;});

//then
Assertions.assertThat(isExecuted[0]).isFalse();
}

@Test
void 서비스가_잠긴후_특정시간이_지나면_다시_요청을_보낼수있다() throws InterruptedException {
//given
for (int i = 0; i < 8; i++) {
service.call(() -> {});
}
for (int i = 0; i < 2; i++) {
service.call(() -> {throw new RuntimeException();});
}
Thread.sleep(2 * SECOND);

//when
service.call(() -> {isExecuted[1] = true;});

//then
Assertions.assertThat(isExecuted[1]).isTrue();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.parkingcomestrue.fake;

import com.parkingcomestrue.external.api.CircuitBreaker;
import java.util.concurrent.TimeUnit;
import org.springframework.stereotype.Service;

@Service
public class CircuitBreakerTestService {

@CircuitBreaker(resetTime = 2, timeUnit = TimeUnit.SECONDS)
public void call(Runnable runnable) {
runnable.run();
}
}