백엔드 개발자는 개발을 할 때 항상 클라이언트를 생각해야 한다. 여기서 클라이언트 개발자는 서버, 모바일, 웹이 될 수 있다. 오늘은 필자의 모바일 (iOS, Android) 개발 경험을 바탕으로 어떻게 모바일 클라이언트에게 API를 제공하는것이 좋을지에 대해 이야기하는 시간을 가져보도록 하겠다.
우리가 핸드폰을 사용할 때 핸드폰 안에 수많은 부품과 장치가 있는 것은 알지만 우리가 정확히 핸드폰 안의 어떠한 부품이 존재하지 모르는 것처럼 모바일 개발자도 백엔드 내부 구성을 정확히 알 수 없다. 나또한 모바일 개발을 할 때 서버의 구성 전체를 알 수 없었다. 내가 알 수 있는 사실은 API 스팩과 어떠한 응답을 받을 수 있는지가 전부였다. 이러한 사실을 토대로 모바일 개발을 할 때 API 스팩 (METHOD, URL, Parameter)을 보고 내부에서 어떠한 동작이 일어날지 어떠한 값을 받아올 수 있는지에 대해 잘 예측하는것이 중요했다.
서버 개발을 할 때 이러한 사실을 알고 있기에 반대로 클라이언트에게 API를 제공할 때 내부의 동작을 잘 예측할 수 있게 METHOD, URL을 구조적이고 일관되게 정의하고 직관적인 파라미터명을 사용하는 방향으로 개발하고 있다. 또한 응답을 할때 정의와 일맥상통하는 충분한 응답값을 내리는것에 집중하고 있다. 이러한 원칙을 잘 적용하는 방법에는 RESTful API가 있다. 이러한 사실을 인지하고 더더욱 RESTful API를 설계할 때 사용자가 잘 사용할 수 있을지 원칙이 잘 지켜지는지 응답이 적절하지 다시 한번 확인하며 개발하자.
모바일 개발을 할 당시 Http 통신에 성공, 실패해도 Http Status Code를 2xx으로 내려받았던 적이 있다. 이때 성공과 실패를 Body에서 정의한 상태코드로 구분하며 로직을 작성하였는데, 이 당시 이러한 방식이 회사의 컨벤션이어서 이에 맞춰 개발했으나, 백엔드 개발을 하며 다시 이 부분을 회고했을 때 이러한 방식보다는 Http Status를 명확히 구분하고 Body에서 이에 따른 세부 정보를 내려주는 것이 클라이언트에서 더욱 명확하고 효율적인 대응이 가능하다고 판단된다.
모바일에서는 서버와 요청, 응답에 대응하는 코드는 공통으로 구현한다. 공통 부분에는 1차적으로 Http Status Code를 확인하고 이에 따라 Body를 파싱하는 로직이 작성되어 있다. 모바일에서는 이렇게 확인된 정보를 토대로 서버와의 통신이 성공적으로 이루어 졌을 때, 성공적으로 이루어지지 않을 때의 로직이 작성된다. 성공하면 Response에 대한 정보를 연관된 Component에 전달하고, 실패하면 모달을 띄우거나, 토스트를 띄우는 등 UI / UX 로직이 처리되며 추가로 연관된 Component에 실패 정보를 넘겨준다. 이렇게 넘겨받은 값을 토대로 각 연관된 Component에서는 적절한 처리를 한다.
우리 개발자들은 Http Status Code에 대해서 2xx, 3xx, 4xx, 5xx이 어떠한 의미를 가지는지 알고 있으며, Http Status Code로 여러 상황에 대한 풍부한 표현이 가능하다. 즉, 클라이언트는 Body에서 상태코드를 구분하지 않아도 1차적으로 Http Status Code만 보고 어떠한 대응을 할지 알 수 있으며 공통 부분에서는 이러한 사실을 근거로 Http Status Code에 대한 일반적인 대응이 가능하다. 결과적으로 모바일에서 공통으로 구현한 부분에서는 Http Status Code를 통해 1차 적으로 일반적인 대응을 하도록 하고, 추가로 특정 기능에서 우리가 정의한 상태코드를 통한 로직이 작성되어야 한다고 하면(Ex) USER_PASSWORD_NOT_MATCH ..) Component에서는 넘겨받은 실패 정보를 토대로 대응하는 것이 적합하다. 이렇게 하면 더욱 구조적인 프로그램이 가능하고, 불필요한 클라이언트의 역직렬화를 줄일 수 있어 효율적인 프로그램이 가능하다.
클라이언트에게 응답값을 내려줄 때 기본적으로 성공에 대한 응답값, 실패에 대한 응답값을 구분하고 일관적, 구조적으로 내려주는 것이 핵심이다. 우리 서버에서 공통으로 응답값을 정의한 것을 토대로 클라이언트도 공통으로 어떠한 응답값을 받을지 정의한다. 이러한 관점을 기반으로 서버에서 클라이언트가 공통구조를 잘 설계할 수 있도록 먼저 방향성을 잘 잡아주자.
변수명은 스네이크 케이스보다는 카멜케이스로 내려주자. Java, Kotlin, Swift에서는 변수를 정의할 때 스네이크 케이스로 정의하지 않는다. 카멜케이스로 정의한다. 만약 스네이크 케이스로 내린다고 하면 엄격하게 이러한 컨벤션을 지켜야 하는 경우 파싱할 때 변수를 카멜케이스로 바꾸는 작업을 해야 한다. 실제로 나도 이러한 작업을 했을 때 여간 번거로운 작업이 아니었다. 그럼으로 서버에서는 이러한 사실을 잘 이해하고 카멜케이스로 내려줄수 있도록 하자.
클라이언트는 서버에서 내려주는 응답 DTO를 정의할 때 JSON Parsing Exception을 피하고자 서버에서 정의한 JSON Filed Name을 그대로 DTO 변수명으로 사용하는 경우는 흔한 일이다. 즉, 서버에서 변수명을 대충 정해주거나, 알맞지 않게 정해줘서 내려주면 클라이언트도 이러한 변수명을 그대로 사용할 여지가 충분하다. 이러한 사실을 이해하고 서버에서 클라이언트에 전달할 변수명을 신경 써서 잘 전달할 수 있도록 하자.
서버에서 배열의 값을 내릴 때는 null로 내리지 않고 빈배열로 내린다. 그러나 회사의 컨벤션에 따라 배열 이외의 변수 타입에 대해서는 null을 내리기도 한다.
요즘 클라이언트 언어는 null safety 한 언어가 많다. (iOS - Swift, Android - Kotlin) null safety 한 언어에는 nullable 변수가 존재하는데, 이 nullable 변수에 접근할 때는 null safety 접근 연산자를 사용해야 한다. (?.) nullable 한 변수와 null safety 연산자를 사용하면 NullpointException을 발생시키지 않아 Application의 안정성을 매우 높여주지만 사용을 하는 입장에서는 일반 연산자보다 null safety 연산자가 추가로 null 처리를 매번 해줘야 하므로 여간 번거로운 일이 아니다.
그런데 만약 배열의 값을 내릴 때 null로 내리지 않고 빈배열로 내리는것처럼 다른 타입도 값을 내릴 때 null로 내리지 않는다고, 사전에 정의하면 null safety 한 언어를 사용하는 클라이언트에서 서버와 통신을 통한 값을 받을 때 타입을 nullable로 정의하지 않아도 된다. 이는 개발의 생산성을 높일 수 있다는 장점이 있다. 그러나 nullable로 선언하지 않는 타입에 null 값이 들어오면 JsonParsingExcepion이 발생한다는 단점이 있다. 이 부분은 이러한 사실을 이해하고 안정성과 편의성 둘을 고려하여 각 회사의 룰에 맞는 전략을 잘 따른는 것이 중요하다고 판단된다.
모바일에는 여러 LifeCycle이 존재한다. Application의 LifeCycle도 존재하고, 각 화면에 따른 LifeCycle도 존재한다. 앱을 다시 켰을 때의 시점, 앱을 껐을 때의 시점, 화면의 뷰가 모두 로딩이 완료되었을 때 시점, 화면이 다시 보여졌을 때 시점 등.. 다양한 시점을 LifeCycle이 정의한다. 그리고 이러한 LifeCycle은 사용자의 특정 행위와 더불어 서버의 API를 호출하는 Trigger가 될 수 있다. 이러한 사실을 잘 이해하고 RESTful API를 설계할 때 언제 이 API를 사용하는가에 대한 시점을 생각하는 것은 매우 중요하다.
예를 들어보자. 사용자가 앱을 켜면 모바일에서는 버전 체크 API를 요청한다. 버전 체크가 완료되면 모바일에서는 앱을 사용하기 위해 초기에 반드시 필요한 사용자 정보, Static 한 데이터를 전달받기 위해 서버에 Init API를 요청한다. 모바일에서는 이렇게 요청한 API를 토대로 사용자의 인증정보를 확인하여 어떠한 화면으로 이동할지 결정한다. 이 모든 것은 사용자의 행위로 이루어 지는 것이 아니라 LifeCycle에 의해 이루어진다. 즉 서버 개발자는 각 시점을 잘 이해하고 각 시점에 필요한 적합한 API를 요청하도록 설계하는 것이 중요하다.
모바일은 Stack 기반으로 화면이 관리가 된다. 즉, 화면이 전환되면 각 화면이 FIFO으로 화면이 쌓이게 된다.
어떤 앱에서 디테일 페이지에 들어간다고 하면 이 전에 리스트를 먼저 표현했을 것이고 리스트중 특정 아이템의 ID를 통해 디테일 페이지에 진입했을 것이다. 그런데 만약 디테일 페이지에서 어떠한 행위로 이 아이템의 데이터가 서버에서 변경되었다고 하자. 그러면 이전의 리스트에서도 이 상태를 업데이트해야 할 것이다. 방법은 2가지가 있다. 디테일 페이지에서 아이템의 값을 변경하는 API를 요청하고 요청이 완료되었을 때 이전의 리스트를 받아오는 API를 요청할 수 있고, 처음에 API를 요청했을 때 받아온 응답값을 통해 리스트를 업데이트하는 방법이 있다. 효율성을 고려했을 때 두 번째 방식이 API 요청을 최소화하는 방식이기 트래픽의 관점으로 해석하면 두번째 방식이 효율적이겠다. 이러한 사실을 이해하고 서버에서는 클라이언트가 충분히 업데이트할 수 있도록 데이터를 잘 내려주는 것이 중요하겠다.
(단, 첫 번째 방식으로 구현하는것은 코드를 작성하는 데 있어 상대적으로 더 효율적이다. 또한 다시 한번 서버의 데이터를 받아오는 방식이므로 서버와 클라이언트의 정합성을 확실히 유지할 수 있다는 장점이 있다. 그리고 리스트를 업데이트하여 새로운 정보를 표현할 수 있다는 장점이 있으므로 이러한 방식은 기획과 각 비지니스 상황에 따라 적절하게 고려하는것도 중요하다. 또한 각 화면에서 표현된 데이터들의 너무 상이한 도메인에 대한 데이터들이라고 한다면 여러 API를 호출하는 것도 구조적으로 고려해야 한다.)
모바일에서는 WebView를 사용하지 않는다면, 쿠키와 세션 기반으로 서버에 인증할 수 없다. 일반적인 Native 앱이라고 한다면 JWT 기반으로 서버에 인증을 하는 것이 일반적이다. 로그인한 사용자는 JWT를 내부 저장소에 저장하고 이 토큰이 존재한다고 하면 로그인을 사전에 실시한 것으로 간주하고 서버에 만료되지 않은 사용자인지 요청을 하여 인증을 진행하고, 만약 만료가되었다면 401 상태코드를 확인하여 로그아웃 (JWT를 내부 저장소에서 없애고, 로그인 화면으로 전환)을 진행한다. 유효하다면 이 유효한 토큰을 기반으로 서버와 여러 통신을 진행한다.