스프링 환경에서 배치 프로젝트를 구성하며 겪은 트러블슈팅 경험에 대해 정리하고자 한다. 스프링 쿼츠를 자주 사용해본 사람이라면 별 거 아닌 내용이겠지만, 나를 포함하여 경험이 없는 사람은 원인을 파악하기 쉽지 않은 내용이란 생각이 든다. (원인을 찾기가 너무너무 힘들었다...)
개발 환경
- 스프링배치
- 스프링쿼츠 (Cron Trigger 기반의 스케줄링)
- Spring Boot 3.2.x
문제 상황은 다음과 같았다.
1. 최초에 배치 Job을 개발한 후 서버에 배포하여 스케줄을 등록해놓았는데, Step Reader에서 Sql Syntax Error가 발생하였다. 테스트를 위해 Where조건에 주석을 --로 처리하였는데, xml에서 파싱을 하지 못해 오류가 난 것이다.
2. 로컬에서 수정 및 테스트를 완료한 후 재배포를 하였다. 이후 배치의 메타 테이블에 실행 이력 및 로그 수집 도구의 로그를 모니터링하며 정상수행 여부를 체크하였다.
3. 이상한 점이 발견되었다. 배치가 주기적으로 성공과 실패를 반복하고 있었고, 실패 건의 로그를 살펴보니 처음에 발생하였던 Sql Syntax Error가 발생하고 있었다.
이상했다. 분명 소스를 수정해서 테스트까지 완료하였고, 서버에 배포도 정상적으로 된 상태였다. 더욱 이상한 점은, 스케줄을 통해 처리하는 경우에만 실패가 발생하고 Api를 호출하여 수동으로 실행하는 경우에는 모두 성공하는 것이었다. (Batch Job Name을 파라미터로 받아 수동으로 배치를 실행해주는 Api를 개발하여 사용하였다)
서버의 증상을 확인했으니 로컬에서도 배치를 돌려보기로 하였다. 그런데... 로컬에서도 서버 환경과 동일한 증상이 나타나고 있었다. 거기에 더해, 쿼츠의 스케줄이 동작할 때 반드시 호출되어야 할 스케줄 실행 메서드에 디버깅이 간헐적으로 걸리지 않는 현상이 발견되었다. 성공을 하는 경우에는 디버깅이 걸렸고, 실패하는 경우에는 디버깅이 걸리지 않은 상태로 메타테이블의 배치 로그가 실패로 남고 있었던 것이다.
다양한 가능성을 열어두고 원인을 찾아보기로 하였다.
1. 커밋, 배포가 정말 정상적으로 된 것인가?
git 배포전략에 따라 Cloud 서버에 배포하는 브랜치에 수정한 소스가 정상적으로 반영이 되지 않은 것인지 확인이 필요했다. 하지만 브랜치의 커밋 내역을 살펴보니 수정 커밋 건은 정상적으로 반영이 되어 있었고, 무엇보다 배포가 정상적으로 되지 않았다면 성공하는 케이스는 존재하지 않아야 한다. 따라서 배포에 문제는 없었다는 결론을 내렸다.
2. 스프링 쿼츠 Configuration과 트리거 설정에 문제가 있는가?
Batch와 Quartz를 연동할 때 Batch의 Job과 Quartz의 Job 인터페이스 타입이 다르다 보니, Quartz 스케줄을 등록하는 방법에 대해 많은 고민을 했었다. Quartz에 JobDetail을 등록하기 위해서는 Quartz Job Interface Type이 필요했기 때문이다. 고심 끝에 QuartzJobBean을 활용하여 Super Class를 만들고 BatchConfiguration이 이를 상속받은 후 SuperClass에서 메서드를 구현하여 해당 Job을 실행할 수 있도록 처리하였다. 이렇게 되면 Quartz에 JobDetail을 등록할 때에도, BatchJobConfiguration이 Quartz Job Interface를 상속받고 있기 때문에 문제될 것이 없었다.
https://junspapa-itdev.tistory.com/18
하지만 위 방식은 난생 처음 적용해보는 설정이다보니 정상적으로 테스트를 마쳤음에도 걱정이 앞섰다. 그러다 위 문제를 마주하게 된 것이다. 아무래도 환경 설정에 문제가 있을 것이란 생각이 들어 Quartz 공식 문서 및 구글 여기저기를 뒤져보며 정보를 수집하였으나, 마땅히 원인이 될 만한 것을 발견하지는 못하였다. 무엇보다 최초에 환경을 잡은 후 테스트를 하였을 때에는 정상적으로 스케줄이 수행되었기 때문에 더더욱 이해가 가질 않았다.
3. Mybatis Cursor Reader가 원인인가?
문제가 생겼던 배치는 Reader에서 조회 후 Writer에서 동일한 테이블의 상태를 변경하는 배치였다. Writer에서 상태가 변경되면 테이블의 Row Count가 달라지기 때문에 Mybatis Page Reader를 사용하면 누락되는 Row가 생기게 된다. 위 문제를 타개하고자 DBMS의 Cursor를 활용하여 쿼리의 결과를 DBMS 메모리에 미리 담아둔 후 배치에서 사용하고자 Cursor Reader를 도입하였다.
배치가 실패하는 원인이 Sql에 있다 보니, 혹시나 DBMS에 이전 쿼리에 대한 커서가 존재하여 간헐적으로 이를 호출하는 것이 아닌가 하는 의심이 들었다. 하지만 커서는 실행 결과를 메모리에 담아두는 것이지, 쿼리를 저장하는 것이 아니다. 따라서 원인으로 보기에는 어려울 것이라 판단하였다.
https://velog.io/@ragnarok_code/%EC%BB%A4%EC%84%9CCursor%EB%9E%80
4. Mybatis Cache가 적용되는 것인가?
처음에 배치 설정을 잡을 때 mybatis-config.xml에 쿼리 캐싱을 적용하였던 기억이 났다. 배치가 실패하는 경우 이전 버전의 쿼리를 바라보는 증상이 있다보니, mybatis에서 이전 버전의 쿼리에 캐싱을 적용하여 오류가 발생하는 것이 아닌지 의심이 들었다.
하지만 이 또한 원인이라 보기에는 어려웠다. 관련 내용을 찾아본 결과 mybatis cache에는 JPA처럼 1차 캐시와 2차 캐시가 있는데, 프로젝트에서 Redis 등의 Global Cache를 사용하도록 작업한 내역이 없었기 때문에 In-Memory 기반으로 동작하였을 것이다. 소스가 수정되어 재 배포가 이루어지면 Was는 반드시 재기동이 되기 때문에 Cache는 초기화가 되는 것이 정상이다.
https://codingdreamtree.tistory.com/92
5. Spring Batch 혹은 Quartz에 Caching 기능이 존재하는가?
이 부분은 배치와 쿼츠의 메타 테이블 정보부터 시작해서 구글 및 스프링 라이브러리를 샅샅이 뒤져보았으나 어디에서도 관련 내용을 찾을 수가 없었다. 그리고 만일 캐싱이 적용되었다면 이전 버전의 배치가 수행되어 모든 경우에 실패가 발생해야 정상이다.
여러가지 가설을 세워봤지만 원인이라 볼 수 있는 것은 없었고, 문제는 오리무중에 빠지게 되었다. 그러던 중 인프라를 담당하시는 분께 충격적인 이야기를 전해들었다. 테스트를 진행했던 서버와 동일한 DB를 바라보는 별도의 서버가 하나 더 존재한다는 것이다. 해당 서버에는 HotFix 브랜치를 배포하며, Main 브랜치와 머지하지 않고 수정건만 바로 배포하고 있었다...
이제야 모든 것이 설명이 가능해졌다. 문제의 원인은 바로 데이터 베이스를 기반으로 동작하는 Quartz의 Clustering 기능이었다. 로드밸런서를 사용하는 분산환경에서는 애플리케이션이 다수 존재할 수 있다. 쿼츠의 스케줄은 여러 서버에서 동시에 수행되는 것을 막고 한 번만 수행되는 것이 보장되어야 하기 때문에, JDBC를 기반으로 상태를 관리하며 랜덤으로 하나의 서버에서 스케줄을 동작시킨다.
https://advenoh.tistory.com/56
즉, 하나의 DB를 바라보는 두 개의 서버 (수정사항이 정상 반영된 서버, 이전 버전을 바라보는 HotFix 테스트 서버)가 랜덤으로 스케줄을 수행하다보니 발생한 문제였던 것이다.
그렇다면 로컬에서는 왜 문제가 되었을까? 로컬도 마찬가지다. 하나의 DB를 여러 개발자가 공유하여 사용하다보니, Pull을 받지 않은 사람의 소스에 의해 배치가 실패하는 경우가 발생했던 것이다.
이를 검증하기 위해 스키마 덤프를 뜬 후 로컬에 DBMS 컨테이너를 올려 테스트를 진행해보았다. 옆사람에게 부탁하여 의도적으로 Sql에 Syntax Error를 발생시키고 브레이크 포인트를 걸어보니 옆사람과 내 소스가 번갈아가며 디버깅이 잡히는 것을 확인했고, 배치 실행 이력 역시 성공과 실패를 반복하고 있었다.
운영 환경에서는 이런 일이 없겠지만, 개발 단계에서는 이러한 내용을 숙지해두는 것이 반드시 필요할 것 같다.
'development > spring' 카테고리의 다른 글
[Spring] Reflect + Aop 를 활용한 로그 처리 (0) | 2024.09.23 |
---|---|
[Log] Logback 기반의 Slf4j 로그 추상화 설정 (1) | 2024.09.16 |
@Transactional 전파속성 주의사항 (0) | 2024.09.10 |
@RequestBody와 Spring의 MessageConverter (Feat. Xss Prevent Filter) (2) | 2024.09.09 |
JPA 참고사항 정리 (0) | 2023.09.15 |