본문 바로가기

Android

#Volley 로 안드로이드 앱에 #API #JSON 연결하기

반응형

API는 무엇일까요

우리가 만드는 애플리케이션이 기존에 존재하는 외부 서비스에서 제공하는 기능을 사용하기 위해서는 API, Application Programming Interface를 사용하여야 합니다. API는 서비스가 다른 서비스를 위해 외부로 노출하는 포장의 일종이며, 실제 데이터 처리나 일련의 과정은 백엔드, 서버에서 이루어집니다. 반환되는 데이터는 일반적으로 JSON, XML 형태로 제공됩니다. API 개념은 안드로이드 앱에만 적용되는 것이 아닌, 네트워크를 통해 이루어지는 프로그램과 백엔드 서버의 통신을 중개하는 Wrapper 전체를 말합니다.

Application <-> API <-> Back-end Server

API 사용을 위한 준비

외부 서버와 통신하기 위해서는 HTTP 프로토콜에 대해서 알아야 하고, 네트워크 구조에 대해서 알아야 합니다. 문제는 이 과정이 너무 복잡하다는 것입니다. 물론 이 과정을 모두 정확히 이해하고 있다면 훨씬 더 좋겠지만, 일단은 이러한 기능이 이미 만들어진 Android SDK를 이용하여 API를 사용하는 것부터 해보려고 합니다. 네트워크 통신을 도와주는 수많은 라이브러리가 있지만, 여기서는 구글에서 제공하는 Volley 라이브러리를 사용해보도록 하겠습니다.

먼저, 새로운 프로젝트를 Empty Activity로 시작합니다. Volley를 사용하기 위해서는 Gradle 스크립트에 추가하는 과정이 필요하겠지요. 안드로이드 개발자 페이지에서 다음 게시글을 참고하여 모듈의 build.gradle에 의존성을 주입하여 봅시다. Gradle 스크립트를 변경한 뒤에는 이를 반영하기 위해 Sync Now를 잊지 말아주시고요.

developer.android.com/training/volley?hl=en

 

Volley 개요  |  Android 개발자  |  Android Developers

Volley는 Android 앱의 네트워킹을 더 쉽고, 무엇보다도 더 빠르게 하는 HTTP 라이브러리입니다. Volley는 GitHub에서 사용할 수 있습니다. Volley를 사용하면 다음과 같은 이점이 있습니다. 네트워크 요청

developer.android.com

dependencies {
    ...
    implementation 'com.android.volley:volley:1.1.1'
}

 

혹은 File > Project Structure의 Dependencies 탭에서 Library Dependency를 통해 의존성을 주입할 수도 있습니다. 패키지 이름을 입력하고 Search 버튼을 눌러 검색할 수 있으며, version 또한 적절하게 설정해줍니다. 이런 방식을 통해 의존성을 주입하게 되면 자동으로 프로젝트를 Sync 하게 됩니다.

 

jsonplaceholder.typicode.com/todos에서 제공하는 데이터를 사용하여 데이터 fetch, parsing을 테스트 해보겠습니다.

[
  {
    "userId": 1,
    "id": 1,
    "title": "delectus aut autem",
    "completed": false
  },
  {
    "userId": 1,
    "id": 2,
    "title": "quis ut nam facilis et officia qui",
    "completed": false
  },
  
  ...
  
  {
    "userId": 10,
    "id": 200,
    "title": "ipsam aperiam voluptates qui",
    "completed": false
  }
]

 

Volley Request 생성하기

Volley를 이용하여 Network Request를 생성하기 위해서는 먼저 RequestQueue가 필요합니다. RequestQueue는 네트워크 작업을 하는 쓰레드를 관리하고, 캐시에 데이터를 읽고 쓰며, 서버 응답을 파싱하는 역할을 합니다. 각 Request가 raw Response를 파싱하고 Volley는 파싱된 응답이 메인 쓰레드에 전달되었는지 확인합니다. RequestQueue는 프로그램을 통틀어 하나만 존재하는, Singleton 패턴으로 형성되며, 이를 통해 다양한 액티비티에서 Request를 요청할 때에도 순차적으로 처리할 수 있으며, 캐시를 활용하여 중복된 요청을 관리할 수 있습니다.

다음은 간단히 생성한 Volley Request입니다. 응답을 받아올 수 있다면 해당 응답을 앞에서부터 500자 잘라내어 화면에 표시하며, 에러가 발생할 경우 해당 에러 메시지를 화면에 표시하는 액티비티입니다.

