Post

API의 응답 바디는 커도 좋을까

서론 : API 설계 시 든 의문

API 응답과 요청 과정에서의 오버헤드는 얼마나 성능에 영향을 끼칠까요?

예를 들어 여러 카테고리 열에 담긴 카드 정보를 불러오는 API를 작성해야할 때가 있었는데, 저는 전체 데이터를 한번에 가져오는게 아니라, 열의 인덱스 번호를 URL에 담아서 보내고, 해당 열에 대응하는 카드들을 가져오는 API 로 설계를 했었습니다.

이렇게 설계를 했던 이유는 사실 REST 원칙에서 “API 계층적으로 구성해야한다”는 원칙에 따라 구성을 하였기 때문입니다. 사실 카드 인덱싱을 하는데 있어서 계층적으로 구성하려면 URL 파라미터를 /columns/cards 이런식으로 불러오도록 만드는게 좋지 않았을까 하는 생각이 듭니다.

API의 응답 바디는 과연 커도 상관이 없을까?

하지만 그때 의문이 들었습니다. API 요청을 받았을 때, 만약에 전체 카드에 대한 정보를 API로 요청을 하였다면, 만약 보내야할 응답의 바디가 커진다고 가정하면, 패킷의 경우 응답 바디가 커지게 되면 애플리케이션 단에서 보내게되는 패킷을 트래픽에 맞게 나누어서 보내야한다고 알고 있는데, 그러면 라우팅의 수가 많아지니 손해가 아닐까라는 것이었습니다.

요청을 여러번하면, 그만큼 네트워크 상황에 따라 최적화되어 보내지지 않을까라고 순수하게 궁금해했던 것 같습니다. 단순하게 생각해보면 HTTP 요청은 애플리케이션 단 이므로 의미가 없지만, 실제로 나누어 보내는게 오버헤드가 얼마나 큰지도 궁금하기도 했습니다.

이미지를 불러오기 위한 서버와 이미지 처리 방식

이게 신기한 점이, 실제로 노션과 같은 경우 이미지를 불러오는 방식은 따로 입니다. 비교적 용량이 큰 이미지의 경우 요청을 별도로 된다는 뜻인데, 노션의 경우 일반적으로 문서가 블락단위로 나누어서 각각의 블락을 하나의 객체로 담아서 API로 응답 바디에 담아서 보냅니다.

실제 노션 API 를 통해서 노션 페이지를 제 개인 깃허브 페이지로 배포해보려고 예전에 잠깐 찾아봤었는데 (물론 완전히 해보진 못했지만..) 노션에서는 페이지를 불러올 때 각 블락단위로 불러오고, 이때 이미지의 경우 이것을 하나의 “파일” 취급합니다. 노션에서는 파일 블록에는 별도의 url 참조만 올려놓습니다.

노션 파일 링크

전체 블락을 가져오게 되면 모든 페이지의 내용이 구현되어야하지만, 최근 슬랙에서 다른 동료 캠퍼분께서 이미지만 로딩이 안되는 현상을 보셨을 겁니다. 사실 이러한 현상은 이미지가 별도의 파일로 취급되기 때문에, 블락은 불러와지고, 이미지와 연결된 파일의 url 을 통해 불러오는 것에 실패했기 때문입니다.

일반적으로 백엔드 기술 중에 많이 알려진 로드 밸런싱 같은 별도의 확장 설계를 한 이유는 하나의 API로 불러오는 응답의 내용이 많아지기 때문에 이렇게 해결한 것이라고 저는 추측합니다.

그래서 API 응답의 바디가 큰 하나의 API를 사용하는 것이 좋을까, 그게 아니라면, 내가 생각한 것 처럼 여러개의 API를 쓰더라도 응답 바디가 작은게 좋을까 라는 것에 대한 의문이 커지기 시작했습니다. 다른 말로 저런 이미지도 한번에 불러오면 되는 거 아닌가? 라는 것이죠.

사실 트래픽 상으로만 보면 한번에 담아서 보내는게 이득일 것 입니다. 하지만 “왜?”라는 질문을 생각해보는게 우선이라고 생각했고, 아래와 같이 좀 더 깊게 고민해볼 수 있는 시간이었던 것 같습니다.

왜? 라는 질문을 생각해보기

