development/spring

Trouble Shooting - Spring Quartz Clustering

bokshiri 2024. 9. 12. 02:01

스프링 환경에서 배치 프로젝트를 구성하며 겪은 트러블슈팅 경험에 대해 정리하고자 한다. 스프링 쿼츠를 자주 사용해본 사람이라면 별 거 아닌 내용이겠지만, 나를 포함하여 경험이 없는 사람은 원인을 파악하기 쉽지 않은 내용이란 생각이 든다. (원인을 찾기가 너무너무 힘들었다...)

개발 환경

  • 스프링배치
  • 스프링쿼츠 (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

 

[JAVA] 스프링(Spring) 에서 자동실행 스케쥴러 설정하기 (Spring Quartz + job scheduler + Cron Expression)

어떤 경우에 사용하나? 주기적으로(ex: 매 1시간 마다) 또는 정해진 시각(ex: 매일 오전 7시) 에 특정 프로세스를 수행하고 싶은 경우에 사용합니다. 저는 매일 오전에 특정 API를 호출해서 데이터를

junspapa-itdev.tistory.com

 

 

하지만 위 방식은 난생 처음 적용해보는 설정이다보니 정상적으로 테스트를 마쳤음에도 걱정이 앞섰다. 그러다 위 문제를 마주하게 된 것이다. 아무래도 환경 설정에 문제가 있을 것이란 생각이 들어 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

 

커서(Cursor)란 ?

커서란 특정 SQL 문장을 처리한 결과를 담고 있는 영역을 가리키는 일종의 포인터입니다. 커서를 사용하면 처리된 SQL 문장의 결과 집합에 접근할 수 있습니다.명시적(Explicit) 커서: 사용자가 선언

velog.io

 

 

4. Mybatis Cache가 적용되는 것인가?

처음에 배치 설정을 잡을 때 mybatis-config.xml에 쿼리 캐싱을 적용하였던 기억이 났다. 배치가 실패하는 경우 이전 버전의 쿼리를 바라보는 증상이 있다보니, mybatis에서 이전 버전의 쿼리에 캐싱을 적용하여 오류가 발생하는 것이 아닌지 의심이 들었다.

 

하지만 이 또한 원인이라 보기에는 어려웠다. 관련 내용을 찾아본 결과 mybatis cache에는 JPA처럼 1차 캐시와 2차 캐시가 있는데, 프로젝트에서 Redis 등의 Global Cache를 사용하도록 작업한 내역이 없었기 때문에 In-Memory 기반으로 동작하였을 것이다. 소스가 수정되어 재 배포가 이루어지면 Was는 반드시 재기동이 되기 때문에 Cache는 초기화가 되는 것이 정상이다.

https://codingdreamtree.tistory.com/92

 

MyBatis 의도치 않은 캐싱

1. 발견 실무에서 복잡한 비즈니스를 다루는 로직에서 도저히 이해가 안가는 상황이 나오게 되었다. 상황은 이렇다. 첫 번째로 조회한 객체에는 객체 내부에 컬렉션을 가지고 있는데, 다음 메서

codingdreamtree.tistory.com

 

 

5. Spring Batch 혹은 Quartz에 Caching 기능이 존재하는가?

이 부분은 배치와 쿼츠의 메타 테이블 정보부터 시작해서 구글 및 스프링 라이브러리를 샅샅이 뒤져보았으나 어디에서도 관련 내용을 찾을 수가 없었다. 그리고 만일 캐싱이 적용되었다면 이전 버전의 배치가 수행되어 모든 경우에 실패가 발생해야 정상이다.

 

여러가지 가설을 세워봤지만 원인이라 볼 수 있는 것은 없었고, 문제는 오리무중에 빠지게 되었다. 그러던 중 인프라를 담당하시는 분께 충격적인 이야기를 전해들었다. 테스트를 진행했던 서버와 동일한 DB를 바라보는 별도의 서버가 하나 더 존재한다는 것이다. 해당 서버에는 HotFix 브랜치를 배포하며, Main 브랜치와 머지하지 않고 수정건만 바로 배포하고 있었다...

이제야 모든 것이 설명이 가능해졌다. 문제의 원인은 바로 데이터 베이스를 기반으로 동작하는 Quartz의 Clustering 기능이었다. 로드밸런서를 사용하는 분산환경에서는 애플리케이션이 다수 존재할 수 있다. 쿼츠의 스케줄은 여러 서버에서 동시에 수행되는 것을 막고 한 번만 수행되는 것이 보장되어야 하기 때문에, JDBC를 기반으로 상태를 관리하며 랜덤으로 하나의 서버에서 스케줄을 동작시킨다.

https://advenoh.tistory.com/56

 

Multi WAS 환경을 위한 Cluster 환경의 Quartz Job Scheduler 구현

Gatsby로 블로그 마이그레이션을 하여 이 링크를 클릭하면 해당 포스팅으로 갑니다. 감사합니다. http://blog.advenoh.pe.kr 1. 들어가며 Quartz에서는 메모리 기반의 스케줄러뿐만이 아니라 DB 기반의

advenoh.tistory.com

 

즉, 하나의 DB를 바라보는 두 개의 서버 (수정사항이 정상 반영된 서버, 이전 버전을 바라보는 HotFix 테스트 서버)가 랜덤으로 스케줄을 수행하다보니 발생한 문제였던 것이다.

그렇다면 로컬에서는 왜 문제가 되었을까? 로컬도 마찬가지다. 하나의 DB를 여러 개발자가 공유하여 사용하다보니, Pull을 받지 않은 사람의 소스에 의해 배치가 실패하는 경우가 발생했던 것이다.

이를 검증하기 위해 스키마 덤프를 뜬 후 로컬에 DBMS 컨테이너를 올려 테스트를 진행해보았다. 옆사람에게 부탁하여 의도적으로 Sql에 Syntax Error를 발생시키고 브레이크 포인트를 걸어보니 옆사람과 내 소스가 번갈아가며 디버깅이 잡히는 것을 확인했고, 배치 실행 이력 역시 성공과 실패를 반복하고 있었다. 

 

운영 환경에서는 이런 일이 없겠지만, 개발 단계에서는 이러한 내용을 숙지해두는 것이 반드시 필요할 것 같다.