국비교육(22-23)

77일차(4)/Android App(40) : Http POST방식 요청하기(3)

서리/Seori 2023. 1. 30. 00:46

77일차(4)/Android App(40) : Http POST방식 요청하기(3)

 

step17httprequest3

- http 요청 코틀린으로 작성해보기

 

- 안드로이드 스튜디오의 기능 중 java->kotlin 변환기능이 있다.

 

- Util을 가져와서 우클릭- convert to Kotlin 을 해본다!

- 오류나는건 일부 수정하기..

 

Util.kt

package com.example.step17httprequest3

import android.app.Activity
import android.content.Context
import android.os.AsyncTask
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import java.io.*
import java.net.HttpURLConnection
import java.net.URL
import java.net.URLEncoder

object Util {
    //키보드 숨기는 메소드
    fun hideKeyboard(activity: Activity) {
        val iManager = activity.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
        if (activity.currentFocus == null) return
        iManager.hideSoftInputFromWindow(activity.currentFocus!!.windowToken, 0)
    }

    //메소드의 인자로 전달되는 View 객체의 포커스 해제하는 메소드
    fun releaseFocus(view: View) {
        var parent = view.parent
        var group: ViewGroup? = null
        var child: View? = null
        while (parent != null) {
            if (parent is ViewGroup) {
                group = parent
                for (i in 0 until group!!.childCount) {
                    child = group.getChildAt(i)
                    if (child !== view && child.isFocusable) child.requestFocus()
                }
            }
            parent = parent.parent
        }
    }

    /*
        1. 사용할때 RequestListener 인터페이스 Type 을 전달한다.
        2. 결과는 RequestListener 객체에 전달된다.
        3. Map<String,Object>  에서 응답 코드는
            "code" 라는 키값으로 200, 404, 500, -1 중에 하나가 리턴되고
             -1 이 리턴되면 Exception 발생으로 실패이다. onFail() 호출
     */
    fun sendGetRequest(requestId: Int,
                       requestUrl: String?,
                       params: Map<String?, String?>?,
                       listener: RequestListener?) {
        val task = RequestTask()
        task.setRequestId(requestId)
        task.setRequestUrl(requestUrl)
        task.setListener(listener)
        task.execute(params)
    }

    //POST 방식 REQUEST
    fun sendPostRequest(requestId: Int, requestUrl: String?, params: Map<String?, String?>?, listener: RequestListener?) {
        val task = PostRequestTask()
        task.setRequestId(requestId)
        task.setRequestUrl(requestUrl)
        task.setListener(listener)
        task.execute(params)
    }

    interface RequestListener {
        fun onSuccess(requestId: Int, result: Map<String, Any?>?)
        fun onFail(requestId: Int, result: Map<String, Any?>?)
    }

    private class RequestTask : AsyncTask<Map<String?, String?>, Void?, Map<String, Any?>>() {
        private var requestId = 0
        private var requestUrl: String? = null
        private var listener: RequestListener? = null
        fun setListener(listener: RequestListener?) {
            this.listener = listener
        }

        fun setRequestId(requestId: Int) {
            this.requestId = requestId
        }

        fun setRequestUrl(requestUrl: String?) {
            this.requestUrl = requestUrl
        }

