일친 (IlChin) - Webflux로 외부 API 호출

no.oneho·2025년 6월 18일
0

일친 개발기

목록 보기
17/17

깃헙에 등록된 repo의 정보를 등록해야할 차례가 왔다.

그럴려면 깃허브의 api를 호출해야할 필요가 있고, repo를 등록할 때 유효성을 먼저 검증하도록 하겠다.

주로 api를 스프링에서 호출할 때 사용하는 방식으로는

주로 RestTemplate와 WebClinet를 이용한 방법이 있는데 RestTemplate의 경우 현재 maintenance모드(더 이상 기능추가가 안된다)이기 때문에 계속해서 기능및 관리가 되는 WebClinet를 사용하기로 했다.

WebClinet를 사용하려면 webflux 의존성을 먼저 추가해줘야한다.

implementation 'org.springframework.boot:spring-boot-starter-webflux'

를 gradle에 추가하여 의존성을 받아준다.

API를 호출하게 되면 코드도 비동기성 성질을 띄게 만드는것이 시간이나 관점측면에서 매우 효율적인데 webflux를 이용하여 reactive 프로그래밍을 사용할 수 있게되고
그러한 비동기 시퀀스들을 사용할 수 있게 만들어주는게 webflux의 Mono와 Flux이다

여기서는 Mono 객체를 사용하도록 하겠다.

아래는 트러블슈팅의 과정이므로 완성코드는 맨 아래에 위치해있으니 조심!

@RestController
@RequestMapping("api/github")
@RequiredArgsConstructor
public class GithubRepositoryInfoController {

    private final GithubRepositoryInfoService githubRepositoryInfoService;

    @Auth
    @PostMapping
    public Response<GithubRepositoryInfoCreate> createGithubRepositoryInfo(@RequestBody GithubRepositoryInfoCreate request) {
        return Api.success(200, "깃헙 레포 등록 완료", githubRepositoryInfoService.createGitHubRepositoryInfo(request));
    }

}

먼저 컨트롤러를 만들고,

    @Transactional
    public GithubRepositoryInfoCreate createGitHubRepositoryInfo(GithubRepositoryInfoCreate request) {
        Mono<String> apiResponse = webClient.get()
                .uri(API_URL + "/repos/{owner}/{repo}", request.owner(), request.repo())
                .headers(header -> header.setBearerAuth(request.token()))
                .retrieve()
                .bodyToMono(String.class)
                .onErrorMap((e) -> {
                    throw new CustomException(ExtendApiException.BAD_REQUEST_INFO);
                });

        apiResponse
                .flatMap(response -> {
                    if (response == null || response.trim().isEmpty()) {
                        return Mono.error(new CustomException(ExtendApiException.BAD_REQUEST_INFO));
                    }
                    return Mono.empty();
                })
                .subscribe(
                        success -> {
                        },
                        error -> {
                            throw new CustomException(ExtendApiException.BAD_REQUEST_INFO);
                        }
                );

        GithubRepositoryInfo githubRepositoryInfo = GithubRepositoryInfo.createEntity(request.owner(), request.repo(), request.token());
        repositoryInfoRepository.save(githubRepositoryInfo);

        return new GithubRepositoryInfoCreate(githubRepositoryInfo.getOwner(), githubRepositoryInfo.getRepo(), githubRepositoryInfo.getToken());
    }

서비스를 만들었다. 직접 코드를 실행해보니 저장도 잘되고 코드도 잘 실행되는거같았다.

유효성 검증을 위해 일부러 맞지않는 값을 넣었는데..

콘솔에는 예외가 찍히나

실제 코드 동작은 예외가 캐치되지않고 끝까지 코드가 실행되었다.

왜그랬을까를 생각해보니 Mono로 선언한 부분은 비동기로 코드가 실행됨에 따라 다음 코드들이 github api 호출에 대한 결과값을 기다리지않고 실행되는것이였다.

그러다보니 메서드가 객체를 반환 할 때 reactive 스트림의 실행결과를 기다리지 않아 수정할 필요가 생겼다.

이에따라 컨트롤러부터 Mono타입을 반환하게 변경하였다.
그리고 Api 호출이 끝나기를 대기하고 코드를 실행하도록 변경하였다.

@RestController
@RequestMapping("api/github")
@RequiredArgsConstructor
public class GithubRepositoryInfoController {

    private final GithubRepositoryInfoService githubRepositoryInfoService;

    @Auth
    @PostMapping
    public Mono<Response<GithubRepositoryInfoCreate>> createGithubRepositoryInfo(@RequestBody GithubRepositoryInfoCreate request) {
        return githubRepositoryInfoService.createGitHubRepositoryInfo(request)
                .map(result -> Api.success(200, "깃헙 정보 생성 완료", result));
    }

}

서비스코드도 변경된 리턴값에 따라 수정하여

 @Transactional
    public Mono<GithubRepositoryInfoCreate> createGitHubRepositoryInfo(GithubRepositoryInfoCreate request) {
        return webClient.get()
                .uri(API_URL + "/repos/{owner}/{repo}", request.owner(), request.repo())
                .headers(header -> header.setBearerAuth(request.token()))
                .retrieve()
                .bodyToMono(String.class)
                .flatMap(response -> {
                    if (response == null || response.isEmpty()) {
                        return Mono.error(new CustomException(ExtendApiException.BAD_REQUEST_INFO));
                    }
                    GithubRepositoryInfo githubRepositoryInfo = GithubRepositoryInfo.createEntity(request.owner(), request.repo(), request.token());
                    return Mono.fromCallable(() -> repositoryInfoRepository.save(githubRepositoryInfo))
                            .subscribeOn(Schedulers.boundedElastic())
                            .map(savedInfo -> new GithubRepositoryInfoCreate(savedInfo.getOwner(), savedInfo.getRepo(), savedInfo.getToken()));
                })
                .onErrorMap((e) -> {
                    throw new CustomException(ExtendApiException.BAD_REQUEST_INFO);
                });

    }

이와같은 최종코드가 나오게되었다.

profile
이렇게 짜면 요구사항이나 기획이 변경됐을 때 불편하지 않을까? 라는 생각부터 시작해 설계를 해나가는 개발자

0개의 댓글