오늘은 콜백 지옥 탈출을 해볼 것이다.
먼저 다음의 그림을 보자.
출처 : 에스코어
위 그림에서 microservice가 다른 microservice를 호출한 장면을 볼 수 있다.
저 상황을 코드로 살펴보자. (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은 많은 메소드를 제공한다.
(클래스 길이만 2894줄 이다..)
모든 메소드를 알 수는 없다. 그래서 간략하게 몇개만 이용하여 사용해보자.
코드부터 살펴보자.
@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 : 콜백 함수로, 다음 동작을 단순히 내부 구현 메소드로 실행시키는 함수
위의 내용을 살펴보면, 결과는 다음과 같이 나온다.
우리가 생각하는대로 움직인다.
우리는 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이 생길 수 있다.
이 때 처리방법을 보자.
@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);
}
위 코드의 결과 값은 다음과 같다.
앞선 콜백에서의 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을 이용하여 아주 편~안해졌다.