[Reactive Java #5] 콜백 지옥 탈출하기

YoungHo-Cha·2022년 10월 3일
2

Web Flux

목록 보기
6/6
post-thumbnail

오늘은 콜백 지옥 탈출을 해볼 것이다.

목차

  • 콜백이 생기는 상황
  • 자바에서 제공해주는 콜백 지옥 탈출법

콜백이 생기는 상황

먼저 다음의 그림을 보자.

출처 : 에스코어

위 그림에서 microservice가 다른 microservice를 호출한 장면을 볼 수 있다.

  1. 1번 서버는 2번 서버에게 데이터를 요청한다.
  2. 받은 데이터를 3번 서버에게 넘겨서 다른 데이터를 요청한다.
  3. 3번 서버에게서 데이터를 받은 1번서버는 사용자에게 리턴한다.

저 상황을 코드로 살펴보자. (1번 서버 입장)


@GetMapping("/msa")
    public DeferredResult<String> msa(){
        AsyncRestTemplate rt = new AsyncRestTemplate(new Netty4ClientHttpRequestFactory(new NioEventLoopGroup(1)));
        DeferredResult<String> dr = new DeferredResult<>();

        ListenableFuture<ResponseEntity<String>> f1 = rt.getForEntity("1번 서버", String.class);
		//------ 콜백 1번 ---------
        f1.addCallback(s ->{ // s = ResponseEntity<String>
            //2번 서버에게서 응답을 성공적으로 받았을 때
            ListenableFuture<ResponseEntity<String>> f2 = rt.getForEntity("2번 서버", String.class, s.getBody());
            
            // ------ 콜백 2번 -------
            f2.addCallback(s2 -> {
                dr.setResult(s2.getBody()); //3번 서버에서 받은 데이터를 DeferredResult에 저장
            }, e2 -> {
                // 3번 서버에서 요청 에러시
                dr.setErrorResult(e2.getMessage());
            });
            
            
        }, e ->{
            // 2번 서버에서 요청 에러시
            dr.setErrorResult(e.getMessage());
        });
        return dr;
    }

지금은 2개의 서버에 요청을 날려서 콜백 지옥이 2단계 까지만 내려간 것을 볼 수 있다. 하지만..... 서버가 엄청나게 많고..... 각 콜백 요청 로직이 엄청나게 길면 코드가 너무 어려워진다.


자바가 제공해주는 콜백 지옥 탈출법

위에서 콜백지옥에 빠지게 되는 현상을 보았을 것이다.

이를 해결하기 위해서 자바 진영에서는 jdk 8 부터 "CompletableFuture"을 제공한다.

CompletableFuture

CompletableFuture은 많은 메소드를 제공한다.
(클래스 길이만 2894줄 이다..)

모든 메소드를 알 수는 없다. 그래서 간략하게 몇개만 이용하여 사용해보자.

return을 받지 않을 때

코드부터 살펴보자.

@GetMapping("/async/call-back-hell/solution")
    public void callBackHellSolution() throws InterruptedException {
        CompletableFuture
                .runAsync(() -> log.info("runAsync"))//한번 실행할 코드를 명시하는 함수
                .thenRunAsync(() -> log.info("thenRunAsync"))
                .thenRunAsync(() -> log.info("thenThenRunAsync"));
        log.info("exit");

        ForkJoinPool.commonPool().shutdown();
        ForkJoinPool.commonPool().awaitTermination(10, TimeUnit.SECONDS);
    }

내용을 살펴보면 아주 간단하게 chain을 하여 함수들을 콜백하는 것을 볼 수 있다.

  • runAsync : 단순히 내부구현만 사용하여 내부구현 메소드를 실행시키는 함수

  • thenRunAsync : 콜백 함수로, 다음 동작을 단순히 내부 구현 메소드로 실행시키는 함수

위의 내용을 살펴보면, 결과는 다음과 같이 나온다.

우리가 생각하는대로 움직인다.

return이 필요할 때

우리는 1번 서버에서 나온 데이터를 2번 서버에서 가공하고싶다. 그러기 위해서는 내부 구현을 실행한 결과를 받을 수 있어야 한다.

@GetMapping("/async/call-back-hell/solution/chain/exception")
    public void callBackHellSolutionChainException() throws InterruptedException {
        CompletableFuture
                .supplyAsync(() -> {
                    log.info("supplyAsync");
                    return 1;
                })
                .thenApply(s -> {
                    log.info("thenApplyAsync");
                    if(true) throw new RuntimeException();
                    return s + 1;
                })
                .thenApply(s2 -> {
                    log.info("thenThenApplyAsync");
                    return s2 + 100;
                })
                .exceptionally(e -> -10)
                .thenAccept(s3 -> log.info("result : {}", s3));
        log.info("exit");

        ForkJoinPool.commonPool().shutdown();
        ForkJoinPool.commonPool().awaitTermination(10, TimeUnit.SECONDS);
    }

이를 위해서

  • supplyAsync : 내부 구현을 실행하고 결과를 리턴한다.

  • thenApply : 앞선 동작에서 콜백으로 동작하며, 리턴 값을 이어 받는다.

위 코드를 실행하면 결과는 다음과 같다.

비동기이니까 exit는 상관하지않고, 다른 동작을 보면 result가 102가 나온 것을 볼 수 있다. return 값을 정상적으로 받고 정상적인 처리를 하였다.

콜백 중 Exception 처리

콜백 중간에 Exception이 생길 수 있다.

이 때 처리방법을 보자.

 @GetMapping("/async/call-back-hell/solution/chain/exception")
    public void callBackHellSolutionChainException() throws InterruptedException {
        CompletableFuture
                .supplyAsync(() -> {
                    log.info("supplyAsync");
                    return 1;
                })
                .thenApply(s -> {
                    log.info("thenApplyAsync");
                    if(true) throw new RuntimeException();
                    return s + 1;
                })
                .thenApply(s2 -> {
                    log.info("thenThenApplyAsync");
                    return s2 + 100;
                })
                .exceptionally(e -> -10)
                .thenAccept(s3 -> log.info("result : {}", s3));
        log.info("exit");

        ForkJoinPool.commonPool().shutdown();
        ForkJoinPool.commonPool().awaitTermination(10, TimeUnit.SECONDS);
    }
  • exceptionally : 콜백 중 exception을 만날 경우 내부 구현을 실행한다.

위 코드의 결과 값은 다음과 같다.

앞선 콜백에서의 return은 무시하고 result가 최정적으로 -10이 리턴된 것을 볼 수 있다.


콜백지옥 해결하기

위의 내용은 단순히 log만 찍는 것이다. 우리는 ResponseEntity 형태로 요청자에게 응답하고 싶다.

문제는 AsyncRestTemplate다. 해당 클래스의 getForEntity는 ListenableFuture을 리턴한다. 이를 해결해보자.

먼저 다음 함수를 생성하자.

<T> CompletableFuture<T> toCF(ListenableFuture<T> lf){
        CompletableFuture<T> cf = new CompletableFuture<>();
        lf.addCallback(s -> cf.complete(s), e -> cf.completeExceptionally(e));
        return cf;
    }

이 코드는 getForEntity가 리턴하는 ListenableFuture을 CompleteFuture로 감싸서 특별한 동작을 하기위해서 생성한 것이다.

이를 이용하여 코드를 고쳐보자.

@GetMapping("/async/call-back-hell/solution/chain2")
    public DeferredResult<String> solution(){
        AsyncRestTemplate rt = new AsyncRestTemplate(new Netty4ClientHttpRequestFactory(new NioEventLoopGroup(1)));
        DeferredResult<String> dr = new DeferredResult<>();
        toCF(rt.getForEntity("1번 요청", String.class))
                .thenCompose(s -> toCF(rt.getForEntity("2번 요청", String.class, s.getBody())))
                .thenAccept(s3 -> dr.setResult(s3.getBody()))
                .exceptionally(e -> {dr.setErrorResult(e.getMessage()); return (Void) null;}
                );

        return dr;

    }

최종 코드이다.

우리는 콜백을 CompleteFuture을 이용하여 아주 편~안해졌다.


참고

profile
관심많은 영호입니다. 궁금한 거 있으시면 다음 익명 카톡으로 말씀해주시면 가능한 도와드리겠습니다! https://open.kakao.com/o/sE6T84kf

0개의 댓글