수정사항

더보기

20200823 : 데이터베이스 구축 링크 추가

20200824 : 엑셀파일의 테이블화 링크 추가

 

 

API 신청

날씨 API를 사용하기 위해서는 먼저 API 신청이 필요하다.

아래 공공데이터 포털로 이동한다.

https://www.data.go.kr/

 

공공데이터 포털

국가에서 보유하고 있는 다양한 데이터를『공공데이터의 제공 및 이용 활성화에 관한 법률(제11956호)』에 따라 개방하여 국민들이 보다 쉽고 용이하게 공유•활용할 수 있도록 공공데이터(Datase

www.data.go.kr

 

회원가입, 로그인 후 "동네예보 조회서비스"를 검색 후 활용 신청한다.

기상정보는 동네예보 조회서비스 이외에도 다양한 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/

 

Retrofit

A type-safe HTTP client for Android and Java

square.github.io

 

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

stackoverflow.com/questions/39918814/use-jsonreader-setlenienttrue-to-accept-malformed-json-at-line-1-column-1-path

 

인터페이스는 후술 할 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용으로만 쓰이기 때문에 내부 데이터베이스를 구축하는것이 간편하다.

 

 

 

[Android, Kotlin] Room을 활용한 데이터베이스 구축

Room 오라클, mysql과 같은 데이터베이스를 사용하기 위해서는 외부 데이터베이스 구축과 웹서버가 필요하다. 다시 말해 인터넷이 연결되어있어야 한다. 그러나 로컬 데이터베이스인 SQlite를 활용�

hydroponicglass.tistory.com

 

 

 

엑셀파일의 테이블화

구축된 데이터베이스의 테이블에 '기상청18_동네예보 조회서비스_오픈API활용가이드_격자_위경도(20200706)' 파일의 내용을 넣어야 한다. 3000개 이상의 라인을 일일이 수작업으로 넣는것 외에 다른 방법을 찾고 싶다면 아래 글을 참고할 수 있다.

 

 

[Android, Kotlin] 엑셀, 텍스트파일의 데이터베이스 테이블화(Room)

읽기 전에 이 글은 안드로이드에서 기상청 날씨 API(동네예보) 사용법(retrofit) , Room을 활용한 데이터베이스 구축에 이어지는 글입니다. 따라서 아래 글을 읽지 않으신다면 이해가 힘든 부분이 있�

hydroponicglass.tistory.com

 

 

참조

github.com/google/gson

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

acaroom.net/ko/blog/youngdeok/%EC%97%B0%EC%9E%AC-%EC%BD%94%ED%8B%80%EB%A6%B0-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-02-5%EB%8B%A8%EA%B3%84-retrofit%EC%9D%98-rest-api%EC%9D%98-%EC%B2%98%EB%A6%AC

jungwoon.github.io/android/2019/07/11/Retrofit/

stackoverflow.com/questions/37698501/retrofit-2-path-vs-query

stackoverflow.com/questions/39918814/use-jsonreader-setlenienttrue-to-accept-malformed-json-at-line-1-column-1-path