Spring Retry로 재시도 로직 구현을 통한 코드 개선

Yoonhwan Kim·2023년 2월 23일
0

실무문제해결

목록 보기
1/2

개요

현재 진행중인 프로젝트에서는 병원 데이터와 관련된 공공 API를 사용합니다.
공공 API의 내부 정보들이 언제 어떤식으로 바뀌는지 알 수 없어,

저희는 특정 시간마다 배치 프로그램 을 통해서 공공 API 의 데이터를 불러와 DB 를 업데이트 합니다. 기존에 작성 된 코드를 프로젝트의 아키텍처를 고려하여 다음과 같은 프로세스의 개선이 필요해졌습니다.

1) 재시도 프로세스
2) 동시성 처리

재시도 프로세스를 위한 개선

AS-IS

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 를 사용하는 목적에는 동시성 처리를 위한 목적도 가지고 있었는데요,

우선 해당 장에서는 재시도 로직에 대해서 개선했던 점을 다루겠습니다.

TO-BE

재시도 로직을 직접 구현하려면 정말 어렵습니다.
따라서 저는 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());
    }
}

마무리

실제 로그를 보여드릴 순 없지만, 재시도를 하는 로그를 발견했었고,

재시도자체는 잘 적용시켰습니다.

재시도 프로세스를 적용하면서 임시방편으로 적용해둔 동시성 제어에 대한 문제가 다시 생겼는데, 여러 컨테이너(서버)가 아닌 하나의 컨테이너에서만 스케줄러가 동작해야하기 때문에 적용시켜야 한다고 생각합니다.

이 부분은 다음 장에서 작성하도록 하겠습니다.

0개의 댓글