구마찌의 이진수 여행기
[Android]JSON파일 파싱하기 본문
1. JSON ?
json이라는 확장자명이 붙어있는 파일 종류이다. Key, Value로 구성되어 있으며 Map형태와 비슷한 모양을 가지고 있다.
2. 목표 및 구성
> 클래스 구성
나는 서버에 있는 '페어링 된 기기 정보'를 가져오기 와서 화면에 해당 기기의 정보들을 보여줄 것이다.
MainActivity에서 버튼을 클릭하면 다음 화면인 SubActivity에 파싱된 데이터들을 ListView로 보여줄 계획이다.
하지만, "웹"서버에 있는 데이터를 긁어오는 작업이 필요한데, 이를 위해 웹과 소통이 가능하도록 해주는
NetworkConnection java파일을 만들었다. 왜 굳이 만들어야 하는지는 뒤에서 설명하도록 하겠다. ( 4번 )
3. 소스 ( EmptyActivity 이용 )
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="btnServe"
android:id="@+id/btnserve"/>
</LinearLayout>
activity_sub.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".SubActivity">
<ListView
android:id="@+id/lvDevice"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
MainActivity.java
public class MainActivity extends AppCompatActivity{
Button btnserve;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
btnserve = findViewById(R.id.btnserve);
}
public void btnServe(View v) {
if(v.getId() == R.id.btnserve) {
Intent it = new Intent(MainActivity.this, SubActivity.class);
startActivity(it);
finish();
} else {
}
}
}
> activity_main.xml에서의 버튼을 누르면 SubActivity로 이동
NetworkConnection.java
public class NetworkConnetion extends AsyncTask<Void, Void, String>{
// 1번 void : 내가 보낼 값
// 2번 void : 내가 받는 값
// 3번 void : 내가 보냈다가 다시 받는 값*/
String stBuffer;
@Override
protected void onPreExecute() {
super.onPreExecute();
}
@Override
protected String doInBackground(Void... voids) {
try {
URL url = new URL("가져올 해당 URL");
HttpURLConnection huc = (HttpURLConnection) url.openConnection();
huc.setRequestMethod("GET");
huc.setRequestProperty("Key1", "Value1");
huc.setRequestProperty("Key2", "Value2");
huc.setRequestProperty("Key3", "Value3");// 해당 URL에서 000의 정보를 가져오기 위해서 000의 Value값을 넣어준다.
if (huc.getResponseCode() == HttpURLConnection.HTTP_OK) {
BufferedReader in = new BufferedReader(new InputStreamReader(huc.getInputStream()));
String line;
StringBuffer buffer = new StringBuffer();
//Log.d("INPUT: ", in.readLine());
while ((line = in.readLine()) != null) {
buffer.append(line + ", ");
}
stBuffer = buffer.toString();
in.close();
}
} catch (IOException e) {
e.printStackTrace();
} catch (NetworkOnMainThreadException e) {
e.printStackTrace();
} catch(Exception e) {
e.printStackTrace();
}
return stBuffer;
}
@Override
protected void onPostExecute(String aVoid) {
super.onPostExecute(aVoid);
}
}
* 메소드 순서 : onPreExcute() -> doInBackground() -> onPostExcute()
-> onPreExcute()와 onPostExcute()는 백그라운드에서 돌아가야할 내용들이 있는 doInBackground()의 전, 후에
있어야 하는 이벤트에 대해 다루는 메소드다.
* setRequestMethod() : URL에 request를 보낼 때 어떤 형식으로 response 하고싶은지 정하는 메소드. Default는 Get형태.
* setRequestProperty("Key", "Value") : 직역하면 Request성질이다. key에 해당하는 value의 성질, 즉 key와 value에
부합하는 데이터만 가져오기 위해서 setting해주는 메소드.
> URL로 부터 받은 데이터를 inputStream으로 받는다 -> inputStream의 형태로 읽는다 -> BufferedReader으로 input된
결과를 읽어들인다 -> BufferedReader을 한줄씩(readLine()) 읽어서 line이라는 문자열 변수에 넣는다 -> line이 null값이
아니면(line에 값이 있으면) StringBuffer에 line을 추가한다. -> StringBuffer로 받은 데이터를 toString()으로 바꾼 다음에
stBuffer(String)에 넣어준다. (굳이 String을 두개로 둔 이유는 한번에 담아서 넣기 위함) -> Stream close. -> stBuffer
return
SubActivity.java
public class SubActivity extends AppCompatActivity {
ListView lvDevice;
ArrayAdapter<Object> adapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_sub);
lvDevice = findViewById(R.id.lvDevice);
try {
String value = new NetworkConnetion().execute().get();
Log.d("Value: ", value);
JSONObject jsonObject = new JSONObject(value);
HashMap<String, Object> hsh = new HashMap<String, Object>();
ArrayList<Object> _res = new ArrayList<>();
JSONArray jsonArr = jsonObject.getJSONArray("device_list");
JSONObject jsonobj = jsonObject.getJSONObject("result");
switch (jsonobj.get("code").toString()) {
case "20000":
for(int i=0;i<jsonArr.length();i++) {
JSONObject objResult = jsonArr.getJSONObject(i);
hsh.put("model", objResult.get("model"));
hsh.put("device_nm", objResult.get("device_nm"));
hsh.put("scene_id", objResult.get("scene_id"));
hsh.put("device_id", objResult.get("device_id"));
_res.add(hsh);
}
/*
Log.d("HSH: ", hsh.toString());
Log.d("LIST: ", _res.toString());
확인 완료 !
*/
adapter = new ArrayAdapter<Object>(this, R.layout.listview_item,R.id.tvList1, _res);
lvDevice.setAdapter(adapter);
break;
default:
break;
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
> NetworkConnection.java에서 주된 스레드인 doInBackground()에서 return한 stBuffer을 String value에 담는다.
> json파일 형태
{ }로 이루어진 부분은 Object, 하나의 개체가 되고 []로 이루어진 부분은 Array, 배열이 된다.
따라서 전체적으로 큰 Object로 감싸져 있는 상태. < jsonObject
device_list라는 이름의 Array들, < jsonArr
result라는 이름의 Object로 구성되어 있다. < jsonobj
result의 구성 요소인 code가 20000이 되었을 때 실행해야 한다. -> 연결이 성공됨을 알림.
model ~ device_id는 device_list Array의 구성요소가 된다. device_list에 있는 요소들을 다 가져와야 하기 때문에
배열의 길이만큼(model~device) 반복을 시켜주고, 배열 안에서도 하나하나 요소들을 가져와야 하기 때문에
jsonArr안에서도 Object를 받아와야 한다.
* 말로 설명하기가 너무 어렵다....
HashMap의 형태(Key, Value)로 받아와서 HashMap에 붙여준다.
* Adapter ?
listView와 Data를 이어주기 위해서는 Adapter의 역할이 중요하다.
Adapter는 단어 그대로의 뜻을 지니고 있다. 어떤 객체들 혹은 개체들을 매개시켜주는 역할을 한다.
나는 ListView에 기기리스트를 보여주기 위해서 ArrayList<>를 사용했다.
여기서 Adapter는 listview_item.xml에 있는 tvList1(TextView)의 형식으로 '어떤 공간'에 ArrayList<>를 갖다 붙여주는 역할이다.
'어떤 공간'은 list를 보여줄 곳, 즉 SubActivity에 있는 lvDevice다.
listview_item.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/tvList1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="TextView" />
</LinearLayout>
</LinearLayout>
4. Network연결을 왜 SubActivity에서 할 수 없지?
안드로이드는 스레드 형태로 돌아간다. onCreate를 메인 스레드라고 생각하면 된다.
안드로이드 버전업이 되면서 안드로이드 기기에서는 메인 스레드 안에서 네트워크 연결을 하게되면
앱이 멈춰버리는 현상이 발생하게 되었다. (아마 비동기/동기 문제 때문 아닐까 싶다.)
하나의 클래스에서 다른 메소드(onCreate이외의 메소드)를 만들고 호출하는 형태도 앱이 작동이 안되더라.
이를 해결하기 위해서 두가지 방법이 있는데 첫 번째는 스레드를 생성해서 돌리는 것, 두 번째는 새로운 클래스를 만드는
것이다. AsyncTask, 비동기 클래스를 상속받은 클래스.
네트워크 연결과 본래 해야하는 동작을 동시에 돌리면 각자의 통행로가 부딪혀서 뻗어버린다고 생각할 수 있다.
이것을 방지하기 위해 스레드를 사용한다. 스레드는 백그라운드에서 돌아간다. 비동기로 진행을 할 수 있다는 말이다.
( 스레드 == 비동기는 아니다!) 또한 AsyncTask, 비동기로 작동하는 기능을 가진 클래스를 상속받으면 백그라운드에서
자유롭게 동작가능하다. (스레드를 사용할 줄 모르기 때문에 클래스 상속받아서 새로운 java파일을 만들었다)
5. 결과화면
6. 정리
엄청 엄청 어려웠다. json파일의 형태를 이해하는 데에도 많은 시간이 걸렸고, HTTPConnection을 이해하는 데에도 많은
어려움을 겪었다. 내가 느끼기에, 안드로이드는 진입장벽이 높다고 생각한다. API가 넘쳐나고, 스레드와 클래스를
자유롭게 다루지 못하고, 필요한 메소드들을 그때그때 꺼내지 못하면(혹은 찾지 못하면) 엄청 힘들다. 구글에서 제공하는
Firebase(서버와 DB둘다 지원)를 연동하는 방법과 일반 웹서버를 이용하는 방법이 상이해서 이 부분에서도 충격적이였다.