티스토리 뷰

Android App Coding

안드로이드 네트워크 Network

IT Knowledge Share 2021. 7. 13. 02:47
반응형

안드로이드에서 Manifest.xml 파일에 아래의 코드를 추가하면 네트워크 작업을 실행할 수 있습니다.

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

 

일반적인 네트워크의 개념은 안드로이드에서도 동일하게 적용됩니다.

 

동기화 및 상호작용의 한계점을 지닌 로컬 데이터베이스 보다는 네트워크 작업을 통해서 DB <-> 서버 <-> 클라이언트(앱/웹)가 서로 통신할 수 있습니다.

 

저희가 앱이나 웹으로 티스토리 홈페이지에 접속한다고 가정하면, 해당 클라이언트(앱/웹)가 인터넷 URL 주소를 티스토리 서버에 요청을 보냅니다. 해당 URL 주소를 담당하는 티스토리 서버는 보낸 요청에 응답을 하게 되고, 티스토리 첫 화면을 클라이언트 쪽에 띄우도록 합니다.

 

서버는 이미지, 텍스트, JSON 파일 등을 포함한 여러 리소스를 클라이언트에 보내고, 클라이언트는 전달받은 여러 리소스를 포맷에 맞게 그리게 됩니다. 이후로 사용자의 인풋을 클라이언트가 받은 후, 서버에게 요청하면, 서버는 다시 응답하는 식으로 네트워킹이 이루어집니다.

 

서버에 요청할 때, 여러 가지 타입이 있는데 자주 사용되는 타입을 다음과 같습니다.

* Request 타입

1. GET (서버의 리소스에서 정보 요청)

2. POST (서버의 리소스를 새로 생성하거나 업데이트)

3. PUT (정보 수정)

4. DELETE (정보 삭제)

반응형

이렇게 요청을 보내면, 서버에는 이에 대한 응답을 하게 되는데, 응답 코드는 다음 링크에서 자세히 확인이 가능합니다.

1로 시작되면 조건부 응답, 2로 시작되면 성공, 4 또는 5는 요청이나 서버 오류입니다.

https://ko.wikipedia.org/wiki/HTTP_%EC%83%81%ED%83%9C_%EC%BD%94%EB%93%9C

 

HTTP 상태 코드 - 위키백과, 우리 모두의 백과사전

아래는 HTTP(하이퍼텍스트 전송 프로토콜) 응답 상태 코드의 목록이다. IANA가 현재 공식 HTTP 상태 코드 레지스트리를 관리하고 있다. 모든 HTTP 응답 코드는 5개의 클래스(분류)로 구분된다. 상태 코

ko.wikipedia.org

 

클라이언트가 서버와 통신하려면, URL로 요청을 하고, 인증 정보(로그인 등)를 보냅니다. 이때 정보는 JSON 형식을 사용해서 데이터로 보내게 됩니다. JSON 형식은 리스트 [ ] 및 객체 { }로 이루어져 있습니다. JSON은 코틀린과 같은 강타입 언어와는 다르게, 내부에서 정수나 문자열만 사용하는 약타입 언어입니다.

* JSON 형식 예시 (user.json)

[ 
   {
   "ID": "TEST1", 
   "PASSWORD": 1234
   },
   {
   "ID": "TEST2", 
   "PASSWORD: 4321"
   } 
 ]

 

이러한 JSON 타입의 데이터를 코틀린이나 자바가 이해할 수 있도록 JSON Parsing 작업이 이루어집니다. 이러한 파싱 작업은 직렬화(Serialization) 과정이 필요합니다. 직렬화 과정은 JSON 언어를 자바나 코틀린이 이해할 수 있도록 서로 매칭해주는 것과 같습니다. 위의 JSON 데이터가 직렬화 과정을 거친다면, 아래의 User 클래스에 JSON 데이터를 매칭시켜서 넣어줄 수 있도록 어떤 틀이나 형태를 만들어 주는 작업이 직렬화입니다.

class User (
 var id: String? = null
 var password: Int? = null
): Serializable

 

네트워크를 실제로 구성하는 코드를 살펴봅니다. 

 

먼저 액티비티 파일을 준비합니다. 우선 전체적인 코드는 다음과 같습니다. NetworkActivity 클래스에서 네트워크 실행 코드를 작성하면, 메인스레드에서 작업이 이루어져 네트워크 에러가 발생하게 됩니다. 메인스레드는 작업을 멈출 수 없기 때문에, 비동기 처리를 해줘야 합니다. 따라서 NetworkTask 크래스를 별도로 생성하고, AsyncTask를 상속받습니다.

class NetworkActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_network)

        NetworkTask(
            recycler_user,
            LayoutInflater.from(this@NetworkActivity)
        ).execute()

    }
}

