😎 이 글에서 다룰 것
스프링 배치의 반복 및 오류 제어 기능과 오류 발생에 의한 알림 모니터링을 구현했던 내용
시스템 결함
현재 배치 서버는 대량의 데이터를 스케줄러와 결합하여 자동으로 데이터를 일괄적 처리하는 시스템이다.
이는 곧 시스템과의 상호작용에 의한 것이 아닌 자체적인 로직이 정해진 시간에 맞춰 작동하는 자동화 시스템이며, 이를 사용하는 사용자에게는 시스템에 의존하는 것을 의미하게 된다.
따라서 시스템의 오류나 중단이 사용자에게는 큰 문제를 일으킬 여지가 있으며 , 안정적이고 신뢰성 높은 시스템 요소를 갖추기 위해 오류나 결함에 따른 적절한 대응책이 필요하다고 판단하였다.
Retry? Skip?
위의 프로젝트 사항처럼 배치 기능의 특성을 고려하여 스프링 배치에서는 청크 기반( 데이터를 처리하는 트랜잭션 단위 )의 프로세스에 FaultTolerant 구조를 제공한다.
그리고 해당 구조에서는 Retry와 Skip 기능을 제공하는데, 먼저 Retry는 기능 중에 특정 레코드에서 발생하는 오류에 대해 작업을 재시도 하는 것이며, 특정 예외를 개발자가 설정할 수 있고 재시도 횟수를 제한할 수 있다. 또한 재시도 간의 대기 시간( backoff )도 설정 가능하다.
한편, Skip은 오류를 발생시킨 해당 레코드를 건너뛰고 로직을 진행하면서 오류에 대응한다( 건너뛰는 횟수를 제한 가능하다 ).
적용 시나리오
위와 같은 기능을 활용하여 본 프로젝트에 적용한 시나리오는 아래와 같았다.
임의의 에러 및 오류로 인하여 전체 시스템의 중단이 발생하지 않는 안정적인 작동이 필요하다.
즉, 예외가 발생하여도 전체 시스템이 발생하지 않으며 일단 할당된 로직을 끝까지 처리한다.
단, 운영자 및 개발자가 해당 예외 및 오류사항을 인지할 수 있도록 로깅 및 모니터링을 구현하자.
고려사항
그리고 위와 같은 사항을 적용하기 위해서 구체적으로 다음과 같은 고려사항을 적용하였다.
🤚 예외 처리 로직
모든 예외에 대하여 Retry 및 Skip 기능을 사용한다.
단, 일시적 예외일 수 있으니 예외 발생상황에 대해서 Retry는 일정 횟수까지만 동작하도록 만든다.
그리고 특정 레코드의 예외가 발생하여도 시스템은 정상 동작하기 위해 Skip의 동작에 있어 횟수는 제한없이 가능하도록 만든다.
🤚 모니터링
스프링 배치에는 메타 테이블 제공하기에 예외가 발생하면 해당 테이블에 접근하여 살펴보면 되지만..
이는 즉각적인 인지와 대응이 어렵고, 데이터를 일일이 살펴보는 것은 비효율적이다.
따라서 리스너를 통해 예외 처리 로직의 동작에 따른 warn 이상의 로그를 발생시키고 logback과 webhook을 사용하여
warn이상의 로그에 대해 현재 프로젝트에 사용 중인 슬랙에 알림가도록 설정한다.
구현사항
아래의 코드는 프로젝트에 구현한 일부 Skip 및 Retry를 설명하기 위한 코드이다.
@Bean
public Step jdbcGenerateInvoiceStep(JobRepository jobRepository, PlatformTransactionManager transactionManager) {
CustomBackOffPolicy customBackOffPolicy = new CustomBackOffPolicy(1000L, 2.0, 4000L);
TaskletStep jdbcGenerateInvoiceStep = new StepBuilder("jdbcGenerateInvoiceStep", jobRepository)
.<Contract, Contract>chunk(CHUNK_SIZE, transactionManager)
.reader(jdbcContractItemReader())
.writer(jdbcContractItemWriter())
.faultTolerant()
.retry(Exception.class)
.retryLimit(2)
.backOffPolicy(customBackOffPolicy)
.listener(retryListener)
.skip(Exception.class)
.skipPolicy(customSkipPolicy)
.listener(customSkipListener)
.listener(stepCompletionCheckListener)
.build();
return jdbcGenerateInvoiceStep;
}
먼저 .faultTolerant() 이후 .retry(Exception.class) 의 코드부터 .listner(retryListener) 부분까지 retry( 재시도 )와 관련된 설정 로직이다.
faultTolerant 구조를 사용하면서 Exception.class는 모든 예외를 처리하며 재시도 로직이 동작할 시 .retryLimit(2)를 통해 횟수를 2회로 제한하며 .retryListener의 구현에 따라 warn 이상의 로그를 발생시킨다.
(추가로 backoff 정책은 재시도 간 시간 간격을 주는 방법인데 필요시에 따라 구현하면 된다. 자세한 내용은 구글링을 해보자.)
그리고 아래는 리스너의 로직이다.
@Component
@Slf4j
@RequiredArgsConstructor
public class CustomRetryListener implements RetryListener {
@Override
public <T, E extends Throwable> boolean open(RetryContext context, RetryCallback<T, E> callback) {
return true;
}
@Override
public <T, E extends Throwable> void onError(RetryContext context, RetryCallback<T, E> callback, Throwable throwable) {
log.warn("CustomRetryListener onError 에러 발생: {}", throwable.getMessage());
}
@Override
public <T, E extends Throwable> void close(RetryContext context, RetryCallback<T, E> callback, Throwable throwable) {
if (throwable != null) {
log.error("CustomRetryListener close 처리 실패. 최종 오류: {}", throwable.getMessage());
}
}
}
리스너를 통해 Retry로직이 동작하면 해당 로그를 발생시킨다.( 전부 WARN 이상의 로그이다. )
이어서 .Skip( 건너뜀 ) 설정에 대한 로직은 .Skip(Exception.class)부터 .listener(customSkipListener)까지 해당한다.
모든 예외를 처리하기 위해 마찬가지로 Exception.class를 사용하였고, .skipPolicy(customSkipPolicy)에서 설정한 customSkipPolicy에 따라 Skip의 기능이 적용된다.
( 앞서 Retry의 경우도 좀 더 정교하게 재시도 로직과 관련하여 구현할 부분이 있다면 RetryPolicy를 통해 적용 가능하다. )
현재 프로젝트에서는 정해진 retry 횟수만큼 재시도 이후에 모든 예외에 대하여 제한없이 예외를 발생시키는 레코드를 skip하도록 하였고 해당 코드는 아래와 같다.
@Component
public class CustomSkipPolicy implements SkipPolicy {
@Override
public boolean shouldSkip(Throwable t, long skipCount) throws SkipLimitExceededException {
return true;
}
}
생각보다 매우 간단하다. shouldSkip의 메소드를 오버라이딩하여 예외 정보를 담고 있는 THrowable 객체와 skip 횟수를 로직으로 사용할 수 있도록 skipCount를 파라미터로 사용하고 boolean 값을 리턴한다.
그리고 리턴값이 ture인 경우 skip 기능이 작동하는데, 위의 설명에서 retry 이후 예외를 발생시키는 모든 레코드에 대하여 전부 skip하기로 하였으니, 그냥 true값을 리턴하면 되는 것이다.
대신 skip에 대한 로깅과 알림을 위해서 역시 .listener(customSkipListener)으로 해당 로직을 구현하였다.
아래는 해당 코드이다.
@Component
@Slf4j
@RequiredArgsConstructor
public class CustomSkipListener implements SkipListener<Contract, Contract> {
@Override
public void onSkipInRead(Throwable t) {
log.error("CustomSkipListener onSkipInRead 데이터 read에서 error발생: {}", t.getMessage());
}
@Override
public void onSkipInWrite(Contract item, Throwable t) {
log.error("CustomSkipListener onSkipInWrite Skip 된 Writing item 데이터 : ID={}, Error: {}", item.getId(), t.getMessage());
}
@Override
public void onSkipInProcess(Contract item, Throwable t) {
log.error("CustomSkipListener onSkipInProcess Skip 된 Processing item 데이터 : ID={}, Error: {}", item.getId(), t.getMessage());
}
}
각 spring batch의 데이터를 처리하는 순서에서 (read -> process -> write) 예외가 발생할 경우, 예외에 대한 메세지 로그를 발생시킨다. ( 모두 warn 이상의 로그이다. )
이를 통해, spring batch를 통한 예외 발생의 경우 재시도 및 스킵 기능을 설정하였고, 리스너를 통해 예외에 대한 로그가 발생하도록 구현하였다.
LogBack 및 Webhook을 통한 알림 설정
앞에서 예외 처리와 리스너를 통한 로그 발생 로직을 알아봤다.
그리고 특히 로그는 운영자 및 개발자가 배치 처리 작동 중 발생하는 예외 사항에 대해 빠르고 쉬운 알람 기능을 위한 것이다.
따라서, logback을 통한 로깅과 webhook으로 배치 동작 과정 중 발생하는 예외에 대하여 로그를 남기고 slack으로 알람이 가도록 구현하였다.
먼저 LogBack 부분에 해당하는 일부 코드이다.
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- Console appender to output JSON logs to the console -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<jsonGenerator>
<prettyPrint>true</prettyPrint>
</jsonGenerator>
</encoder>
</appender>
<!-- File appender to save JSON logs to a file -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/batch-logs.json</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logs/application-%d{yyyy-MM-dd}.%i.log</fileNamePattern> <!-- 일별 로그 파일 패턴 -->
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>10MB</maxFileSize> <!-- 로그 파일 최대 크기 -->
</timeBasedFileNamingAndTriggeringPolicy>
<maxHistory>30</maxHistory> <!-- 최대 보관 기간 (일) -->
</rollingPolicy>
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<jsonGenerator>
<prettyPrint>true</prettyPrint>
</jsonGenerator>
</encoder>
</appender>
먼저 Console Appender는 JSON 형식의 로그를 콘솔에 출력하기 위한 설정이다.
그리고 File Appender는 JSON 형식의 로그를 파일로 저장하기 위한 설정인데 용량을 10MB 제한을 두고 최대 30일간의 파일을 보관하도록 설정하였다.
이로써 배치가 작동한 상태를 확인하기 위해 메타 테이블을 확인하지 않고 로깅 데이터를 통해서도 확인할 수 있게 되었다.
이어서 WebHook을 통해 알람이 가기위한 설정을 확인해보자.
<appender name="SLACK" class="com.github.maricn.logback.SlackAppender">
<webhookUri>https://hooks.slack.com/services/T077W.... 생략</webhookUri>
<username>billingwise-batch-server</username>
<channel>#5팀</channel>
<layout class="ch.qos.logback.classic.PatternLayout">
<pattern>%d{yyyy-MM-dd HH:mm:ss} %-5level %logger{36} - %msg%n</pattern>
</layout>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>WARN</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<webhookUri>의 slack 주소값에 <layout class="ch.qos.logback.classic.PatternLayout">의 패턴으로 메세지를 보내며
<filter class="ch.qos.logback.classic.filter.LevelFilter"> 에서는 warn 이상의 로그에 대해서만 알람이 가도록하는 설정이다.
이를 통해서 예외를 발생시킨 경우, 다음과 같이 슬랙으로 알림이 오는 것을 확인하였다.

