외부 API 테스트 및 502 BadGateway.... 처리(RestTemplate, MockRestServiceServer)

2024. 10. 22. 18:23개발

반응형

외부 API 테스트 및 502 BadGateway....


기존 회사에서 핀포인트(APM)을 사용하여 WAS를 모니터링 하고 있는 상태인데..
간헐적으로 이슈가 발생하면 상황을 확인하고 처리하는 경우가 많다.

현재 회사의 경우 DB가 완전히 분리되지는 않았지만 WAS의 경우 여러개가 용도에 맞게 API형태로 사용중이다.
(MSA의 중간 쯤)
API에서의 통신중에서 간헐적으로 502에러가 발생하였고, 해당 에러로그 관련해서 확인한 사항들에 대해서 정리해보았다.

어느날은 에러로그를 남기는 곳에서 특정 주소를 호출한 부분에 에러가 발생했다고 에러 로그가 남았다.
특정 주소 호출시에 502 Bad Gateway가 발생했다고 한다.

다음과 같이 핀포인트에서 확인되는 부분은 API 통신중에 문제가 발생했었다.

콜스택 상세

 

상세 요청 흐름도를 정리해보면 다음과 같다.

전체 요청 흐름도

  1. 예약취소 요청을 보낸다.
  2. 취소처리 후 어드민에 취소반영 요청을 보낸다.
  3. 취소반영 후 외부API에 동기화 요청을 보낸다.
  4. 동기화요청이 실패했다.(502)

핀포인트(APM) 에서 확인해보면 위 와 같이 확인 된다.
API의 요청에 대해서 이벤트 기반으로 대체 하지 않는다고 할 경우. 로직내부에서 다른 API를 요청하거나 하는 형태가 많아지는데.. 이 경우 한군데에서 문제가 발생하면 확인이 어렵거나 처리가 정상적으로 안되거나 하는 경우가 많다...

어디서 발생했는지는 확인 했고, 위와 같은 상황에서 내가 확인하고자 했던건 2가지였다.

커스텀 Exception이 발생하지 않는 이유 확인

커스텀 Exception을 throw하는 로직이 존재했는데 왜 반응하지 못했을까…?
RestTemplate을 사용한 API요청을 공통로직화 시켜서 사용중인데 Exception이 발생하지 않고 처리된 듯 보여져서 추가적으로 찾아보았다.
예를들면 API 통신을 실패하는 경우 다음과 같이 Exception으로 찍히지 않았다.

ResponseEntity<String> resEntity = null;
try{
		resEntity = restTemplate.postForEntity(url, httpEntity, String.class);
		if(!HttpStatus.OK.equals(resEntity.getStatusCode())){
			throw new ApiException(ApiStatus.SYSTEM_ERROR, "http status code : " + resEntity.getStatusCode());
		}
}finally{
	//로그성 정보가공
}

원래 정상적으로 탔다면 catch가 있고 Exception을 던져야하는데
catch가 없었고 API 요청을 정상적으로 처리하고 200으로 API통신이 정상적으로 응답을 받은 경우만 커스텀 Exception으로 보내준 상태에 따라 처리되고 있었다.

현재 작성된 형태로 돌아가는 기존 로직에서 어떻게 동작을 하는게 맞는지 테스트 코드로 테스트 해보기로 했다.

  1. 외부 API 요청시 502인 경우 에러가 throw안된다고 가정하는 경우 정상적으로 타는지.
    • 실제로 502로 응답받았다고 가정 할 경우 커스텀Exception을 타는게 맞는지 확인.
    • 실제로 외부 요청은 하지않는 상태에서 작성된 로직이 제대로 타는건제 확인해보고자 했다.
        @Test
        public void testPostByJson_502Error() {
            // Given: 502 Bad Gateway 응답을 가정한 ResponseEntity
            ResponseEntity<String> resEntity = new ResponseEntity<>("", HttpStatus.BAD_GATEWAY);
    
            try {
                // 비즈니스 로직: 상태 코드가 OK가 아니면 ApiException을 던짐
                if (!HttpStatus.OK.equals(resEntity.getStatusCode())) {
                    throw new ApiException(ApiStatus.SYSTEM_ERROR, "http status code : " + resEntity.getStatusCode());
                }
    
                // 상태 코드가 OK이면 테스트는 실패로 처리
                fail("ApiException이 발생해야 하지만 발생하지 않았습니다.");
            } catch (ApiException e) {
                // ApiException 발생 시 예외 내용 확인
                assertEquals(ApiStatus.SYSTEM_ERROR.getCode(), e.getCode());
                assertTrue(e.getMessage().contains("http status code : 502"));
            }
        }
  2. 외부 API 요청시 502인 경우 에러가 throw된다고 가정하는 경우
    • try catch에서 에러가 발생하여 해당 커스텀 에러를 안 발생시키는게 맞는지 확인.
    • Mock서버를 사용하여 Resttemplate를 사용시 Bad Gateway를 반환하고 에러를 뱉는지 확인하였다.
@RestClientTest
@Slf4j
public class RestTemplateTest {

    private RestTemplate restTemplate;
    private MockRestServiceServer mockServer;