class NetworkTask(
    val recyclerView: RecyclerView,
    val inflater: LayoutInflater
) : AsyncTask<Any?, Any?, Array<User>>() {
    override fun onPostExecute(result: Array<User>?) {
        
        val adapter = UserAdapter(result!!, inflater)
        recyclerView.adapter = adapter
        super.onPostExecute(result)
    }

    override fun doInBackground(vararg params: Any?): Array<User> {
        val urlString: String = "URL 주소"
        val url: URL = URL(urlString)
        val connection: HttpURLConnection = url.openConnection() as HttpURLConnection

        connection.requestMethod = "GET"
        connection.setRequestProperty("Content-Type", "application/json")

        var buffer = ""
        if (connection.responseCode == HttpURLConnection.HTTP_OK) {
            val reader = BufferedReader(
                InputStreamReader(
                    connection.inputStream,
                    "UTF-8"
                )
            )
            buffer = reader.readLine()
        }
        val data = Gson().fromJson(buffer, Array<User>::class.java)
        return data
    }
}

 

다음은 직렬화 작업에 필요한 액티비티 파일입니다. Serializable 인터페이스를 받게 됩니다.

package com.example.myapplication
import java.io.Serializable

class User (
 var id: String? = null
 var password: Int? = null
): Serializable

 

먼저 NetworkTask 클래스의 doInBackground 메소드 부분을 살펴보면, urlString에 주소값을 넣은 후, url: URL 타입의 실제 url로 바꿔줍니다.

이후로 HTTP 연결 작업을 수행합니다. url.openConnection( ) 때에 HttpURLConnection 타입으로 캐스팅해줘야 합니다.

 

위에서 설명한 요청 타입 중에 GET 방식을 사용한 경우입니다.

GET 방식이기 때문에 요청을 보내는 속성을 설정하는 부분, 즉 setRequestProperty에 "Content-Type", "application/json"으로 적어줍니다. application/json은 GET 방식을 의미하며, HTTP의 헤더에 해당합니다.

(참고로 HTTP 구조는 헤더와 바디로 이루어져 있습니다.)

 

이후에 응답 코드, responseCode가 HTTP_OK (200)라면, 아래 코드처럼 buffer = reader.readLine( )을 수행하도록 합니다. 컴퓨터는 바이트 언어를 사용하기에, 사람이 이해할 수 있는 언어로 inputStream을 읽도록 도와주는 InputStreamReader 부분을 보면, 인자로 connection.inputStream, "UTF-8"을 받습니다. inputStream을 UTF-8 형식으로 읽을 수 있도록 하는데, BufferedReader 메소드 안에서 처리하게 합니다. BufferedReader는 각각의 데이터 개별이 아닌, 데이터를 한뭉텅이 형태로 여러 개를 읽을 수 있도록 도와준다고 생각하시면 됩니다. 이렇게 UTF-8 형식으로 준비된 데이터를 reader에 담은 후, readLine으로 읽어내면, 데이터를 바이너리 형식이 아닌 JSON 타입처럼 출력하게 됩니다.

 

출력된 데이터는 미리 준비된 User 클래스, 즉 직렬화 틀에 데이터가 잘 들어갈 수 있도록 도와줘야 하는데, get(인덱스번호) 처럼 손수 코딩하기 보다는, Gson 라이브러리를 통해서 처리하면 훨씬 작업이 수월합니다. 해당 라이브러리를 이용해 데이터를 User 클래스에 넣어줄 수 있도록 합니다. Gson 라이브러리를 이용하려면, 당연히 build.gradle의 앱단위에 라이브러리를 implementation 시켜야 합니다.

 

Gson().fromJson(buffer, Array<User>::class.java) 부분을 확인하면, fromJson의 인자로 출력하려는 데이터, 데이터를 찍어내는 부분인 직렬화 클래스가 오게 됩니다. 이때, JSON 데이터의 객체 { } 하나만 리턴하는게 아니라, 전체 리스트 [ ]를 가져와야 하므로, User::class.java가 아닌 제너릭 타입으로 Array<User>::class.java가 온 것에 주의하시기 바랍니다.

 

class NetworkTask(
    val recyclerView: RecyclerView,
    val inflater: LayoutInflater
) : AsyncTask<Any?, Any?, Array<User>>() {
    override fun onPostExecute(result: Array<User>?) {
        
        val adapter = PersonAdapter(result!!, inflater)
        recyclerView.adapter = adapter
        super.onPostExecute(result)
    }

    override fun doInBackground(vararg params: Any?): Array<User> {
        val urlString: String = "URL 주소"
        val url: URL = URL(urlString)
        val connection: HttpURLConnection = url.openConnection() as HttpURLConnection

        connection.requestMethod = "GET"
        connection.setRequestProperty("Content-Type", "application/json")

        var buffer = ""
        if (connection.responseCode == HttpURLConnection.HTTP_OK) {
            val reader = BufferedReader(
                InputStreamReader(
                    connection.inputStream,
                    "UTF-8"
                )
            )
            buffer = reader.readLine()
        }
        val data = Gson().fromJson(buffer, Array<User>::class.java)
        return data
    }
}

 

