• 개인 공부 목적으로 작성한 포스팅입니다.
  • 아래 출처를 참고하여 작성하였습니다. :)

적용배경

  • 모든 요청을 단일 쓰레드에서 처리할 수는 없으므로 멀티 쓰레드 구조로의 전환이 필요했습니다.
  • 최소, 리소스 생성같은 역할은 별도의 쓰레드로 분리하여 리소스를 생성하는 과정에서 생기는 오류/장애 분리

handle

  • handle()작업이 완료된 다음에 얻게될 결과예외를 모두 parameter로 사용할 수 있습니다.
  • handle()은 현재 얻은 결과 또는 예외를 다른 값으로 변환할 수 있습니다.
public <U> CompletableFuture<U> handle(
    BiFunction<? super T, Throwable, ? extends U> fn) {
  ...
}

사용하면 좋은 경우

  • handle()을 사용하면 좋은 경우는, 정상 결과와 예외가 모두 필요하며 결과 유형을 변환해야 할 때 적합합니다.
// CompletableFuture<User> to CompletableFuture<Response>
cf.handle((user, ex) -> {
  if (ex != null) {
    return Response.failure("Unknown user");
  } else {
    return Response.success(user);
  }
}

예시 코드

  • handle()은 정상 결과를 첫 번째 인자로, 예외를 두 번째 인자로 받는 BiFunction 인터페이스를 콜백 함수로 받아서 작업을 진행합니다.
  • 만약 앞의 비동기 작업이 정상적으로 진행되었다면 두 번째 인자의 throwable 값은 null이 됩니다.
  • 만약 앞의 비동기 작업에서 예외가 발생했다면 결과값으로 받는 첫 번째 인자 값이 null이 됩니다.
@PostMapping("/join")
public CompletableFuture<ResponseEntity<ApiResult<?>>> join(@RequestBody JoinRequest request) {

    String accessToken = request.getAccessToken();
    OAuthResponse.LoginResponse userInfo = oAuthService.getUserInfo(OAuthRequest.LoginRequest.from(accessToken));

    return userService.create(userInfo.getId(), userInfo.getProfileImage()).handle((userId, throwable) -> {

        if (userId != null) {
            User user = userService.getUser(userId);
            return new ResponseEntity<>(OK(JoinResponse.from(user)), OK);
        }

        return ErrorResponseEntity.from(throwable, true);
    });
}

ErrorResponseEntity

  • 멀티 쓰레드를 적용하면서 가장 안 좋아진 점은 @RestControllerAdvice의 도움을 받을 수 없다는 점입니다.
  • 기존 방식에서는 성공 케이스에 대해서만 정의했으므로 ResponseEntity를 래핑할 필요가 없었지만 별도 쓰레드에서 처리하고자 하는 로직은 성공/예외를 모두 메서드에서 다뤄야 하므로 결과를 ResponseEntity로 래핑해줘야 합니다.
  • 그래서 handle 메소드의 두 번째 파라미터로 받을 수 있는 Throwable을 전담해서 처리하는 클래스를 생성해서 정적 메소드로 예외 응답값을 만들어주었습니다.
public class ErrorResponseEntity {
    public static ResponseEntity<ApiResult<?>> from(Throwable throwable, boolean logFlag) {...}
}

Service 메소드

  • 생성 로직을 별도의 쓰레드에서 처리하도록 @Async 애노테이션을 적용했기 때문에 리소스를 생성하는 서비스 메소드는 다음과 같은 형태를 가집니다.
public class UserServiceImpl implements UserService {

    //...

    @Async
    @Override
    @Transactional
    public CompletableFuture<Long> create(Long oauthId, String profileImageFullPath) {...}

}