“요청 응답 헤더가 포함되어서 오버헤드가 커져서 그래요” 라고 단순히 생각하고 끝낼 수도 있을 것입니다. 하지만 그렇게 단순히 생각할 수가 없을지도 모릅니다.

첫번째로, 만약 요청 응답 헤더를 무시할만큼 바디가 커지게 되면 패킷을 나누어야 합니다. (근거 : 내 학교 수업 중 “데이터통신, 자세한건 IPv4 패킷 전달 과정 참고”) 그렇게 된다면, 패킷을 나눈 만큼 네트워크 단의 패킷의 헤더는 다시 붙게 되고 오히려 전달해야할 패킷은 더 많아집니다. 즉, 큰 차이가 없게 될 것이란 거죠.

두번째로, 한번에 보내는 애플리케이션의 요청 혹은 응답이 많아진다면, 패킷 로스로 인한 복구에 드는 시간이 증가할 수도 있다는 점입니다. 그렇게된다면 적은 크기를 주고받는 API를 여러번 호출하는게 이득이 될 수도 있지않을까란 생각입니다.

사실 두번째에 관한 실험을 제대로 진행해볼 순 없었는데, 혹시 알고있는 분이 있다면 댓글로 정보공유해주시면 감사하겠습니다.

실제로 스프링부트를 사용하다가 응답 바디가 큰 경우 중간에 패킷로스로 인해서 다시 보내야하는 경우가 있다고 한다.

Data loss when calling a HTTP request with huge response body

하지만 이런 것도 직접 실험을 해보고 원인을 찾아보는게 의미 있다고 생각합니다. 그래서 주말에 남는 시간에 직접 실험을 해보았고, 배울 점들을 많이 발견할 수 있었던 것 같습니다.

API 네트워크 테스트 진행

우선 위에 언급한 제 의문들에 대한 영향을 최대화 하기 위해 파일의 크기를 다르게 해서 진행했습니다. API 응답 바디에 큰 JSON 파일을 보내주었습니다.

또한, 라우팅에 대한 최적화가 진행될 수 있다는 가정하에,

하지만 돈이 없어(…) 서버를 사지 못하므로 나와 서버 간의 루트가 최적화 되어있는지는 별도로 환경을 분리하지 못함을 인식해주길 바라겠습니다.

데이터 파일 크기 선정 기준

일단, IPv4 데이터 패킷이 65535Byte 이므로, 64KB 가 넘는 JSON 파일이 필요했습니다. 마침 구글링을 해보니까 실제로 크기에 따른 JSON 파일들을 제공해주는 사이트가 있었습니다. 다른분들도 필요하시면 참고하면 좋을 것 같습니다.

해당 파일로 진행

해당 파일로 진행

20 MB Dummy JSON File - Sample Json File Free Download

Postman

실험을 하려고 여러 툴을 찾아보았는데, 어쩌다보니 Postman 에 대해서 사용해보게 되는 경험을 갖게 되었습니다. 주변에 많은 백엔드 개발자 친구들이 애용하는 툴이였는데, 그때까지만 해도 저는 이걸 왜 쓰는가? 그냥 브라우저에서 요청해보면 되지 않을까? 라고 생각했었는데, 프론트엔드를 만들지 않은 상황에서는 이게 엄청 편했었습니다.

사용방법

Postman 사용방법은 정말 간단합니다.

image.png

메인 화면에 요청 방식을 선택하고, 요청 주소를 쓰면 끝입니다.

paramsURL 에 포함해서 보내는 파라미터를 뜻합니다.

요청 body 는 Body 항목에 이런식으로 입력하면 됩니다.

image.png

또 HTTP 뿐만 아니라, 웹소켓, GraphQL 등의 요청도 보낼 수 있는 것 같습니다. 여러모로 유용할 것 같습니다!

image.png

실험 과정

솔직히 말해서 로컬 서버를 통해서 요청들을 받아냈고, 다른 영향 요소들을 완벽히 분리하진 못해서 엄밀히 정확한 측정을 하진 못했습니다.

하지만 그 과정에서 얻어낸 결론과 공부할 점들이 많았습니다.