package com.example.api_json;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.util.Log;
import android.widget.TextView;

import com.android.volley.Request;
import com.android.volley.RequestQueue;
import com.android.volley.toolbox.StringRequest;
import com.android.volley.toolbox.Volley;

public class MainActivity extends AppCompatActivity {

    private TextView textView;
    String url = "https://jsonplaceholder.typicode.com/todos";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        textView = findViewById(R.id.text);

        RequestQueue queue = Volley.newRequestQueue(this);
        StringRequest stringRequest = new StringRequest(Request.Method.GET, url, response -> {
            textView.setText("response: " + response.substring(0, 500));
        }, error -> {
            textView.setText("error: " + error.getMessage());
        });

        queue.add(stringRequest);
    }
}

 

하지만 코드를 실행하면 다음과 같은 에러 메시지가 출력되며 앱이 강제 종료되게 됩니다.

E/Volley: [1315] NetworkDispatcher.processRequest: Unhandled exception java.lang.SecurityException: Permission denied (missing INTERNET permission?)
    java.lang.SecurityException: Permission denied (missing INTERNET permission?)
        at java.net.Inet6AddressImpl.lookupHostByName(Inet6AddressImpl.java:150)
        at java.net.Inet6AddressImpl.lookupAllHostAddr(Inet6AddressImpl.java:103)
        at java.net.InetAddress.getAllByName(InetAddress.java:1152)
        at com.android.okhttp.Dns$1.lookup(Dns.java:41)
        at com.android.okhttp.internal.http.RouteSelector.resetNextInetSocketAddress(RouteSelector.java:178)
        at com.android.okhttp.internal.http.RouteSelector.nextProxy(RouteSelector.java:144)
        at com.android.okhttp.internal.http.RouteSelector.next(RouteSelector.java:86)
        at com.android.okhttp.internal.http.StreamAllocation.findConnection(StreamAllocation.java:176)
        at com.android.okhttp.internal.http.StreamAllocation.findHealthyConnection(StreamAllocation.java:128)
        at com.android.okhttp.internal.http.StreamAllocation.newStream(StreamAllocation.java:97)
        at com.android.okhttp.internal.http.HttpEngine.connect(HttpEngine.java:289)
        at com.android.okhttp.internal.http.HttpEngine.sendRequest(HttpEngine.java:232)
        at com.android.okhttp.internal.huc.HttpURLConnectionImpl.execute(HttpURLConnectionImpl.java:465)
        at com.android.okhttp.internal.huc.HttpURLConnectionImpl.getResponse(HttpURLConnectionImpl.java:411)
        at com.android.okhttp.internal.huc.HttpURLConnectionImpl.getResponseCode(HttpURLConnectionImpl.java:542)
        at com.android.okhttp.internal.huc.DelegatingHttpsURLConnection.getResponseCode(DelegatingHttpsURLConnection.java:106)
        at com.android.okhttp.internal.huc.HttpsURLConnectionImpl.getResponseCode(HttpsURLConnectionImpl.java:30)
        at com.android.volley.toolbox.HurlStack.executeRequest(HurlStack.java:96)
        at com.android.volley.toolbox.BasicNetwork.performRequest(BasicNetwork.java:123)
        at com.android.volley.NetworkDispatcher.processRequest(NetworkDispatcher.java:131)
        at com.android.volley.NetworkDispatcher.processRequest(NetworkDispatcher.java:111)
        at com.android.volley.NetworkDispatcher.run(NetworkDispatcher.java:90)
     Caused by: android.system.GaiException: android_getaddrinfo failed: EAI_NODATA (No address associated with hostname)
        at libcore.io.Linux.android_getaddrinfo(Native Method)
        at libcore.io.ForwardingOs.android_getaddrinfo(ForwardingOs.java:74)
        at libcore.io.BlockGuardOs.android_getaddrinfo(BlockGuardOs.java:200)
        at libcore.io.ForwardingOs.android_getaddrinfo(ForwardingOs.java:74)
        at java.net.Inet6AddressImpl.lookupHostByName(Inet6AddressImpl.java:135)
        at java.net.Inet6AddressImpl.lookupAllHostAddr(Inet6AddressImpl.java:103) 
        at java.net.InetAddress.getAllByName(InetAddress.java:1152) 
        at com.android.okhttp.Dns$1.lookup(Dns.java:41) 
        at com.android.okhttp.internal.http.RouteSelector.resetNextInetSocketAddress(RouteSelector.java:178) 
        at com.android.okhttp.internal.http.RouteSelector.nextProxy(RouteSelector.java:144) 
        at com.android.okhttp.internal.http.RouteSelector.next(RouteSelector.java:86) 
        at com.android.okhttp.internal.http.StreamAllocation.findConnection(StreamAllocation.java:176) 
        at com.android.okhttp.internal.http.StreamAllocation.findHealthyConnection(StreamAllocation.java:128) 
        at com.android.okhttp.internal.http.StreamAllocation.newStream(StreamAllocation.java:97) 
        at com.android.okhttp.internal.http.HttpEngine.connect(HttpEngine.java:289) 
        at com.android.okhttp.internal.http.HttpEngine.sendRequest(HttpEngine.java:232) 
        at com.android.okhttp.internal.huc.HttpURLConnectionImpl.execute(HttpURLConnectionImpl.java:465) 
        at com.android.okhttp.internal.huc.HttpURLConnectionImpl.getResponse(HttpURLConnectionImpl.java:411) 
        at com.android.okhttp.internal.huc.HttpURLConnectionImpl.getResponseCode(HttpURLConnectionImpl.java:542) 
        at com.android.okhttp.internal.huc.DelegatingHttpsURLConnection.getResponseCode(DelegatingHttpsURLConnection.java:106) 
        at com.android.okhttp.internal.huc.HttpsURLConnectionImpl.getResponseCode(HttpsURLConnectionImpl.java:30) 
        at com.android.volley.toolbox.HurlStack.executeRequest(HurlStack.java:96) 
        at com.android.volley.toolbox.BasicNetwork.performRequest(BasicNetwork.java:123) 
        at com.android.volley.NetworkDispatcher.processRequest(NetworkDispatcher.java:131) 
        at com.android.volley.NetworkDispatcher.processRequest(NetworkDispatcher.java:111) 
        at com.android.volley.NetworkDispatcher.run(NetworkDispatcher.java:90) 
     Caused by: android.system.ErrnoException: android_getaddrinfo failed: EPERM (Operation not permitted)
        at libcore.io.Linux.android_getaddrinfo(Native Method) 
        at libcore.io.ForwardingOs.android_getaddrinfo(ForwardingOs.java:74) 
        at libcore.io.BlockGuardOs.android_getaddrinfo(BlockGuardOs.java:200) 
        at libcore.io.ForwardingOs.android_getaddrinfo(ForwardingOs.java:74) 
        at java.net.Inet6AddressImpl.lookupHostByName(Inet6AddressImpl.java:135) 
        at java.net.Inet6AddressImpl.lookupAllHostAddr(Inet6AddressImpl.java:103) 
        at java.net.InetAddress.getAllByName(InetAddress.java:1152) 
        at com.android.okhttp.Dns$1.lookup(Dns.java:41) 
        at com.android.okhttp.internal.http.RouteSelector.resetNextInetSocketAddress(RouteSelector.java:178) 
        at com.android.okhttp.internal.http.RouteSelector.nextProxy(RouteSelector.java:144) 
        at com.android.okhttp.internal.http.RouteSelector.next(RouteSelector.java:86) 
        at com.android.okhttp.internal.http.StreamAllocation.findConnection(StreamAllocation.java:176) 
        at com.android.okhttp.internal.http.StreamAllocation.findHealthyConnection(StreamAllocation.java:128) 
        at com.android.okhttp.internal.http.StreamAllocation.newStream(StreamAllocation.java:97) 
        at com.android.okhttp.internal.http.HttpEngine.connect(HttpEngine.java:289) 
        at com.android.okhttp.internal.http.HttpEngine.sendRequest(HttpEngine.java:232) 
        at com.android.okhttp.internal.huc.HttpURLConnectionImpl.execute(HttpURLConnectionImpl.java:465) 
        at com.android.okhttp.internal.huc.HttpURLConnectionImpl.getResponse(HttpURLConnectionImpl.java:411) 
        at com.android.okhttp.internal.huc.HttpURLConnectionImpl.getResponseCode(HttpURLConnectionImpl.java:542) 
        at com.android.okhttp.internal.huc.DelegatingHttpsURLConnection.getResponseCode(DelegatingHttpsURLConnection.java:106) 
        at com.android.okhttp.internal.huc.HttpsURLConnectionImpl.getResponseCode(HttpsURLConnectionImpl.java:30) 
        at com.android.volley.toolbox.HurlStack.executeRequest(HurlStack.java:96) 
        at com.android.volley.toolbox.BasicNetwork.performRequest(BasicNetwork.java:123) 
        at com.android.volley.NetworkDispatcher.processRequest(NetworkDispatcher.java:131) 
        at com.android.volley.NetworkDispatcher.processRequest(NetworkDispatcher.java:111) 
        at com.android.volley.NetworkDispatcher.run(NetworkDispatcher.java:90) 

 

