스케쥴러 기능을 통한 주문 상태변화 기능 구현

    왜 스케쥴러를 사용하게 되었는가?

    현재 진행중인 프로젝트는 공동구매 기능이 존재하기 때문에 이전에 작성하였던 결제 시스템을 구축하였었다.

    원래라면 실제 운영하게 되는 사이트의 경우라면

    • 공동구매 결제, 공동구매 성공, 공동구매 실패 -> 공동구매 조건 및 유저 결제확인상태에 따라 달라진다
    • 배송준비, 배송중, 배송완료 -> 상품을 판매하는 업체측이 직접 배송관련 관리를 하고 이에 따라 상태를 변화시킨다
    • 구매확정 -> 배송완료가 된 후, 유저가 상품을 판단하고 구매확정 버튼을 눌러 구매를 확정시킨다.

    와 같은 형태로 업체와 유저가 직접 버튼 등을 눌러서 조작하여야 하지만 우리 프로젝트는 실제로 배포를 진행하여 사용자를 받고 결제를 진행하지는 않기 때문에 결제 이후 배송등과 관련된 사항에서 시나리오를 작성하여 이에 맞춰서 주문 상태를 변경하게 하였다.

    • 유저 결제 완료
      • 공동구매가 마감되지 않았을 경우 -> "공구진행중" 상태
      • 공동구매가 마감되고 최소 수량 조건이 충족했을 경우 -> "배송준비중" 상태
      • 공동구매가 마감되었지만 최소 수량 조건이 충족하지 못했을 경우 -> "공구실패" 상태
    • 공동구매가 성공한 이후
      • 공동구매 마감 기한 2일 후 -> "배송중" 상태
      • 배송중 + 2일 후 or 교환요청 + 2일 후 -> "배송완료" 상태
      • 배송완료 + 5일 후 -> "구매확정" 상태

    와 같이 변경을 해주기로 하였다. 하지만, 매 번 00시가 되었을 때 개발자가 직접 모든 주문을 확인하고 상태를 변경하는 것은 매우 비효율적일 뿐만 아니라, 사람이 직접하게 되면 예상하지 못한 에러를 발생시킬 수도 있다고 생각하였다.

    따라서, 날짜가 지나갔을 때 이벤트를 발생시켜서 주문들을 체크하고 상태를 변경하여야 하는데 이를 해결하기 위한 방법으로 스케쥴러 기능을 사용하기로 하였다.

     

    Spring Scheduler vs Quartz

    스케쥴러 기능을 사용하기로 결정한 이후 가장 많이 했던 고민은, 존재하는 기술들 중에서 어떤 것을 사용하는 것이 프로젝트에 적합한 것인지를 확인하는 것이었다.

    Spring Scheduler

    • spring-boot-starter 라이브러리에 기본적으로 포함되어 있어 사용하기 편리하다
    • @Scheduler 어노테이션을 이용해서 간단하게 작성이 가능하다.
    • DB클러스터링이 가능하지 않다.

    Quartz

    • Cron, 일회성 작업, 반복 작업 등등 다양한 스케쥴링 옵션을 지원한다.
    • DB클러스터링이 가능해, 분산
    • 작업 상태를 DB에 저장하고, 시스템 종료 후에도 복구가 가능하다.
    • 복잡한 작업 관리가 필요할 때 유용하다.

    가장 큰 차이점으로 보이는 것은 DB 클러스터링이 가능한지 아닌지에 대한 부분으로 보인다.

    DB클러스터링이란, 하나의 데이터베이스를 여러 서버가 나누어서 구축하는 것이다. 단일 서버로 구성하였을 때, 서버가 예상치 못한 오류로 인해 장애를 겪더라도 다른 서버가 이어서 처리할 수 있으며, 많은 트래픽에서도 작업을 나누어서 처리할 수 있어 효율적이다.

    실제로 사용을 해본 적은 없지만, 예상하는 바로는 여러 서버를 통해 DB를 관리하기 때문에 동시성 문제가 발생할 가능성이 있는데 Quartz 라이브러리에서는 이러한 부분을 보완해줄 수 있는 것으로 보인다.

     

    결국 아무리 좋은 기술이 존재하더라도, 현재 프로젝트의 상황에 맞게 기술을 선택하는 것이 중요하다고 생각한다. 진행중인 프로젝트의 상황은 다음과 같다

    • 1개의 스프링 서버, 1개의 데이터베이스
    • 유명한 서비스들처럼 높은 트래픽이 몰릴 것이라고 현재는 예상하고 있지 않음
    • 복잡하지 않은 단순 조회 및 update 작업 예상

    이기 때문에 굳이 Quartz를 사용하지 않고, Spring scheduler만으로 충분하다고 판단하였다.

    만약에 이후에 프로젝트의 시나리오를 확장하며 높은 트래픽에 대한 처리를 어떻게 할 것인지? 판단을 하게 된다면, 부하를 줄이기 위해 여러 개의 서버 혹은 DB를 구성하고 로드밸런싱을 추가하며, 이러한 부분에서 Quartz를 사용하는 것이 더 적절하다고 판단하고 기술을 바꾸어서 도입하게 될 것 같다.

     

    코드 구현

    Spring scheduler 사용은 너무 쉽고 간단했다.

    먼저 메인 애플리케이션에 @EnableScheduling 어노테이션을 추가하여, 스케쥴링 기능을 사용할 것이라는 것을 알렸다.

     

    그 후에는, 스케쥴러 작업을 할 클래스를 생성하여 Component로 지정 후, 필요한 repository와 상태 등을 지정했다.

    그 후에는, 크론 표현식을 통해 언제 스케쥴링 기능을 실행할 것인지 작성하고, 시나리오에 맞게 작업 내용을 생성하였다.

     

    테스트는 총 2가지 방법으로 진행하였다.

    • 테스트 코드를 통한 테스트
    • 실제 db 데이터를 조작하여 매 분 (0 * * * * ?) 작업을 수행하여 테스트

    테스트 코드를 통해 스케쥴링작업이 정상적으로 동작하는지 확인을 하는데, 문제가 발생했었다.

    이 테스트 코드의 시나리오는 다음과 같다.

    • 10명의 유저와, 1개의 공동구매, 10개의 주문을 생성한다.
    • 공동구매의 경우 최소 수량을 모두 충족하였으며 주문 시점을 기준으로 3일 전 마감되었다.
    • 주문 데이터의 경우 모두 결제가 완료되어 "공구진행중" 상태이다.
    • 따라서, 스케쥴링 작업이 끝난 후 모든 주문은 "배송준비중" 상태로 변경되어야 한다.

    하지만, 결과는 다음과 같이 "공구진행중" 상태 그대로인 것으로 파악되었다.

    원인을 찾지 못해 인터넷을 계속 뒤지던 중, 로그를 확인해보는데 이상한 점을 발견하였다.

    눈치가 빠른 분들은 알 수 있겠지만, 내 스케쥴링 작업 코드를 확인해보면 시작과 끝 모두에 로그가 남아있다. 하지만, 결과가 나오기 이전 로그에서는 시작 이라는 로그밖에 확인되지 않았다.

    그래서 아래 쪽 로그를 더 확인해보니,

    데이터 검증이 이루어진 후, update 로직이 이루어지고 스케쥴링 종료 로그가 남아있는 것을 확인할 수 있었다.

    원인은 다음과 같다.

    • 스케쥴링 작업은, 별도의 쓰레드를 이용해 비동기적으로 작업이 이루어진다.
    • 따라서, 데이터가 업데이트 되기 이전에 판별하는 로직이 실행되어 아직 업데이트 되지 못한 주문 데이터때문에 failed로 결과가 발생했다.

    나는 단순히 스케쥴링 작업이 실행되면 서버가 스케쥴링 작업을 하는 동안 대기 상태로 들어간다고 생각하였던 것이었다.

    따라서, 테스트 코드에

    1초동안 메인 스레드를 쉬게 해주고 결과를 확인하였더니

    테스트를 통과하게 되었다.

     

    만약 이 작업을 Quartz로 작성하려면?

    사실 맨 처음에는 Quartz가 조금 더 나은 기술이라고 생각하여 Quartz를 통해 코드를 작성했었다. 하지만, 팀원 중 한 분이 굳이 "Quartz를 사용할 필요가 있을까요?" 라며 의문을 표했고, 더 자세히 알아보니 불필요하다고 판단되어 코드를 수정했다.

    라이브러리는 spring-starter-quartz를 사용했다.

    조금 시간이 지난 글들 혹은 정석적(?)으로 사용하는 글에서는 설정이 너무 복잡해서 나는 spring-start-quartz를 이용해 auto configuration을 진행했다.

    Quartz를 사용하면서 가장 고생했던 부분은, Job에 repository 사용을 해야 하는데

    단순히 @RequiredArgsConstructor 어노테이션을 이용해서 의존성 주입을 진행하면 제대로 생성이 되지 않았다. 인터넷 글에 따르면 기본 생성자가 필요하다 하여서 

    @RequiredArgsConstrucor 어노테이션과 기본 생성자를 만들어주면, Job에서는 다시 

    Error: ~~~ cause orderService is null 이라면서 에러를 뱉어서 정말 고생하였다.

    Quartz에 대해 조사를 하던 도중, Spring scheduler로 변경을 하여서 정확한 작동방식을 공부하지는 못했지만 아래에는 해결 방법을 올려놓도록 하겠다.

    AutoWiringSpringBeanJobFactory 클래스 생성

    위 쪽의 QuartzConfig 파일을 확인해보면, setJobFactory를 통해서 이 AutoWiringSpringBeanJobFactory를 지정해주는 것을 확인할 수 있다. 이 방식을 통해서 JobFactory를 설정하면, @Autowired 어노테이션을 통해서 Job에서도 스프링 의존성을 주입받아 사용할 수 있다고 한다.

    위의 setJobFactory를 진행하엿어도, @RequiredArgsConstructor을 사용하거나, 기본 생성자를 만들어주지 않는다면 에러가 발생하며 진행이 되지 않는다.(이 이유를 찾던 도중 Spring scheduler로 변경을 해서 원인은 모른다.. 나중에 Quartz를 제대로 사용하게 되면 찾아봐야겠다.)

    (초기에 쿼리 최적화를 진행하지 않고 작동이 하나만 확인해보기 위해 작성한 옳지 못한 방향의 모든 데이터를 가져와 체크하는 코드...)

    테스트 코드에서 강제로 스케쥴링 작업을 실행하기 위한 TriggerService 코드

    TriggerService를 이용해 강제로 스케쥴링 작업을 실행한 테스트코드

    Thread.sleep(1000); 등과 같은 방식으로 스케쥴링 작업이 끝날때까지 기다리게 하지 않으면 결과가 제대로 보여지지 않는다.

     

    이번에 스케쥴링 기능을 구현하면서 가장 크게 느낀 점은, 구현적인 어려움보다는 기술을 적용할 때 단순히 "유명하고 남들이 많이 사용하니까 나도 사용해야지" 라는 생각보다는 상황에 맞는 기술을 선택하는 것이 가장 올바른 생각 및 방법이라는 것을 다시 한 번 깨닫게 되었다.

    댓글