서버 : 데이터나 리소스를 제공하는 시스템. 사용자의 요청을 기다리고, 요청이 들어오면 그에 맞는 응답을 전송한다.
클라이언트 : 사용자를 대표하여 서버에 정보나 서비스를 요청하는 시스템. 웹 브라우저, 모바일 앱, 데스크톱 앱 등... 형태가 있다.
이렇게 보면 무슨 소리인지 모를 것 같아서... 예시를 들자면
-> 서버는 식당으로 따지면 점원의 역할과 같다.
-> 클라이언트는 음식을 주문하는 손님.... 과 같다(?)
-> 그 둘 사이의 소통을 하는 방법이 있듯이 웹 서버와 클라이언트 간의 소통도 방법이 있다.
-> 클라이언트가 서버로 필요한 데이터를 요청하면 서버가 그걸 받아서 자기만의 방식으로 준다. 그걸 다시 가공해서(?) 코드에 적용시킨다.
어어엄청 많은데 사실 이번에 주로 배운 건 HTTP이다.
주요 통신 방식들
-> HTTP/HTTPS: 웹 기반의 애플리케이션에서 주로 사용됩니다. REST API나 SOAP와 같은 웹 서비스 통신 방식에서 기반이 됩니다.
-> WebSockets: 실시간 양방향 통신이 필요한 애플리케이션 (예: 채팅 애플리케이션, 실시간 게임)에서 사용됩니다.
-> Socket (TCP/UDP): TCP 또는 UDP 프로토콜을 사용하여 데이터를 전송합니다. 지속적인 연결을 유지하면서 양방향 통신이 가능합니다.
-> FTP (File Transfer Protocol): 파일 전송에 특화된 프로토콜입니다. 클라이언트와 서버 간의 파일 전송을 위해 사용됩니다.
-> RPC (Remote Procedure Call): 네트워크를 통해 다른 주소 공간에 있는 프로그램의 프로시저(또는 함수)를 호출하는 방식입니다.
-> SOAP (Simple Object Access Protocol): XML 기반의 메시징 프로토콜로, 주로 웹 서비스에서 사용됩니다.
-> GraphQL: 페이스북이 개발한 데이터 쿼리 및 조작 언어로, 클라이언트가 필요한 데이터의 구조를 명시하여 서버로부터 데이터를 받아옵니다.
-> gRPC: 구글이 개발한 RPC 프로토콜로, 효율적인 양방향 통신을 지원하며 여러 프로그래밍 언어를 지원합니다.
-> MQTT: IoT (Internet of Things) 기기와 같은 경량 클라이언트에서 사용되는 메시지 프로토콜입니다.
-> Message Queues (예: RabbitMQ, Kafka): 비동기 메시징을 위한 시스템으로, 메시지를 전송하는 생산자와 메시지를 받아 처리하는 소비자 사이에서 메시지를 전송합니다.
"dog": [
{"name" : "식빵", "family" : "웰시코기", "weight" = 1.5},
{"name" : "콩콩", "family" : "푸들", "weight" = 3.2},
{"name" : "강아지", "family" : "포메라니안", "weight" = 2.45}
]
이런 식이다.
val myClassInstance: MyClass = gson.fromJson(jsonString, MyClass::clss.java)
파라미터 정의
data class Person (
@SerializedName("person_name") <- 이게 문서에 적힌 이름..?
val name : String <- 이게 내가 실제로 선언한 변수
)
// MainActivity.kt
package com.example.androidappcodingtesttraining
import android.graphics.Color
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.example.androidappcodingtesttraining.databinding.ActivityMainBinding
import com.skydoves.powerspinner.IconSpinnerAdapter
import kotlinx.coroutines.launch
class MainActivity : AppCompatActivity() {
private val binding by lazy { ActivityMainBinding.inflate(layoutInflater) }
private var items = mutableListOf<DustItem>() // 미세먼지 데이터를 여기다 저장한다.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
// 시나 도를 나타내는 데이터를 받아온 데이터 클래스에서 꺼내오는 메소드를 호출
binding.spinnerViewSido.setOnSpinnerItemSelectedListener<String> { _, _, _, text ->
communicateNetWork(setUpDustParameter(text))
}
// 구나 시를 클릭하면 뽑아낸 시/도랑 구/시를 텍스트에 출력함
binding.spinnerViewGoo.setOnSpinnerItemSelectedListener<String> { _, _, _, text ->
Log.d("miseya", "selectedItem: spinnerViewGoo selected > $text")
// 파라미터들이 담긴 items에서 stationName을 기준으로 필터링해서 selectedItem에 저장
var selectedItem = items.filter { f -> f.stationName == text }
Log.d("miseya", "selectedItem: sidoName > " + selectedItem[0].sidoName)
Log.d("miseya", "selectedItem: pm10Value > " + selectedItem[0].pm10Value)
// 무슨 원리인지는 모르겠지만 예를 들어서 서울의 송파구를 선택해 selectedItem으로 들어오면
// 그 송파구에 해당하는 각각의 데이터들을 각각의 위젯에 값으로 설정한다.
binding.tvCityname.text = selectedItem[0].sidoName + " " + selectedItem[0].stationName
binding.tvDate.text = selectedItem[0].dataTime
binding.tvP10value.text = selectedItem[0].pm10Value + " ㎍/㎥"
// 미세먼지 지수를 바탕으로 UI 설정
when (getGrade(selectedItem[0].pm10Value)) {
1 -> {
binding.mainBg.setBackgroundColor(Color.parseColor("#9ED2EC"))
binding.ivFace.setImageResource(R.drawable.mise1)
binding.tvP10grade.text = "좋음"
}
2 -> {
binding.mainBg.setBackgroundColor(Color.parseColor("#D6A478"))
binding.ivFace.setImageResource(R.drawable.mise2)
binding.tvP10grade.text = "보통"
}
3 -> {
binding.mainBg.setBackgroundColor(Color.parseColor("#DF7766"))
binding.ivFace.setImageResource(R.drawable.mise3)
binding.tvP10grade.text = "나쁨"
}
4 -> {
binding.mainBg.setBackgroundColor(Color.parseColor("#BB3320"))
binding.ivFace.setImageResource(R.drawable.mise4)
binding.tvP10grade.text = "매우나쁨"
}
}
}
}
private fun communicateNetWork(param: HashMap<String, String>) = lifecycleScope.launch() {
// 클라이언트는 getDust라는 함수로 연결된다. (interface에 있다)
val responseData = NetWorkClient.dustNetWork.getDust(param)
Log.d("Parsing Dust ::", responseData.toString())
// SpinnerAdapter를 통해서 items에 파라미터들을 저장 (dustBody -> dustItem)
val adapter = IconSpinnerAdapter(binding.spinnerViewGoo)
items = responseData.response.dustBody.dustItem!!
// items의 여러 파라미터 중 stationName을 goo 변수에 리스트로 저장
val goo = ArrayList<String>()
items.forEach {
Log.d("add Item :", it.stationName)
goo.add(it.stationName)
}
// spinnerViewGoo라는 이름의 spinnerView에 그 goo를 차례로 넣는다.
runOnUiThread {
binding.spinnerViewGoo.setItems(goo)
}
}
private fun setUpDustParameter(sido: String): HashMap<String, String> {
// 키를 API서버 문서에서 받아서 여기다 적기
val authKey =
"QZUxotLLH0wmG17OBRLz8yAvsGZikpfmdTFG/ALLJ/PVLOoSOulqicLZFURik7KII8Ygk664CEW7Izd0RXS3GA=="
// 이건 요청 파라미터들 (결과값 파라미터들은 데이터 클래스에)
// interface에 선언해야 하는데 특별히 여기로 옮김
return hashMapOf(
"serviceKey" to authKey,
"returnType" to "json",
"numOfRows" to "100",
"pageNo" to "1",
"sidoName" to sido,
"ver" to "1.0"
)
}
// 미세먼지 지수가 들어오면 각각 해당하는 숫자를 반환
// 위에서 그 숫자를 가지고 등급을 나누어 UI를 바꾼다.
fun getGrade(value: String): Int {
val mValue = value.toInt()
var grade = 1
grade = if (mValue >= 0 && mValue <= 30) {
1
} else if (mValue >= 31 && mValue <= 80) {
2
} else if (mValue >= 81 && mValue <= 100) {
3
} else 4
return grade
}
}
// DustDTO.kt
// 각각의 파라미터들을 문서에 나와있는 상태 그대로 선언해 주면 되는데...
// 너무 많을 때도 있으니 공식 문서에서 받아서 이렇게 바꿔주는 flugin을 쓰드록 하자.
package com.example.androidappcodingtesttraining
import com.google.gson.annotations.SerializedName
data class Dust(val response: DustResponse)
data class DustResponse(
@SerializedName("body")
val dustBody: DustBody,
@SerializedName("header")
val dustHeader: DustHeader
)
data class DustBody(
val totalCount: Int,
@SerializedName("items")
val dustItem: MutableList<DustItem>?,
val pageNo: Int,
val numOfRows: Int
)
data class DustHeader(
val resultCode: String,
val resultMsg: String
)
data class DustItem(
val so2Grade: String,
val coFlag: String?,
val khaiValue: String,
val so2Value: String,
val coValue: String,
val pm25Flag: String?,
val pm10Flag: String?,
val o3Grade: String,
val pm10Value: String,
val khaiGrade: String,
val pm25Value: String,
val sidoName: String,
val no2Flag: String?,
val no2Grade: String,
val o3Flag: String?,
val pm25Grade: String,
val so2Flag: String?,
val dataTime: String,
val coGrade: String,
val no2Value: String,
val stationName: String,
val pm10Grade: String,
val o3Value: String
)
// NetWorkInterface.kt
package com.example.androidappcodingtesttraining
import retrofit2.http.GET
import retrofit2.http.QueryMap
interface NetWorkInterface {
// GET 키워드를 통해 받는다.
@GET("getCtprvnRltmMesureDnsty") // End Point
suspend fun getDust(@QueryMap param: HashMap<String, String>): Dust
}
// Main Activity에서 HashMap 형태로 선언되어서 여기에는 비어있다.
// NetworkClient.kt
package com.example.androidappcodingtesttraining
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.util.concurrent.TimeUnit
object NetWorkClient {
// BaseUrl 이거도 문서에 적혀 있다.
private const val DUST_BASE_URL = "http://apis.data.go.kr/B552584/ArpltnInforInqireSvc/"
private fun createOkHttpClient(): OkHttpClient {
val interceptor = HttpLoggingInterceptor()
// if (BuildConfig.DEBUG)
interceptor.level = HttpLoggingInterceptor.Level.BODY
// else
// interceptor.level = HttpLoggingInterceptor.Level.NONE
return OkHttpClient.Builder()
.connectTimeout(20, TimeUnit.SECONDS)
.readTimeout(20, TimeUnit.SECONDS)
.writeTimeout(20, TimeUnit.SECONDS)
.addNetworkInterceptor(interceptor)
.build()
}
private val dustRetrofit = Retrofit.Builder()
.baseUrl(DUST_BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.client(createOkHttpClient()) // 이거는 써도 되고, 안 써도 된다.
.build()
// 이게 문제의 JSON 객체 생성 및 변환
val dustNetWork: NetWorkInterface = dustRetrofit.create(NetWorkInterface::class.java)
}
// MainActivity.kt에서 나온 코드 (원래는 interface에 있어야 맞다)
val authKey = "QZUxotLLH0wmG17OBRLz8yAvsGZikpfmdTFG/ALLJ/PVLOoSOulqicLZFURik7KII8Ygk664CEW7Izd0RXS3GA=="
package com.example.androidappcodingtesttraining
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.util.concurrent.TimeUnit
object NetWorkClient {
// 바로 여기!
private const val DUST_BASE_URL = "http://apis.data.go.kr/B552584/ArpltnInforInqireSvc/"
private fun createOkHttpClient(): OkHttpClient {
val interceptor = HttpLoggingInterceptor()
// if (BuildConfig.DEBUG)
interceptor.level = HttpLoggingInterceptor.Level.BODY
// else
// interceptor.level = HttpLoggingInterceptor.Level.NONE
return OkHttpClient.Builder()
.connectTimeout(20, TimeUnit.SECONDS)
.readTimeout(20, TimeUnit.SECONDS)
.writeTimeout(20, TimeUnit.SECONDS)
.addNetworkInterceptor(interceptor)
.build()
}
private val dustRetrofit = Retrofit.Builder()
.baseUrl(DUST_BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.client(createOkHttpClient())
.build()
val dustNetWork: NetWorkInterface = dustRetrofit.create(NetWorkInterface::class.java)
}
package com.example.androidappcodingtesttraining
import retrofit2.http.GET
import retrofit2.http.QueryMap
interface NetWorkInterface {
@GET("getCtprvnRltmMesureDnsty") // 여기에 들어간다.
suspend fun getDust(@QueryMap param: HashMap<String, String>): Dust
}
dependencies {
implementation("com.google.code.gson.gson:2.10.1")
implementation("com.squareup.okhttp3:okhttp:4.10.0")
implementation("com.squareup.okhttp3:logging-interceptor:4.10.0")
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
}
android {
buildFeatures {
viewBinding = true
buildConfig = true
}
}
<uses-permission android:name="android.permission.INTERNET"/>