수정사항
20200823 : 데이터베이스 구축 링크 추가
20200824 : 엑셀파일의 테이블화 링크 추가
API 신청
날씨 API를 사용하기 위해서는 먼저 API 신청이 필요하다.
아래 공공데이터 포털로 이동한다.
회원가입, 로그인 후 "동네예보 조회서비스"를 검색 후 활용 신청한다.
기상정보는 동네예보 조회서비스 이외에도 다양한 API가 존재한다. 상황에 맞는 걸 사용할 수 있다.
동네예보 조회서비스는 활용신청 후 곧바로 승인이 난다. 마이페이지로 이동하여 승인된 API의 인증키를 기억한다.
API 사용
동네예보 조회서비스에는 초단기실황조회, 초단기예보조회, 동네예보조회, 예보버전조회가 있으며 이 글은 동네예보조회를 기준으로 한다.
인터페이스는 REST이며 데이터 표준은 JSON을 이용한다.
AndroidManifest.xml
인터넷을 사용해야 하므로 아래 퍼미션을 AndroidManefest.xml에 추가한다.
<manifest>
<application>
~
</application>
<uses-permission android:name="android.permission.INTERNET" /> // 추가
</manifest>
또한 기상청 공공데이터는 http로 만들어져 있는데 안드로이드 9부터 https 대신 http를 사용하면 컴파일 시 보안 관련해서 에러가 발생한다. 따라서 아래 코드를 추가하여 http를 사용할 수 있도록 허용한다.
<manifest
<application
~
android:usesCleartextTraffic="true"> // 추가
<activity>
</activity>
</application>
</manifest>
build.gradle(Module:app)
구현을 위해 retrofit, gson 라이브러리를 사용한다.
retrofit은 HTTP 통신을 쉽게 하기 위해 만들어진 라이브러리이며 gson은 JSON과 JAVA Object 간 변환을 도와주는 라이브러리다. 여기서는 json으로 받은 데이터를 JAVA Object로 변환시킨다.
▶ retrofit 참조
https://square.github.io/retrofit/
build.gradle(Module:app)에 retrofit, gson 라이브러리를 추가한다.
dependencies {
~
implementation 'com.squareup.retrofit2:retrofit:2.8.0' // 추가
implementation 'com.squareup.retrofit2:converter-gson:2.8.0' // 추가
}
MainActirivy.kt
빌더 생성
retrofit을 사용하기 위한 빌더를 생성한다.
private val retrofit = Retrofit.Builder()
.baseUrl("http://apis.data.go.kr/1360000/VilageFcstInfoService/")
.addConverterFactory(GsonConverterFactory.create())
.build()
object ApiObject {
val retrofitService: WeatherInterface by lazy {
retrofit.create(WeatherInterface::class.java)
}
}
baseUrl에는 API를 주소를 쓰는데, 마지막에는 반드시'/'가 포함되어야 한다.
addConverterFactory에는 gson을 사용한다.
인터페이스 생성
HTTP로 보낼 요청 메시지를 작성한다. 요청 메시지에는 인증키, 예보를 받을 장소 , 시간 등이 포함된다.
공공데이터 API 페이지에 함께 있는 동네예보 조회서비스 활용 가이드를 보면 요청 메시지의 예제는 다음과 같다. 요청메세지 각각의 항목 설명도 활용 가이드에 포함되어있다.
http://apis.data.go.kr/1360000/VilageFcstInfoService/getUltraSrtFcst
?serviceKey=
인증키&numOfRows=10&pageNo=1&base_date=20151201&base_time=0630&nx=55&ny=127
위 예제의 "http://apis.data.go.kr/1360000/VilageFcstInfoService/" 부분은 빌더의 baseUrl에 포함되었다. 따라서 그 이후의 내용만 Interface에 들어간다. 아래 "서비스키"에는 API 신청 시 받은 서비스키를 입력한다.
interface WeatherInterface {
@GET("getUltraSrtFcst" +
"?serviceKey=서비스키&numOfRows=10&pageNo=1" +
"&base_date=20200808&base_time=0630&nx=55&ny=127")
fun GetWeather(): Call<WEATHER> // DATA CLASS
}
그런데 위와 같이 구현하면 base_date, base_time 등이 고정되는 문제가 있다. 따라서 아래와 같이 Query를 사용하여 데이터를 동적으로 보낼 수 있다.
val num_of_rows = 10
val page_no = 1
val data_type = "JSON"
val base_time = 1100
val base_data = 20200808
val nx = "55"
val ny = "127"
interface WeatherInterface {
@GET("getVilageFcst?serviceKey=서비스키")
fun GetWeather(
@Query("dataType") data_type : String,
@Query("numOfRows") num_of_rows : Int,
@Query("pageNo") page_no : Int,
@Query("base_date") base_date : Int,
@Query("base_time") base_time : Int,
@Query("nx") nx : String,
@Query("ny") ny : String
): Call<WEATHER> // WEATHER는 DATA CLASS
}
Query는 주소 '?' 뒷부분의 속성을 작성할 수 있다. 서비스키도 동적으로 구현하려고 했으나 다른 오류가 발생해서 서비스키는 정적으로 구현했다.
▶Query와 오류에 대해서는 아래 내용을 참고한다.
stackoverflow.com/questions/37698501/retrofit-2-path-vs-query
인터페이스는 후술 할 Data Class를 호출한다.
Data Class
HTTP로 메시지를 호출했으니 응답 메세지를 받아야 한다. 활용 가이드를 통해 확인한 응답 메시지의 형식은 아래와 같다.아래는 json이 아닌 xml인데, 표현 방법만 다를 뿐 틀은 같다.
<response>
<header>
<resultCode>0</resultCode>
<resultMsg>NORMAL_SERVICE</resultMsg>
</header>
<body>
<dataType>XML</dataType>
<items>
<item>
<baseDate>20181010</baseDate>
<baseTime>0600</baseTime>
<category>RN1</category>
<nx>55</nx>
<ny>127</ny>
<obsrValue>0</obsrValue>
</item>
</items>
<numOfRows>10</numOfRows>
<pageNo>1</pageNo>
<totalCount>1</totalCount>
</body>
</response>
위 형식과 똑같이 데이터 클래스를 생성하여 메시지를 수신한다.
특히 변수명이 위와 같아야 한다. 클래스 명은 달라도 된다.
item은 다수를 받기 때문에 리스트로 구현했다.
data class WEATHER (
val response : RESPONSE
)
data class RESPONSE (
val header : HEADER,
val body : BODY
)
data class HEADER(
val resultCode : Int,
val resultMsg : String
)
data class BODY(
val dataType : String,
val items : ITEMS
)
data class ITEMS(
val item : List<ITEM>
)
data class ITEM(
val baseData : Int,
val baseTime : Int,
val category : String
)
item에는 baseData, baseTime, category, nx 등이 있지만 수신하고 싶은 데이터만 데이터 클래스로 만들어줄 수 있다.
Main
만들어둔 빌더, 인터페이스, 데이터 클래스를 사용한다.
retrofit은 enqueue로 호출한다. 언젠가부터 네트워크 관련 함수들은 메인 스레드에서 처리를 못하기 때문.
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val call = ApiObject.retrofitService.GetWeather(data_type, num_of_rows, page_no, base_data, base_time, nx, ny)
call.enqueue(object : retrofit2.Callback<WEATHER>{
override fun onResponse(call: Call<WEATHER>, response: Response<WEATHER>) {
if (response.isSuccessful){
Log.d("api", response.body().toString())
Log.d("api", response.body()!!.response.body.items.item.toString())
Log.d("api", response.body()!!.response.body.items.item[0].category)
}
}
override fun onFailure(call: Call<WEATHER>, t: Throwable) {
Log.d("api fail : ", t.message)
}
})
}
}
메시지를 수신했을 때 로그를 기록하도록 했으며 세 종류의 로그는 아래와 같다.
D/api: WEATHER(response=RESPONSE(header=HEADER(resultCode=0, resultMsg=NORMAL_SERVICE), body=BODY(dataType=JSON, items=ITEMS(item=[ITEM(baseData=0, baseTime=1100, category=POP), ITEM(baseData=0, baseTime=1100, category=PTY), ITEM(baseData=0, baseTime=1100, category=REH), ITEM(baseData=0, baseTime=1100, category=SKY), ITEM(baseData=0, baseTime=1100, category=T3H), ITEM(baseData=0, baseTime=1100, category=TMX), ITEM(baseData=0, baseTime=1100, category=UUU), ITEM(baseData=0, baseTime=1100, category=VEC), ITEM(baseData=0, baseTime=1100, category=VVV), ITEM(baseData=0, baseTime=1100, category=WSD)]))))
D/api: [ITEM(baseData=0, baseTime=1100, category=POP), ITEM(baseData=0, baseTime=1100, category=PTY), ITEM(baseData=0, baseTime=1100, category=REH), ITEM(baseData=0, baseTime=1100, category=SKY), ITEM(baseData=0, baseTime=1100, category=T3H), ITEM(baseData=0, baseTime=1100, category=TMX), ITEM(baseData=0, baseTime=1100, category=UUU), ITEM(baseData=0, baseTime=1100, category=VEC), ITEM(baseData=0, baseTime=1100, category=VVV), ITEM(baseData=0, baseTime=1100, category=WSD)]
D/api: POP
응답메시지 각 항목에 대한 설명은 활용가이드를 참고한다.
MainActivity.kt 전체 코드
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import android.util.Log
import retrofit2.Call
import retrofit2.Response
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.GET
import retrofit2.http.Query
val num_of_rows = 10
val page_no = 1
val data_type = "JSON"
val base_time = 1100
val base_data = 20200808
val nx = "55"
val ny = "127"
data class WEATHER (
val response : RESPONSE
)
data class RESPONSE (
val header : HEADER,
val body : BODY
)
data class HEADER(
val resultCode : Int,
val resultMsg : String
)
data class BODY(
val dataType : String,
val items : ITEMS
)
data class ITEMS(
val item : List<ITEM>
)
data class ITEM(
val baseData : Int,
val baseTime : Int,
val category : String
)
interface WeatherInterface {
@GET("getVilageFcst?serviceKey=서비스키")
fun GetWeather(
@Query("dataType") data_type : String,
@Query("numOfRows") num_of_rows : Int,
@Query("pageNo") page_no : Int,
@Query("base_date") base_date : Int,
@Query("base_time") base_time : Int,
@Query("nx") nx : String,
@Query("ny") ny : String
): Call<WEATHER>
}
private val retrofit = Retrofit.Builder()
.baseUrl("http://apis.data.go.kr/1360000/VilageFcstInfoService/") // 마지막 / 반드시 들어가야 함
.addConverterFactory(GsonConverterFactory.create()) // converter 지정
.build() // retrofit 객체 생성
object ApiObject {
val retrofitService: WeatherInterface by lazy {
retrofit.create(WeatherInterface::class.java)
}
}
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val call = ApiObject.retrofitService.GetWeather(data_type, num_of_rows, page_no, base_data, base_time, nx, ny)
call.enqueue(object : retrofit2.Callback<WEATHER>{
override fun onResponse(call: Call<WEATHER>, response: Response<WEATHER>) {
if (response.isSuccessful){
Log.d("api", response.body().toString())
Log.d("api", response.body()!!.response.body.items.item.toString())
Log.d("api", response.body()!!.response.body.items.item[0].category)
}
}
override fun onFailure(call: Call<WEATHER>, t: Throwable) {
Log.d("api fail : ", t.message)
}
})
}
}
Room을 이용한 데이터베이스 구축
기상청 API를 활용하는 대부분의 앱은 지역 이름을 받아와서 기상 데이터를 출력하는 방법을 사용할 것이다.
따라서 지역 이름에 대한 좌표의 매핑이 필요한데, 이는 API 문서인 '기상청18_동네예보 조회서비스_오픈API활용가이드_격자_위경도(20200706)' 에 지역과 좌표가 나와있다. 따라서 엑셀 파일의 내용을 데이터베이스로 구축할 필요가 있다. 그리고 이 데이터베이스는 사용자간 공유가 없고 오로지 Read용으로만 쓰이기 때문에 내부 데이터베이스를 구축하는것이 간편하다.
엑셀파일의 테이블화
구축된 데이터베이스의 테이블에 '기상청18_동네예보 조회서비스_오픈API활용가이드_격자_위경도(20200706)' 파일의 내용을 넣어야 한다. 3000개 이상의 라인을 일일이 수작업으로 넣는것 외에 다른 방법을 찾고 싶다면 아래 글을 참고할 수 있다.
참조
www.raywenderlich.com/6994782-android-networking-with-kotlin-tutorial-getting-started
square.github.io/retrofit/2.x/retrofit/
https://medium.com/@c004112/android-pie-http-%ED%97%88%EC%9A%A9-dc62c632261b
dev.to/paulodhiambo/kotlin-and-retrofit-network-calls-2353
jungwoon.github.io/android/2019/07/11/Retrofit/
stackoverflow.com/questions/37698501/retrofit-2-path-vs-query
최근댓글