        protected override fun doInBackground(vararg params: Map<String?, String?>?): Map<String, Any?>? {
            var requestUrl = requestUrl
            val param = params[0]
            if (param != null) { //요청 파리미터가 존재 한다면
                //서버에 전송할 데이터를 문자열로 구성하기
                val buffer = StringBuffer()
                val keySet = param.keys
                val it = keySet.iterator()
                var isFirst = true
                //반복문 돌면서 map 에 담긴 모든 요소를 전송할수 있도록 구성한다.
                while (it.hasNext()) {
                    val key = it.next()
                    var arg: String? = null
                    //파라미터가 한글일 경우 깨지지 않도록 하기 위해.
                    var encodedValue: String? = null
                    try {
                        encodedValue = URLEncoder.encode(param[key], "utf-8")
                    } catch (e: UnsupportedEncodingException) {
                    }
                    if (isFirst) {
                        arg = "?$key=$encodedValue"
                        isFirst = false
                    } else {
                        arg = "&$key=$encodedValue"
                    }
                    buffer.append(arg)
                }
                val data = buffer.toString()
                requestUrl += data
            }
            //서버가 http 요청에 대해서 응답하는 문자열을 누적할 객체
            val builder = StringBuilder()
            var conn: HttpURLConnection? = null
            var isr: InputStreamReader? = null
            var br: BufferedReader? = null
            //결과값을 담을 Map Type 객체
            val map: MutableMap<String, Any?> = HashMap()
            try {
                //URL 객체 생성
                val url = URL(requestUrl)
                //HttpURLConnection 객체의 참조값 얻어오기
                conn = url.openConnection() as HttpURLConnection
                if (conn != null) { //연결이 되었다면
                    conn.connectTimeout = 20000 //응답을 기다리는 최대 대기 시간
                    conn.requestMethod = "GET" //Default 설정
                    conn.useCaches = false //케쉬 사용 여부
                    //응답 코드를 읽어온다.
                    val responseCode = conn.responseCode
                    //Map 객체에 응답 코드를 담는다.
                    map["code"] = responseCode
                    if (responseCode == 200) { //정상 응답이라면...
                        //서버가 출력하는 문자열을 읽어오기 위한 객체
                        isr = InputStreamReader(conn.inputStream)
                        br = BufferedReader(isr)
                        //반복문 돌면서 읽어오기
                        while (true) {
                            //한줄씩 읽어들인다.
                            val line = br.readLine() ?: break
                            //더이상 읽어올 문자열이 없으면 반복문 탈출
                            //읽어온 문자열 누적 시키기
                            builder.append(line)
                        }
                        //출력받은 문자열 전체 얻어내기
                        val str = builder.toString()
                        //아래 코드는 수행 불가
                        //console.setText(str);
                        //Map 객체에 결과 문자열을 담는다.
                        map["data"] = str
                    }
                }
            } catch (e: Exception) { //예외가 발생하면
                //에러 정보를 담는다.
                map["code"] = -1
                map["data"] = e.message
            } finally {
                try {
                    isr?.close()
                    br?.close()
                    conn?.disconnect()
                } catch (e: Exception) {
                }
            }
            //결과를 담고 있는 Map 객체를 리턴해준다.
            return map
        }

        override fun onPostExecute(map: Map<String, Any?>) {
            super.onPostExecute(map)
            val code = map["code"] as Int
            if (code != -1) { //성공이라면
                listener!!.onSuccess(requestId, map)
            } else { //실패 (예외발생)
                listener!!.onFail(requestId, map)
            }
        }
    }

    class PostRequestTask : AsyncTask<Map<String?, String?>, Void?, Map<String, Any?>>() {
        private var requestId = 0
        private var requestUrl: String? = null
        private var listener: RequestListener? = null
        fun setListener(listener: RequestListener?) {
            this.listener = listener
        }

        fun setRequestId(requestId: Int) {
            this.requestId = requestId
        }

        fun setRequestUrl(requestUrl: String?) {
            this.requestUrl = requestUrl
        }

