안녕하세요!
Betalabs 백엔드 개발팀의 찬쿤입니다.
개발을 하며 우리는 다양한 예외를 처리하는 상황에 놓이게 되는데요.
이번에는 스프링 배치를 사용하면서 놓쳤던 부분을 공유하고 스프링 배치뿐만 아니라 예외 처리는 어떻게 해야 하는지 다시 한번 학습하는 기회가 됐으면 하는 마음에 이 글을 작성하게 되었습니다.
저희는 스프링 배치 스케줄링하는 다양한 방법 중 Jenkins
를 이용하여 일정 시간마다 Spring Batch
서버에 Job
실행 api 호출하는 방법을 사용하고 있었습니다.
또한 배치 관련 실패 메시지는
1. api 요청이 200이 아닐 때 슬랙 메시지
2. 다양한 스프링 배치에 있는 예제처럼 코드를 작성하여 예외가 발생 시 슬랙 메시지가 발생하도록 처리하였습니다.
@Component
class DefaultJobRunner(
private val jobLauncher: JobLauncher,
private val snsProperties: snsProperties,
private val eventPublisher: ApplicationEventPublisher
) : JobRunner {
override fun run(job: Job, jobParameters: JobParameters) {
try {
jobLauncher.run(job, jobParameters)
} catch (ex: Exception) {
eventPublisher.publishEvent(
UnhandledExceptionEvent(ex, channel = snsProperties.slackErrorChannel)
)
throw ex
}
}
}
으흠?
1. 도메인 관련 로직도 모든 성공 실패 경우에 대해 단위 테스트 작성하였고
2. 배치잡 테스트도 작성하였으며
3. 혹시 모를 예외 상황도 이중으로 슬랙 알림이 발생하도록 했기 때문에
이 정도면 배치 서버 구성 잘한 것일지도?
넵 예상하신 대로 다행히(?) 개발 서버 테스트 중 문제를 발견하였습니다.
문제 상황이 발생했던 시나리오는 다음과 같았습니다.
프론트
는api 서버
에 사용자가 가지고 있는 토큰 개수를 조회 및 전송내용 기록api 서버
는프론트
에 전송내용에 id와 가능 개수를 전달프론트
는블록체인 네트워크
에 transfer 트랜잭션 생성transfer
요청 후 생성txHash
를api 서버
에 저장- KST 기준 오늘 생성되었고
txHash
를 확인하지 않은 목록에 대해batch 서버
에서 결과 처리- (!) 결과 처리
Job
과Step
은FAILED
상태로 전송내용도 처리되지 않고 알림도 오지 않았음 (!)
Job
과 Step
이 친절하게 문제가 발생한 위치와 에러 메시지를 알려주고 있었기 때문에 빠르게 찾을 수 있었습니다.
결과만 요약하자면
외부 시스템(이더리움 네트워크)에서 전송한 값
(amount)
과 내부적으로 기록된 값(transferAmount)
이 맞지 않아require(this.transferAmount.same(amount))
예외가 발생하고 있었고 이에 따라Step
과Job
이 실패하고 있었던 것입니다.
테스트를 위해 임의로 넣었던 txHash
데이터가 남아서 발생한 문제였던 것입니다.
JobLauncher
설명을 읽어본다면 다음과 같이 Job
관련 실행 문제에 대해서만 예외를 처리하고 있는 것을 알 수 있습니다.
Throws:
- JobExecutionAlreadyRunningException – if the JobInstance identified by the properties already has an execution running.
- IllegalArgumentException – if the job or jobInstanceProperties are null.
- JobRestartException – if the job has been run before and circumstances that preclude a re-start.
- JobInstanceAlreadyCompleteException – if the job has been run before with the same parameters and completed successfully
- JobParametersInvalidException – if the parameters are not valid for this job
우리가 원했던 예외 처리는 사실
AbstractJob
내에서 아래와 같이 BatchStatus
와 addFailureException에서 관리되고 있었고 이것을 이용한 예외 처리 코드를 작성함으로써 문제를 해결할 수 있었습니다.
(step에 custom exceptionHandler을 추가)
} catch (Throwable t) {
logger.error("Encountered fatal error executing job", t);
execution.setExitStatus(getDefaultExitStatusForFailure(t, execution));
execution.setStatus(BatchStatus.FAILED);
execution.addFailureException(t);
}
Spring Batch
는 대용량 처리에 특화 되어 있으며 step
은 특히 falutTolerant(), skip(), retry()
등과 같이 데이터가 처리되는 동안 Exception이 발생했을 경우, 해당 데이터를 처리하거나 건너뛰기를 결정할 수 있습니다.
데이터의 사소한 오류에 의해 발생하는 문제를 실패 대신 Skip함으로써, 배치수행의 빈번한 실패를 줄일 수 있게 해주고 있습니다.
제가 개발을 시작하고 처음 이슈를 할당 받았을 때 당시 멘토님께서 강조했던 것이
라고 해주시며 토비의 스프링 4장을 보면 예외 처리에 관한 내용을 읽으면 좋을 것이라고 해주셨습니다.
토비의 스프링을 보면 초난감 예외 처리 예시로
예외 블랙홀이 나옵니다.
try {
// do something
} catch(SQLException e) {
//
}
- 예외는 잡았지만 아무것도 하지 않고 있다. 예외 발생을 무시해버리고 정상적인 상황인 것처럼 다음 라인으로 넘어가겠다는 분명한 의도가 있는 게 아니라면 연습 중에도 절대 만들어서는 안 되는 코드이다.
- 예외가 발생하면 그것을 catch 블록을 써서 잡아내는 것까지는 좋은데 그리고 아무것도 하지 않고 별문제 없는 것처럼 넘어가 버리는 건 정말 위험한 일이다. 원치 않는 예외가 발생하는 것 보다도 훨씬 더 나쁜 일이다.
배치에서 감싼 예외를 처리하지 않았다는 점에서 예외 블랙홀에 빠진 것이나 다름없게 된 것이죠
최종적으로
직접 개발하지 않은 코드를 쓰게 되는 경우 각 메소드가 어떤 예외를 발생시키는지 또한 예외를 어떻게 처리하는지 정확하게 파악하고 다시 한번 확인하는 습관의 필요성을 느낄 수 있었습니다.
혹시 이 글을 읽으시면서 예전에 에러처리를 제대로 하지 못하여 당황했던 경험이나 사연이 있는지 궁금합니다.
이 글을 읽은 분들 전부
모두 고려하는 개발자
가 되시길 바랍니다.
감사합니다.
잘 봤어요