맨 첫 줄에서 보여지듯, 안드로이드 앱이 인터넷에 접근하기 위해서는 권한이 필요합니다. 해당 권한은 AndroidManifest.xml 파일에 다음 구문을 추가하여 획득할 수 있습니다. Internet 권한은 normal permission의 일종으로, 설치 시에 권한을 획득하며 런타임에 권한을 요청할 필요가 없습니다.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.api_json">

    <uses-permission android:name="android.permission.INTERNET"/>
    
    ...
    
</manifest>

 

권한을 추가한 뒤 곧바로 앱을 실행하면 또 다음과 같이 java.net.SocketException: socket failed: EPERM (Operation not permitted) 오류가 뜹니다. 앞서 normal permission은 어플리케이션 설치 시에 권한을 획득한다고 설명한 바 있습니다. 우리의 어플리케이션이 인터넷 접속 권한을 획득하기 위해선 다시 설치 과정을 경험할 필요가 있습니다. 따라서 해당 오류는 어플리케이션 삭제 후 재설치 시 해결됩니다.

 

위에서 요청한 데이터는 String 형식으로 응답하여 파싱을 하는 데에 애로사항이 있습니다. Volley에서는 JSON 형태의 파싱을 위한 Request가 두 가지 형태(JsonObjectRequest, JsonArrayRequest) 존재합니다. 위 StringRequest를 JsonArrayRequest로 바꿔 각 JsonObject의 title만 파싱해봅시다.