또한 추가적으로 그라파나로 시각화하여 모니터링 하도록 구현하여 아래와 같은 동작 상태를 볼 수 있도록 하였다.
( 해당 부분은 같은 팀원이 인프라 및 시스템을 구축하였고, 필자는 HashMap으로 모니터링에 필요한 데이터를 담고 objectMapper을 통해 JSON 문자열로 반환하기만 하였다. )

이와 같은 구현 및 설정으로 스프링 배치을 통한 자동화 시스템에 대해 보다 빠르고 안정적인 작동을 고려하였고, 배치 서버 개발과 관련한 주요 고려사항을 모두 다룬 듯하여 본 프로젝트에 대한 내용을 마무리 지으려한다.
회고
SW 개발자 교육을 받으며 진행했던 프로젝트로 아예 처음 사용해보는 기술을 짧은 기간에 처음부터 공부해서 구현하다보니 과정이 결코 쉽진 않았다.
거기다 4명 구성인데 갑자기 프로젝트 중간에 팀원 이탈로 3명이서 구현하는 악재까지 겹치니 멘탈이나 체력적으로 좀 힘들었던 프로젝트가 아니였나 싶다... ㅎㅎ
그럼에도 불구하고 일당백 팀원들 덕분에 프로젝트를 성공적으로 마무리할 수 있었고 꽤나 만족스러운 프로젝트 경험으로 남을 것 같다.
이번 프로젝트를 통해 개발과 관련하여 얻을 수 있었던 점들은 당장 아래와 같은 것들이 생각난다.(프로젝트를 마무리하고 3개월 정도? 지나니 잘 생각이 안난다.. ㅎㅎ 바로바로 적는 습관을 들이자 )
" 해당 프로젝트를 통해서 '청구/수납'이라는 낯선 도메인에 대한 이해와 더불어 구현 시 고려할 점에 대한 지식을 얻을 수 있었다. "
" 그리고 DB 스펙과 관련하여 접근 기술마다 지닌 작동원리를 파악하고 장단점에 대해 많은 학습과 적용하는 경험으로 그나마 데이터 베이스라는 존재(?)와 가까워진 느낌이었다.(인덱스, paging vs corsor, full scan, JPA.. 등등 DB와 관련해서 배워야할 것들이 정말 많은 것 같다... ) "
" 마지막으로, Sping Batch을 파악하고 기능 구현에 있어 고려할 점들에 대한 지식과 경험을 얻었다는 점이 가장 큰 수확인 거 같다. "
진짜 끝으로..
앞으로도 KEEEP GOING 하자
'Dev Log > Batch개발(청구-수납)' 카테고리의 다른 글
| Spring Batch 서버 개발(2) - ItemReader 와 Bulk Insert처리 (1) | 2024.11.02 |
|---|---|
| Spring Batch 서버 개발(1) (0) | 2024.11.01 |
댓글