    @Before
    public void setUp() {
        // RestTemplate 인스턴스 생성
        restTemplate = new RestTemplate();
        // MockRestServiceServer 설정
        mockServer = MockRestServiceServer.createServer(restTemplate);
    }


    @Test
    public void testPostForEntity_502Error() {
        // Given: 502 Bad Gateway 응답을 설정
        String url = "http://mocked-url.com";
        mockServer.expect(requestTo(url)).andRespond(withStatus(HttpStatus.BAD_GATEWAY));

        // When Then POST 요청 시도시 HttpServerErrorException 에러가 반환되어야함
        Assertions.assertThatThrownBy(() -> restTemplate.postForEntity(url, null, String.class))
                .isInstanceOf(HttpServerErrorException.class)
                .satisfies(exception -> {
                    HttpServerErrorException httpException = (HttpServerErrorException) exception;
                    // 상태 코드 검증
                    Assertions.assertThat(httpException.getStatusCode()).isEqualTo(HttpStatus.BAD_GATEWAY);
                });


        // Ensure all expected requests were made
        mockServer.verify();
    }

    @Test
    public void testPostForEntity_200OK() {
        // Given: 200 응답을 설정
        String url = "http://mocked-url.com";
        mockServer.expect(requestTo(url)).andRespond(withStatus(HttpStatus.OK));

        // When Then POST 요청 시도시 정상응답일 경우 정상적으로 처리된다.
        ResponseEntity<String> resEntity = restTemplate.postForEntity(url, null, String.class);
        Assertions.assertThat(resEntity.getStatusCode()).isEqualTo(HttpStatus.OK);

        if (!HttpStatus.OK.equals(resEntity.getStatusCode())) {
            throw new ApiException(ApiStatus.SYSTEM_ERROR, "http status code : " + resEntity.getStatusCode());
        }

        // Ensure all expected requests were made
        mockServer.verify();
    }

}


1번과 2번을 가지고 테스트를 해 본 결과.
1번 responseEntity가 502로 받아와졌다고 하면 커스텀 Exception으로 체크가 된다.
2번 restTemplate으로 테스트를 해 보면 HttpServerErrorExcetion를 Throw 한다..!

HttpServerErrorExcetion 이 발생하는 이유

HttpServerErrorExcetion이 발생하는 이유를 살펴보자면 Resttemplate의 내부 구현체를 보면 되는데.
공통로직에서 보통의 경우 Resttemplate를 new 로 생성하여 빈으로 등록하던지 하면서 설정을 하게 되어있다.

  	@Bean
	public RestTemplate restTemplate() {
		return new RestTemplate();
	}

거기에서는 에러가 발생하면 어떻게 처리 할건지에 대해서 남길수가 있는데..
ResponseErrorHandler인터페이스를 받아서 사용하는데 아무런 설정이 없는 경우 기본 구현체인
ResponseErrorHandler인터페이스를 구현한 DefaultResponseErrorHandler를 사용하게 된다.

DefaultResponseErrorHandler

내부 구현체를 살펴보면 다음과 같이 에러를 판별하는데
사용자에러(CLIENT_ERROR), 서버에러(SERVER_ERROR)를 중심으로 갈리고 아예 STATUS코드가 없는 경우에는 default로 throws를 던지는 것 같다.

handleError

만약 코드에 따라 별도의 커스텀 에러를 표현하고 싶다면 구현체를 상속받아서 직접 구현하면된다.
자세한 설명은 다른 블로그에 잘 되어 있는 것 같아서 참고하면 좋을 듯 하다.
만약 일정부분만 수정하고 (예를들면 특정 상황에 따라 커스텀 에러를 발생하는 식으로) 나머지 로직은 동일하게 동작하게 하고싶다면 DefaultResponseErrorHandler를 상속받아서 필요한 로직만 override하여 구현한 다음 사용하는게 더 나을 듯 하다.

 

 

개인정리.

해당 로직이 정말 필요한건지..? 아니면 어떻게 돌아가는지 테스트를 하면서 항상 배우는게 많은 것 같다.
또한 외부API를 테스트를 하게되는 경우 WebClient도 그렇고 Resttemplate도 그렇고 Mock서버로 테스트를 할 수 있게 도와주는 MockServer와 같은 좋은것도 알게되었다.

 

참고하면 좋은 블로그

https://jinseong-dev.tistory.com/7

 

[Spring] RestTemplate 에러 핸들러 커스텀하기

안녕하세요. 황진성입니다. 오늘은 Spring Web에서 제공하는 RestTemplate의 ErrorHandler를 커스텀해보겠습니다. 목적 우리는 RestTemplate으로 클라이언트를 생성해서, 또 다른 서버로 동기(sync) 요청을 보

jinseong-dev.tistory.com

https://shyun00.tistory.com/225

 

[Spring] RestClient, MockRestServiceServer로 단위 테스트하기

애플리케이션에 토스 결제 기능을 추가했다. (참고: 토스페이 결제 연동하기)현재 작성한 애플리케이션은 토스 결제 결과에 따라 로직이 다르게 수행된다.ex. 결제에 성공 -> 예약 성공 / 결제 실

shyun00.tistory.com

 

 

반응형