현재 진행중인 프로젝트에서는 병원 데이터와 관련된 공공 API를 사용합니다.
공공 API의 내부 정보들이 언제 어떤식으로 바뀌는지 알 수 없어,
저희는 특정 시간마다 배치 프로그램
을 통해서 공공 API 의 데이터를 불러와 DB
를 업데이트 합니다. 기존에 작성 된 코드를 프로젝트의 아키텍처를 고려하여 다음과 같은 프로세스의 개선이 필요해졌습니다.
1) 재시도 프로세스
2) 동시성 처리
private void 병원_업데이트_메서드(String SIDO_CD) throws IOException {
공공API_데이터_요청_메서드(SIDO_CD);
공공API_등록_메서드(SIDO_CD);
공공API_삭제_메서드(SIDO_CD);
공공API_수정_메서드(SIDO_CD);
}
@Scheduled(....)
public void 병원_기본정보_업데이트() throws InterruptedException {
// Code 정보에서 스케줄 사용중여부를 확인해서 사용중이 아니면 스케줄 실행
Code statusCode = getCode(204L);
if (statusCode == null) return;
//기준이 되는 전체 병원 정보를 업데이트 한다.
try {
// 업데이트 메서드 실행
병원_업데이트_메서드("서울");
병원_업데이트_메서드("경기도");
병원_업데이트_메서드("부산");
....
} catch (IOException e) {
JLog.loge(e);
throw new RuntimeException(e);
}finally {
statusCode.setUseYn("N");
codeRepository.save(statusCode);
}
}
public Code getCode(Long codeId) throws InterruptedException {
Thread.sleep(getRandomNumber()); //랜덤 초 대기
Code statusCode = codeRepository.findCodeById(codeId);
if(statusCode==null||statusCode.getUseYn().equals("Y")) {
return null;
}
// 스케줄 사용여부를 Y로 변경
statusCode.setUseYn("Y");
codeRepository.save(statusCode);
return statusCode;
}
병원_기본정보_업데이트()
메서드에서 Code
엔티티를 조회하고,
해당 엔티티의 값을 통해서 로직의 실행여부를 판단하고 있습니다.
그리고, finally
를 통해서 최종적으로 로직이 실패하여도 Code
엔티티의 값을 원래대로 돌림으로써 이후에 해당 메서드가 동작할 수 있도록 작성해둔 프로세스입니다.
현재 서버는 컨테이너환경으로 이루어져 있는 부분도 있고, 기능의 구현이 우선이였다보니 이러한 코드가 작성이 되었는데, 언제부터인지 finally
로직이 동작하지 않게 되었고, 더 이상 병원_기본정보_업데이트()
메서드가 동작하지 않게 되었습니다.
try-catch
를 통해 예외처리를 만들어뒀지만, 간헐적으로 공공API와 연결하는 작업을 처리하는 도중에 Connection fail
로 인해 로직이 수행되지 않는 상황도 발생했습니다.
언제 공공 API의 데이터가 변하는지 모르는 상황에서 외부적인 요인에 의해 로직이 실패하는 경우에 대한 대처가 필요했는데, 이 때 생각해낸 것이 재시도 프로세스
였습니다.
기존에 재시도를 위한 로직이 있었습니다.
public void method() {
// 동시성을 위한 코드 DB데이터를 이용한다.
Code statusCode = getCode(204L);
if (statusCode == null) return;
try {
....
} catch (IOException e) {
String msg = e.getMessage();
if(msg!=null&& OpenApiUtil.isAcceptableError(msg)){ //재시도 가능한 에러인지 체크하여 다시 시도
// 실행한 현재 메서드 이름
method();
}
throw new RuntimeException(e);
} finally {
statusCode.setUseYn("N");
codeRepository.save(statusCode);
}
}
catch
문에 작성된 if문
이 재시도를 위한 로직이였으나, 실제로 테스트를 하면 다음과 같은 순서로 프로세스가 진행됩니다.
1) method()
2) catch
3) method()
4) finally (2번째 호출된 method())
5) finally (1번째 호출된 method())
결과적으로, finally
는 마지막에 동작하므로 재시도가 실행되지 않습니다.
기존에 있던 재시도 로직을 걷어내려면, Code
와 관련된 로직이 제거되어야 합니다. 사실 Code
를 사용하는 목적에는 동시성 처리를 위한 목적도 가지고 있었는데요,
우선 해당 장에서는 재시도 로직에 대해서 개선했던 점을 다루겠습니다.
재시도 로직을 직접 구현하려면 정말 어렵습니다.
따라서 저는 Spring
에서 제공하는 Spring Retry
를 사용해서 이번 문제를 개선해보려고 했습니다.
먼저 의존성을 빌드했습니다.
implementation "org.springframework.retry:spring-retry"
Spring Retry
와 관련된 자세한 내용에 대해서는 생략합니다.
먼저 테스트를 하기 위해서 RetryService
를 작성했습니다.
@Service
public class RetryService {
@Scheduled(fixedDelay = 1000*60) //1분마다 실행
@Retryable(
value = {RuntimeException.class},
backoff = @Backoff(delay = 2000)
)
public void doRetrySomething() throws InterruptedException {
System.out.println("로직 시작.");
System.out.println("쓰레드 : " +Thread.currentThread());
for (int i =0; i < 5; i++) {
int random = (int)(Math.random() * 10) + 1;
System.out.println("random = " + random);
if(random % 4 == 0) {
throw new RuntimeException();
}
}
Thread.sleep(3000);
}
/*
예외 발생시 maxAttempt만큼 재시도 후 그래도 복구가 안되었을 경우엔 recover() 메서드가 최종 호출됩니다.
*/
@Recover
public void recover() {
System.out.println("예외발생으로 재시도 종료");
}
}
실행될 로직은 5번의 반복문 동안 10이하의 랜덤값을 4로 나눈 나머지 값이 0일경우 예외를 발생하도록 했습니다.
@Retryable
을 선언하여 어떤 예외가 발생했을 경우에 재시도를 수행할지를 작성하고, @Recover
를 적용한 메서드를 통해서 설정한 횟수만큼의 재시도 이후에도 예외가 발생하면 그에 대한 맞는 처리를 합니다.
기본적으로 3번의 재시도를 실행합니다.
@Autowired
private RetryService retryService;
@Test
void test() throws IOException {
retryService.doRetrySomething();
}
-- 첫번째 시도 --
로직 시작.
쓰레드 : Thread[main,5,main]
random = 4
로직 시작.
쓰레드 : Thread[main,5,main]
random = 9
random = 7
random = 2
random = 6
random = 2
-- 두번쨰 시도 ---
로직 시작.
쓰레드 : Thread[main,5,main]
random = 3
random = 4
로직 시작.
쓰레드 : Thread[main,5,main]
random = 6
random = 9
random = 9
random = 4
로직 시작.
쓰레드 : Thread[main,5,main]
random = 1
random = 9
random = 10
random = 10
random = 4
예외발생으로 재시도 종료
첫번째 시도의 경우 한번 예외발생 이후 정상동작을 하였고,
두번째 시도의 경우 여러번의 예외 발생으로인해 @Recover
가 적용된 메서드가 실행했습니다.
@Service
public class HospitalScheduleRetryService {
/**
* 병원 기본정보 업데이트 로직을 재시도 로직으로 설정한다.
*/
@Retryable(
value = {Exception.class},
backoff = @Backoff(delay = 3000)
)
@Transactional
public void 병원 기본정보 업데이트() throws IOException {
JLog.logd("병원 기본정보 업데이트 시작");
method();
JLog.logd("병원 기본정보 업데이트 종료");
}
@Recover
public void recover(Exception e) {
JLog.loge("예외 다수 발생으로 재시도 종료 : " +e.getMessage());
}
}
실제 로그를 보여드릴 순 없지만, 재시도를 하는 로그를 발견했었고,
재시도자체는 잘 적용시켰습니다.
재시도 프로세스를 적용하면서 임시방편으로 적용해둔 동시성 제어에 대한 문제가 다시 생겼는데, 여러 컨테이너(서버)가 아닌 하나의 컨테이너에서만 스케줄러
가 동작해야하기 때문에 적용시켜야 한다고 생각합니다.
이 부분은 다음 장에서 작성하도록 하겠습니다.