스프링에서 비동기 프로그래밍으로 무거운 로직 처리

Adam·2022년 6월 30일
0

트러블슈팅

목록 보기
4/4

전에 다니던 회사에선 Vert.x라는 완전 비동기 프레임워크를 사용해서 반강제적으로 비동기 프로그래밍에 대해서 많이 공부를 하게 되었었다.
사실, 효율성 측면에서 Vert.x가 좋은 프레임워크 일 수는 있으나, 필연적으로 코딩을 할때 지속적으로 람다 함수를 사용을 할 수 밖에 없고, 그렇기 때문에 코드가 지저분해지고 유지 보수 측면에서도 매우 비효율적이다.
따라서 Vert.x를 공부하고 싶다는 사람이 있다면 정말 뜯어 말리고 싶고, 정말 컴퓨팅적인 효율성을 위해서 완전 비동기 프레임워크를 사용해야 한다고 하더라도 차라리 스프링 웹플럭스라는 보다 커뮤니티가 활성화 돼있는 프레임워크를 사용하는 편이 훨씬 낫다고 생각한다.

비동기 프로그래밍이란?

컴퓨터 통신에는 동기와 비동기 프로그래밍이 존재한다.
동기 방식은 어떠한 프로세스가 주어졌을때, 해당 프로세스를 순차적으로 처리하는 방식이고, 비동기 방식에서는 해당 프로세스들을 순서에 상관 없이 처리를 하게 된다.
비동기 방식에서는 순서에 상관 없이 처리를 할때 스레드를 여러개 생성하여 해당 프로세스들을 처리하게 되기 때문에, 일반적으로 멀티 스레드 프로그래밍에 대한 지식이 있는 상태로 프로그램을 짜야한다.

동기 통신과 비동기 통신에 대해서 보다 편리하게 이해하는데 아래 그림이 도움이 될 것 같다.

왼쪽의 동기 방식에서는 1->2->3->4->5번이 순차적으로 진행이 되는데, 1번 프로세스가 끝날때까지 2번 프로세스는 기다리게 된다.
이에 반면 오른쪽의 비동기 방식에서는 각각의 스레드에서 1,2,3,4,5번 프로세스를 각각 실행하기 때문에 그림에서 나온 것처럼 동기 방식에서 45초가 걸릴 로직을 20초만에 해결 할 수 있다.

주의점

위 그림과 같이 비동기 방식이 동기 방식보다 훨씬 시간적인 측면을 봤을때 효율적이라는 것을 확인할 수 있다.
그렇다면 스프링과 같은 대부분의 프레임워크는 동기 방식이 기본 옵션인데, 왜 버택스 같은 비동기 프레임워크를 사용하지 않는 것일까?

1. 코드의 복잡성과 유지보수가 어렵다

위 그림을 예시를 들어보겠다.
1번 프로세스에서 db에서 특정 값을 조회해와서 가공을 한 뒤 2번 프로세스에서 해당 값을 저장을 해야 한다고 가정해보자.
동기 방식에서는 해당 작업이 별 문제 없이 실행이 될 것이다.
하지만, 비동기 방식에서는 1번 프로세스에서 해당 값을 가져와서 처리하기 전에 2번 프로세스에서 널인 상태의 값을 db에 저장을 해버릴 것이다.
이것을 방지하기 위해 콜백함수를 사용하여 1번 프로세스를 완료했을때 2번 프로세스를 실행하게끔 하는데, 이 과정에서 콜백헬이 발생하게 된다.

물론 해당 과정을 해결하기 위해 퓨처와 프로미스를 사용하는 방법도 있으나, Vert.x를 9개월 간 매일 사용해본 경험 상, 아무리 퓨처와 프로미스를 잘 사용한다 하여도 코드가 지저분해질 수 밖에 없고, 코드가 지저분한만큼 유지보수하기에도 매우 어려웠다.

2. 사용하기 어렵다

일반적으로 사람들의 사고방식은 동기다.
예를 들어 라면을 끓인다고 했을때 물을 붇고, 스프를 넣고, 면을 넣은 후, 익을때까지 기다린 이후, 라면을 먹는다.
이 과정이 전부 동기식으로 처리가 되지, 비동기 방식으로 라면을 끓이는 사람은 없을 것이라고 생각된다.
그렇기 때문에 동기식으로 짜여진 프레임워크를 익숙하게 사용하는 것은 해당 언어와 프레임워크에 대한 지식이 있다면 크게 어렵지 않다.
하지만, 비동기 방식의 경우 사람들이 비동기 방식으로 일을 처리하거나 사고하는 경우가 동기 방식보다는 적기 때문에 익숙하지 않기 때문에, 비동기 프레임워크를 사용할때 실수를 하는 경우가 동기 프레임워크 보다는 많이 발생한다.
개인적으로 많이 했던 실수가 null값을 db에서 계속 조회해오는 것이나, db에 null값을 계속 저장하던 것이다.

