api server java로 변경 (WebFlux Webclient 사용)

최준호·2022년 4월 25일
0
post-thumbnail

😂java로...

express를 사용하여 백엔드를 구현하려고 했는데 시간상 자신 있는 Spring back api서버로 변경하려고 한다. 같이 프로젝트를 진행하는 분들도 exrpess랑 node를 사용해본 경험이 없어서

프로젝트를 만든다...ㅜ 나중에 혼자 꼭 express로 백엔드 만들거다... ㅜㅜ

🔨기본 구조 잡기

dependencies {

	...
    
    //WebClient
    compileOnly 'org.springframework.boot:spring-boot-starter-webflux'
    compileOnly 'org.projectreactor:reactor-spring:1.0.1.RELEASE'
}

WebClient를 추가하기 위해 다음과 같이 의존성을 추가해주었다.

...
client:
  url: ${server_url:http://127.0.0.1:8081}

yml에 다음과 같이 설정해둔 값을 지정해두고

@Component
@RequiredArgsConstructor
public class WebClientConfig {
    private final Environment env;

    @Bean
    public WebClient webClient(){
        return WebClient.builder().baseUrl(env.getProperty("client.url"))
                .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
                .build();
    }
}

WebClient를 사용할때 기본 값을 지정해두고 사용하기 위해서 그리고 WebClient 객체를 하나만 생성하여 spring container에 등록해두고 계속해서 사용하기 위해 Bean으로 등록해두었다.

🔨WebClient filter 추가

@Component
@RequiredArgsConstructor
@Slf4j
public class WebClientConfig {
    private final Environment env;

    @Bean
    public WebClient webClient(){
        ExchangeFilterFunction errorFilter = ExchangeFilterFunction
                .ofResponseProcessor( clientResponse -> exchangeFilterResponseProcessor(clientResponse));

        return WebClient.builder().baseUrl(env.getProperty("client.url"))
                .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
                .filter(errorFilter)
                .build();
    }

    private Mono<ClientResponse> exchangeFilterResponseProcessor(ClientResponse response) {
        HttpStatus status = response.statusCode();
        if (status.is5xxServerError()) {
            return response.bodyToMono(String.class)
                    .flatMap(body -> Mono.error(new ServerError(status, body)));
        }
        if (status.is4xxClientError()) {
            return response.bodyToMono(String.class)
                    .flatMap(body -> {
                        log.error("body = {}",body);
                        return Mono.error(new ClientError(status, body));
                    });
        }
        return Mono.just(response);
    }
}

코드를 작성하다 보니 반환 받았을 때 HttpStatus가 4xx이거나 3xx일 경우에 대한 처리가 필요했다. 이부분을 Bealdong을 참고하여 작성했다.

코드의 내용은 위 기본 WebClient를 생성하는 과정에 filter를 추가해준다.
filter에서는 ExchangeFilterFunction.ofResponseProcessor()을 사용하여 해당 요청의 response를 가로채와서 exchangeFilterResponseProcessor()로 확인할 수 있다.

exchangeFilterResponseProcessor()의 코드는 반환 받은 HttpStatus 값에 따라 각 error를 발생시키고 error 배개변수는 Throwble이 가능한 Exception으로 구현된 객체여야한다. 그래서 나는 각각의 Client용 ServerError()ClientError()를 구현했다.

//400번대 에러 처리
@Getter
public class ClientError extends RuntimeException{
    private HttpStatus status;
    private String body;

    public ClientError(HttpStatus status, String body) {
        this.status = status;
        this.body = body;
    }
}
//5xx 에러
@Getter
public class ServerError extends RuntimeException{
    private HttpStatus status;
    private String body;

    public ServerError(HttpStatus status, String body) {
        this.status = status;
        this.body = body;
    }
}

두 코드는 차이가 없다. HttpStatus 값을 그대로 반환해주기 위해 해당 값을 가져왔고 body는 String으로 json으로 반환 받을것이기 때문에 String 그대로 받았다. 만약 요청 api에서 에러 처리가 제대로되어 있지 않다면 우리 서버에도 500에러로 떨어질거다... 그러니 api를 잘 만들자!

그러면 이제 Excetion을 handling해주어야 한다.

@RestControllerAdvice
public class ClientAdvice {

    //4xx 에러
    @ExceptionHandler(ClientError.class)
    public ResponseEntity<String> clientError(ClientError ce){
        return ResponseEntity.status(ce.getStatus()).contentType(MediaType.APPLICATION_JSON).body(ce.getBody());
    }

    //4xx 에러
    @ExceptionHandler(ServerError.class)
    public ResponseEntity<String> serverError(ServerError se){
        return ResponseEntity.status(se.getStatus()).contentType(MediaType.APPLICATION_JSON).body(se.getBody());
    }
}

에러 핸들링은 다음과 같이 간단하게 구현했다. 반환 할때 String 그대로 반환하면 Json으로 인식하지 못하기 때문에 MediaType만 추가해서 반환했다.

🔨Controller 작성

@RestController
@RequestMapping("/member")
@Slf4j
@RequiredArgsConstructor
public class MemberController {
    private final MemberService memberService;

    //회원가입
    @PostMapping("/join")
    public ResponseEntity<String> join(@RequestBody RequestMember requestMember){
        ResponseEntity<String> responseEntity = memberService.join(requestMember);
        return ResponseEntity.status(responseEntity.getStatusCode()).contentType(MediaType.APPLICATION_JSON).body(responseEntity.getBody());
    }
}

🔨Service 작성

Service

public interface MemberService {
    ResponseEntity<String> join(RequestMember requestMember);
}

ServiceImpl

@Service
@RequiredArgsConstructor
public class MemberServiceImpl implements MemberService{
    private final WebClientConfig webClient;

    @Override
    public ResponseEntity<String> join(RequestMember requestMember) {
        ResponseEntity<String> result = webClient.webClient().post()  //get 요청
                .uri("/login-service/game/join")    //요청 uri
                .body(Mono.just(requestMember), RequestMember.class)
                .retrieve()//결과 값 반환
                .toEntity(String.class)
                .block();
        return result;
    }
}

👏테스트 해보기

다음과 같이 요청하면 내가 작성한 api 반환 내용으로 정상적으로 된다.

💭잘못 요청해서 에러가 발생하면??

중복 가입 방지를 해두었는데 그대로 요청하니 예상했던 에러대로 반환이 된다.

입력값에 대한 유효성 검사도 제대로 잘 된다.

📘왜 WebClient?

굳이 WebClient를 사용하지 않고도 api를 요청하는 방법은 많다. 예를 들어 java에서 지원해주는 Url을 사용하여 connection을 열고 요청 값을 보내고 값을 파싱해오고 에러나면 에러 처리하고...

이런 복잡한 과정이 싫었다. node 처럼 axios와 같은 라이브러리를 사용하여 간단하게 요청하고 반환 받을 수 있지 않나? 라는 생각으로 시작해서 RestTemplate와 FeignClient 가 존재하긴 했지만,

Spring 재단에서 가장 권장하는 WebClient를 사용하는 편이 더 낫겠다라는 생각으로 사용해봤는데 역시 axios처럼 쉽게 사용할 순 없지만 그래도 spring의 장점을 그대로 살리면서 api를 좀더 유연하게 사용하고 에러처리까지 복잡하지 않게 처리할 수 있게되어서 너무 재밌었다. 이제 이걸로 로그인까지 얼른 처리해보자!

비록 node를 사용하지 못하지만...

profile
코딩을 깔끔하게 하고 싶어하는 초보 개발자 (편하게 글을 쓰기위해 반말체를 사용하고 있습니다! 양해 부탁드려요!) 현재 KakaoVX 근무중입니다!

0개의 댓글