package com.example.api_json;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.util.Log;
import android.widget.TextView;

import com.android.volley.Request;
import com.android.volley.RequestQueue;
import com.android.volley.Response;
import com.android.volley.toolbox.JsonArrayRequest;
import com.android.volley.toolbox.StringRequest;
import com.android.volley.toolbox.Volley;

import org.json.JSONArray;
import org.json.JSONObject;

public class MainActivity extends AppCompatActivity {

    private TextView textView;
    String url = "https://jsonplaceholder.typicode.com/todos";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        textView = findViewById(R.id.text);

        RequestQueue queue = Volley.newRequestQueue(this);
        JsonArrayRequest jsonArrayRequest = new JsonArrayRequest(Request.Method.GET, url, null, response -> {
            try {
                StringBuilder text = new StringBuilder("response:");
                for (int i = 0; i < response.length(); i++) {
                    text.append("\n").append(response.getJSONObject(i).getString("title"));
                }
                textView.setText(text.toString());
            } catch (Exception error) {
                textView.setText("error: " + error.getMessage());
            }
        }, error -> {
            textView.setText("error: " + error.getMessage());
        });

        queue.add(jsonArrayRequest);
    }
}

 

StringRequest와 대체로 비슷하지만, null로 채워진 필드가 하나 추가되었습니다. 해당 필드는 jsonRequest 필드로 JSONArray 타입의 파라미터를 요구합니다. 해당 필드는 POST 시 전달될 데이터를 요구하는 필드이므로 GET을 통한 요청 시에는 null 값을 채워주는 것으로 충분합니다.

developer.android.com/training/volley/simple

 

간단한 요청 보내기  |  Android 개발자  |  Android Developers

고급 단계에서는 RequestQueue를 만들고 Request 객체를 전달하여 Volley를 사용합니다. RequestQueue는 네트워크 작업 실행, 캐시 읽고 쓰기 및 응답 파싱을 위해 작업자 스레드를 관리합니다. 요청은 원시

developer.android.com

developer.android.com/training/basics/network-ops/connecting

 

네트워크에 연결  |  Android 개발자  |  Android Developers

애플리케이션에서 네트워크 작업을 하려면 매니페스트에 다음과 같은 권한을 포함해야 합니다. 안전한 네트워크 통신 설계 앱에 네트워킹 기능을 추가하기 전에 앱의 데이터와 정보가 네트워

developer.android.com

 
 
 
 
반응형