네트워크가 잘 작동되는지 확인하려면, 안드로이드 스튜디오의 Profiler 탭에서 확인이 가능합니다. 프로파일러 탭에서 CPU, 메모리, 네트워크 등을 체크할 수 있으며, 네트워크 부분에 들어가 작동되는 네트워크 부분을 따로 선택해서 프로파일링이 가능합니다. 만약 선택이 되지 않으면, app - profiling - enable advanced profiling 을 체크해주면 됩니다.

https://developer.android.com/studio/profile/android-profiler?hl=ko 

 

Android 프로파일러  |  Android 개발자  |  Android Developers

Android 스튜디오 3.0 이상에서는 Android 프로파일러가 Android 모니터 도구를 대체합니다. Android 프로파일러 도구에서는 앱에서 CPU, 메모리, 네트워크 및 배터리 리소스를 사용하는 방법을 이해하는

developer.android.com

출처: 안드로이드 공식 페이지(https://developer.android.com)

그럼 리사이클러뷰를 사용해 레이아웃에 네트워크를 나타내도록 합니다.

NetworkActivity 클래스의 레이아웃 코드입니다. 레이아웃 상에서 LinearLayoutManager를 사용할 수 있도록 layoutManager 속성을 가져옵니다.

<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=".NetworkActivity">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recycler_user"
        android:layout_width="match_parent"
        app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
        android:layout_height="match_parent"/>

</LinearLayout>

AsyncTask를 상속받고 있는 NetworkTask 클래스에서 doInBackground 작업이 마친 후 메인스레드로 돌아가야 합니다.

따라서 onPostExecute를 오버라이드합니다. result가 Array<User> 타입어야 하므로, AsyncTask의 세 번째 제너릭 리턴 타입도 Array<User>가 되어야 합니다.

class NetworkTask(
    val recyclerView: RecyclerView,
    val inflater: LayoutInflater
) : AsyncTask<Any?, Any?, Array<User>>() {
    override fun onPostExecute(result: Array<User>?) {
        
        val adapter = UserAdapter(result!!, inflater)
        recyclerView.adapter = adapter
        super.onPostExecute(result)
    }

doInBackground의 리턴 타입 또한 Array<User>으로 바꿔줍니다.

doInBackground(vararg params: Any?): Array<User>

이렇게 되면, doInBackground의 리턴된 data가 onPostExecute의 result로 들어가게 됩니다.

 

리사이클러뷰를 사용하려면, 어댑터가 필요하기에, adapter 클래스도 만들어줍니다. 어댑터 부분에 뷰홀더 클래스는 inner 클래스로 만들고, 크기를 리턴해주는 부분인 getItemCount, 뷰를 찾아주는 부분인 onCreateViewHolder, 뷰를 수정해주는 부분인 onBinderViewHolder 메소드를 채워줍니다. onBindViewHolder의 pwd는 스트링 타입으로 캐스팅해서 홀더에 담도록 합니다.

class UserAdapter(
    val userInfo: Array<User>,
    val inflater: LayoutInflater
) : RecyclerView.Adapter<UserAdapter.ViewHolder>() {

    inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val id: TextView
        val pwd: TextView

        init {
            id = itemView.findViewById(R.id.id)
            pwd = itemView.findViewById(R.id.pwd)
        }
    }

    override fun getItemCount(): Int {
        return userInfo.size
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val view = inflater.inflate(R.layout.user_item, parent, false)
        return ViewHolder(view)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        holder.id.setText(userInfo.get(position).id ?: "")
        holder.pwd.setText(userInfo.get(position).pwd.toString())
    }
}

UserAdapter에서 사용할 아이템뷰도 필요하기에, 레이아웃을 추가로 만들어줍니다.

<?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="wrap_content"
    android:orientation="horizontal">


    <TextView
        android:id="@+id/id"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textColor="@color/colorAccent"
        android:textSize="20dp" />

    <TextView
        android:id="@+id/pwd"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textColor="@color/colorAccent"
        android:textSize="20dp" />


</LinearLayout>

이제 onPostExecute 부분에서 어댑터를 만들고, 리사이클러뷰를 채우도록 합니다. NetworkTask 클래스 부분에서 리사이클러뷰와 인플레이터를 받을 수 있도록, 변수를 생성합니다.

class NetworkTask(
    val recyclerView: RecyclerView,
    val inflater: LayoutInflater
) : AsyncTask<Any?, Any?, Array<User>>() {
    override fun onPostExecute(result: Array<User>?) {
        
        val adapter = UserAdapter(result!!, inflater)
        recyclerView.adapter = adapter
        super.onPostExecute(result)
    }

이제 onCreate로 넘어가 태스크를 생성해줍니다. 태스크에 필요한 인자를 넣어주고 execute 메소드로 리사이클러뷰를 통해 뷰가 나올 수 있도록 실행합니다.

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_network)

        NetworkTask(
            recycler_user,
            LayoutInflater.from(this@NetworkActivity)
        ).execute()

}
반응형
댓글