        protected override fun doInBackground(vararg params: Map<String?, String?>?): Map<String, Any?>? {
            val param = params[0]
            var queryString = ""
            if (param != null) { //요청 파리미터가 존재 한다면
                //서버에 전송할 데이터를 문자열로 구성하기
                val buffer = StringBuffer()
                val keySet = param.keys
                val it = keySet.iterator()
                var isFirst = true
                //반복문 돌면서 map 에 담긴 모든 요소를 전송할수 있도록 구성한다.
                while (it.hasNext()) {
                    val key = it.next()
                    var arg: String? = null
                    if (isFirst) {
                        arg = key + "=" + param[key]
                        isFirst = false
                    } else {
                        arg = "&" + key + "=" + param[key]
                    }
                    buffer.append(arg)
                }
                queryString = buffer.toString()
            }
            //서버가 http 요청에 대해서 응답하는 문자열을 누적할 객체
            val builder = StringBuilder()
            var conn: HttpURLConnection? = null
            var isr: InputStreamReader? = null
            var br: BufferedReader? = null
            var pw: PrintWriter? = null
            //결과값을 담을 Map Type 객체
            val map: MutableMap<String, Any?> = HashMap()
            try {
                //URL 객체 생성
                val url = URL(requestUrl)
                //HttpURLConnection 객체의 참조값 얻어오기
                conn = url.openConnection() as HttpURLConnection
                if (conn != null) { //연결이 되었다면
                    conn.connectTimeout = 20000 //응답을 기다리는 최대 대기 시간
                    conn.doOutput = true
                    conn.requestMethod = "POST"
                    conn.useCaches = false //케쉬 사용 여부
                    //전송하는 데이터에 맞게 값 설정하기
                    conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded") //폼전송과 동일
                    //출력할 스트림 객체 얻어오기
                    val osw = OutputStreamWriter(conn.outputStream)
                    //문자열을 바로 출력하기 위해 osw 객체를 PrintWriter 객체로 감싼다
                    pw = PrintWriter(osw)
                    //서버로 출력하기
                    pw.write(queryString)
                    pw.flush()

                    //응답 코드를 읽어온다.
                    val responseCode = conn.responseCode
                    //Map 객체에 응답 코드를 담는다.
                    map["code"] = responseCode
                    if (responseCode == 200) { //정상 응답이라면...
                        //서버가 출력하는 문자열을 읽어오기 위한 객체
                        isr = InputStreamReader(conn.inputStream)
                        br = BufferedReader(isr)
                        //반복문 돌면서 읽어오기
                        while (true) {
                            //한줄씩 읽어들인다.
                            val line = br.readLine() ?: break
                            //더이상 읽어올 문자열이 없으면 반복문 탈출
                            //읽어온 문자열 누적 시키기
                            builder.append(line)
                        }
                        //출력받은 문자열 전체 얻어내기
                        val str = builder.toString()
                        //아래 코드는 수행 불가
                        //console.setText(str);
                        //Map 객체에 결과 문자열을 담는다.
                        map["data"] = str
                    }
                }
            } catch (e: Exception) { //예외가 발생하면
                //에러 정보를 담는다.
                map["code"] = -1
                map["data"] = e.message
            } finally {
                try {
                    pw?.close()
                    isr?.close()
                    br?.close()
                    conn?.disconnect()
                } catch (e: Exception) {
                }
            }
            //결과를 담고 있는 Map 객체를 리턴해준다.
            return map
        }

        override fun onPostExecute(map: Map<String, Any?>) {
            super.onPostExecute(map)
            val code = map["code"] as Int
            if (code != -1) { //성공이라면
                listener!!.onSuccess(requestId, map)
            } else { //실패 (예외발생)
                listener!!.onFail(requestId, map)
            }
        }
    }
}

 

- 바로 점을 찍어서 사용할 수 있도록 object 형태로 만들어놓았다.

 

- Util.requestListener를 구현

 

- 입력하고 전송버튼을누르면 post 방식으로 전송하고,

 전송 결과를 json 문자열로 받아오는 연습!

 

 


 

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"
    tools:context=".MainActivity"
    android:orientation="vertical">

    <EditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/inputMsg"
        android:hint="서버에 할말 입력" />
    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/sendBtn"
        android:text="전송"/>

</LinearLayout>

 

- AndroidManifest 설정 (인터넷 허용)

 

MainActivity.kt

package com.example.step17httprequest3

import android.os.Bundle
import android.util.Log
import android.widget.Button
import android.widget.EditText
import androidx.appcompat.app.AppCompatActivity
import org.json.JSONObject

class MainActivity : AppCompatActivity() , Util.RequestListener {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        //EditText 객체의 참조값 얻어오기
        val editText=findViewById<EditText>(R.id.inputMsg)
        //Button 객체의 참조값 얻어와서 리스너 등록
        val sendBtn=findViewById<Button>(R.id.sendBtn)
        sendBtn.setOnClickListener {
            //EditText에 입력한 문자열을 읽어와서
            val msg=editText.text.toString()
            Util.sendPostRequest(
                    999,
                    "http://192.168.0.34:9000/boot07/api/send",
                    mapOf("msg" to msg), //"msg" 라는 키값으로 입력한 메세지를 담은 Map
                    this)
        }
    }

    override fun onSuccess(requestId: Int, result: Map<String, Any?>?) {
        if(requestId == 999){
            //웹서버가 응답한 json문자열 { }
            val jsonStr = result?.get("data").toString()
            Log.d("#### json 문자열 ####", jsonStr)
            val obj=JSONObject(jsonStr)
            //key 값을 이용해서 Boolean, String, Int 값을 추출할 수 있다.
            val isSuccess=obj.getBoolean("isSuccess")
            val response=obj.getString("response")
            val num=obj.getInt("num")
        }
    }

    override fun onFail(requestId: Int, result: Map<String, Any?>?) {

    }
}

 