트러블 슈팅

현 어플리케이션에서 상품 판매 시작과 판매 종료를 할때 알람을 보내는 기능까지 시작과 종료에 포함이 되어있었다.
해당 알람을 보내는 로직이 db 순회도 여러번 이루어 나고, 로직 자체가 복잡했기 때문에, 판매 시작과 종료가 원하는 시간이 아닌 대략 15분 정도 딜레이가 발생하는 이슈가 발생했었다.

우선 스프링의 공식 문서를 통해서 해당 문제를 해결할 수 있는 방법이 있을까하고, 해당 로직이 스케쥴러를 통해서 실행이 되었기 때문에, 스케쥴러 부분과 비동기 부분을 읽었으나, 스케쥴러에서 비동기 로직을 사용해도 되는지에 대한 명쾌한 설명을 찾기는 어려웠다.
공식 문서에서 명쾌한 답변을 찾기 어려웠던만큼 나와 비슷한 문제를 겪고 있는 사람이 있는지 구글링 해보았으나, 이 역시 찾지 못하였다.
마지막으로 스택오버플로우에 해당 내용을 질문하였다.
고맙게도 어떤 분이 매우 친절한 설명을 해주었다.

그분의 말씀에 따르면 스프링의 경우 기본적으로 스프링의 스케줄러는 싱글 쓰레드이지만 application.properties 또는 application.yaml에서 쓰레드 수를 설정할 수 있다.

  #schedule multi-thread
  task:
    scheduling:
      pool:
        size: 8

그리고 비동기 로직을 사용하려는 클래스에서 @EnableAsync 애노테이션을 달아주고, 비동기를 사용하려는 로직에 @Annotation 애노테이션을 달아주면 된다고 했다.

@Component
@EnableAsync
public class Scheduler {

그리고 비동기로 처리하려는 로직은 다음과 같다.

@Async
    public void pushAsyncStart(SchGoodsAuctionStartListRes item) throws Exception {
        // PUSH: 판매자
        PushInfo pushInfo = pushMapper.pushGoodsSeller(item.getGoodsIdx());
        pushInfo.setTitle("턴백 셩매 시작 알림");
        pushInfo.setBody("[" + pushInfo.getBrand() + "] 상품의 경매가 시작되었습니다.");
        pushInfo.setPushGrp("001");
        pushInfo.setPushCode("003");
        fcmPushUtil.sendPush(pushInfo);

        // PUSH: 판매자 외 전체
        List<PushInfo> pushInfos = pushMapper.pushGoodsAuctionAll(item.getIdx());
        for (PushInfo pushInfoItem : pushInfos) {
            pushInfoItem.setTitle("\uD83D\uDD14 턴백 경매 오픈");
            pushInfoItem.setBody("[" + pushInfo.getBrand() + "] 상품의 경매가 시작되었습니다. \uD83D\uDC5C");
            pushInfoItem.setPushGrp("002");
            pushInfoItem.setPushCode("008");
            fcmPushUtil.sendPush(pushInfoItem);
        }
    }

비동기 로직을 불러와서 사용하는 스케쥴러에는 특별한 점 없이 호출만 해오면 된다.

@Scheduled(fixedDelay = 10000)
    public void startAuction() throws Exception {
        List<SchGoodsAuctionStartListRes> list = schedulerService.schGoodsAuctionStartList();

        for (SchGoodsAuctionStartListRes item : list) {
            // 경매 시작
            schedulerService.schGoodsAuctionStart(item);

            // 비동기로 무거운 부분 로직 처리
            pushAsyncStart(item);

            System.out.println("==================================================");
            System.out.println(" - 경매 시작 처리");
            System.out.println(" - goods auction idx: " + item.getIdx());
            System.out.println("==================================================");
        }
    }

적용 결과

원하던대로 상품 판매는 정시에 시작하게 되었으며, 푸시 알람에는 딜레이가 약간 발생하였지만 푸시 알람 때문에 판매가 딜레이가 발생하던 현상은 해결 할 수 있었다.
스프링에서 비동기 처리가 이렇게 사용하기 편리하게 되어 있었는데, 이전 회사는 왜 굳이 Vert.x를 고집했는지 아직도 이해가 되지 않는다.
역시 사람들이 많이 쓰는 프레임워크는 많이 쓰는 이유가 있고, 많이 사용하지 않는 프레임워크 역시 마땅한 이유가 있는 것 같다.

profile
Keep going하는 개발자

0개의 댓글