10mb 크기의 JSON을 반환하는 요청 2번, 20mb 크기의 JSON을 반환하는 요청 1번을 각각 10번정도씩 실행하였고, 그 결과를 비교해보려고 합니다.

  • 10mb 요청 → 총 트래픽 9.81mb, 소요시간 110ms ~ 86ms 까지 내려감

image.png

  • 20mb → 총 트래픽 19.44mb, 소요시간 211ms ~ 155ms 까지 내려감

image.png

API 실험 결과

  • 고려해야할 요소
    • 요청을 많이 받으면 서버가 받을 부하도 생각해야합니다.
    • 정보를 보내는 경우 데이터베이스 쿼리를 자주 수행해야할 것임을 인지해야합니다.
    • 만약 그렇다면, 동시성 요소도 고려해야합니다.
      • 예시 : API 요청을 여러번 보내고 받는 도중, 다른 곳에서 수정이 일어났다면?
  • 알아낸 점
    • 의외로 API 요청과 응답의 헤더의 크기는 상관 없습니다.
      • 헤더의 경우 둘다 합쳐서 500Byte 도 채 되지 않았습니다.
    • 시간에 영향을 주는 요소는 다른 곳에 있었습니다.
      • HTTP는 TCP 로 소통하고 응답을 받습니다.
        • TCP 라서 소켓 연결이 이루어지는 게 아닙니다.
        • 따라서 API 요청은 TCP 소켓을 연결한 후, 작업이 끝나면 다시 연결을 닫는 구조로 이루어집니다.
        • WebSocket 을 통해서 지속적으로 연결할 수 있는 것으로 보입니다. (추측)

API가 요청을 보내고 받는 과정

이것을 이해하려면 API 요청을 보내고 받는 과정을 이해해야합니다. HTTP 요청은 TCP 요청인데, TCP 요청은 요청을 보내고, 응답을 받아야합니다. 그 과정에서 TCP 연결을 하게 됩니다. HTTP의 전송 프로토콜은 TCP 입니다.

그 과정에서 생기는 TCP 의 특징으로 인해 이러한 부분에서 오버헤드가 더 큼을 알 수 있을 것 같습니다.

[TCP/UDP] TCP와 UDP의 특징과 차이

TCP 요청

TCP 연결은 IP가 데이터를 어디로 보낼지 처리한다면, TCP는 패킷이 제대로 전달되었는지 확인을 합니다. 일반적으로 연결형 서비스에서 TCP를 자주 사용하게 되는데, 위 링크에 따르면 3-way handshaking을 통해 연결을 설정하고, 4-way handshaking 을 통해서 연결을 해제한다고 합니다.

연결을 수립하는데 세번의 데이터 요청을 주고받기 때문에 3-way handshaking

연결을 수립하는데 세번의 데이터 요청을 주고받기 때문에 3-way handshaking

아래의 사진은 구글 서버에 직접 GET 요청을 보낸 결과입니다. 구글 서버와 같이 로컬보다 경로가 더 길어진 경우 TCP 핸드셰이킹 과정에서 드는 시간이 더욱 커집니다. 이것이 진짜 API 요청에 영향을 주는 핵심이고, API 요청은 클라이언트의 작업 요청에 따라 분리해야하는 원인입니다.

구글에 GET 요청을 보낸 결과. 확실히 TCP 핸드셰이킹 과정이 엄청 길어졌다.

구글에 GET 요청을 보낸 결과. 확실히 TCP 핸드셰이킹 과정이 엄청 길어졌다.

추후에 글로 정리하려고 하지만, 일단 3-way handshake 와 4-way handshake에 관한 글을 올려봅니다.

[네트워크] TCP/UDP와 3 -Way Handshake & 4 -Way Handshake

결론

저는 결국 API 설계를 할 때에는 어떤 것이든 대상의 요구사항에 따라 구성해야한다고 생각하게 되었습니다. 이게 무슨 뜻이냐면, 클라이언트가 몰라도 되는 일은 굳이 수행하지 않도록 하자는 것이었습니다.

예를 들어, 서론에서 언급한 미션 예시를 보더라도, 특정 컬럼에 담긴 꼭 해야하지 않는 이상 그냥 한번에 보내는게 맞다고 생각이 들었고, 해당 수정 사항을 다음에 반영해보려고 합니다.

This post is licensed under CC BY 4.0 by the author.