- 변수를 만들 때 어떤 타입을 받을지 제너릭<> 타입 명시!

 

- 이것은 코틀린에서 function 타입이다.

 

var a = { }

var a:()->Unit = { }

- var a = { } 라고 하면 a의 타입은?

- val a:()->Unit = fun(){} 원래는 이거인데, 생략해서 쓰면 val a:()->Unit = {} 로 작성할 수 있다.

 

- 이것은 인자로 아무것도 전달받지 않고, 아무 값도 리턴해주지 않는 함수라는 의미이다.

 

- 괄호 없이 바로 함수를 넣어주는  것이나 마찬가지다.

 

sendBtn.setOnClickListener(fun(view){})
sendBtn.setOnClickListener({})
sendBtn.setOnClickListener {}

- 위의 3줄 코드는 모두 같은 것이다. 요약하여 작성한 것

 

- sendPostRequest 메소드 사용

- 필요한 인자를 잘 보고 값을 넣어주기! 

 

 

- Map에 msg라는 키값으로 메시지를 담아준다.

- java code로 작성하면 위와 같다.

 

- map, 유틸리티 사용하기

- map 에 담긴 것을 알아내서 쿼리문으로 만들어서 post 방식으로 전송하고 있다.

 

- msg=xx 등 queryString 변수에 들어가 있는 문자를 post 방식으로 전송한다.

→ 이 요청을 처리할 Spring Boot의 컨트롤러가 필요하다.

 

- 웹서버를 연동하지 않는 안드로이드 앱은 거의 없다.

- 로그인, 입력한 id,pw를 서버에 보내서 검증할 때 등등... 서버 연동을 쓸 일이 많다.

 


 

- Spring Boot에서 새 패키지, 컨트롤러 생성

 

AndroidController

package com.sy.boot07.api;

import java.util.HashMap;
import java.util.Map;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class AndroidController {
	/*
	 * JSON 문자열 
	 * 1. @ResponseBody 어노테이션
     * 2. Map 혹은 List 혹은 Dto 를 리턴하면 자동으로 JSON 문자열로 변환 되어서 응답된다.
	 */
	@RequestMapping("/api/send")
	@ResponseBody
	public Map<String, Object> send(String msg){
		System.out.println(msg);
		Map<String, Object> map=new HashMap<>();
		map.put("isSuccess", true);
		map.put("response", "hello client!");
		map.put("num", 1);
		return map;
	}
}

 

- 안드로이드에서 /api/send 를 요청하면 json으로 응답하도록

- http://192.168.0.34:9000/boot07/api/send 이런 주소가 된다.

 

http://192.168.0.34:9000/boot07/api/send?msg=hello

- 브라우저 주소 창에 이렇게 입력해주면, 위와 같이 json으로 응답한다.

- 이것을 이제 안드로이드로 해볼 것이다!

 

- map에 담긴 내용이 json 문자열로 자동으로 변환된다.

- @responsebody 를 작성해주면, map에 담긴 내용이 json으로 바뀌어서 응답된다.

 

- 안드로이드에서 이 json 문자열을 활용하는 방식을 익힌다.

 

- 받아오는 json문자열이 null일 가능성이 있으므로(Any? 타입) result에도 ?를 붙여준다.

 

 

- json 문자열이 안드로이드의 콘솔창, STS의 콘솔창으로 잘 들어왔다.

 

- 안드로이드 java / kotlin 환경에서  이 json문자열을 어떻게 다뤄야 하는가?

- 안드로이드 패키지에 JsonObject 라는 클래스가 있다.

- JsonObject 객체를 생성하면서 이 json 문자열을 넣는다.

 

- 읽어올 값의 타입에 맞는 메소드로 내용을 읽어올 수 있다.

 

- 이렇게 json 문자열을 읽어와서 활용할 수 있다.

- 안드로이드 네이티브 UI에 이 값들을 넣어서 동작하게 만들면 된다.

 

- json값은 쉽게 읽어올 수 있다. 어떻게 사용할지만 집중하면 된다.

- 웹서버와 안드로이드가 대화하는 방식을 이해하기!

- http요청 → json으로 받기 → json에 있는 데이터 추출해서 활용하기