[Kotlin][SpringBoot3] Kopring 서버 기본 실습 15 - 외부 api를 활용해 Scheduler로 정보 수집 실습 2 : open exchange rate api로 주요 국가 환율 정보 수집, 가공, 저장 기능 구현 (with MongoDB 연동)
1. 이전 포스팅
https://growingsaja.tistory.com/981
[Kotlin][SpringBoot3] Kopring 서버 응용 실습 01 - 새로운 프로젝트 하면 좋은 사전 세팅
1. 이전 포스팅 https://growingsaja.tistory.com/984 2. 목표 실시간 환율 정보 및 가상자산 정보를 수집해 저장하고, 데이터를 가공해 제공하는 api가 구현된 백엔드 서버 개발 - 개발하는 백엔드 시스템 기
growingsaja.tistory.com
2. 목표
- MongoDB 연동
- MongoDB에 Dunamu에서 제공해주는 api 활용해 환율 및 주요 원자재 등 가격 정보 조회
3. api 확인하기
https://openexchangerates.org/api/latest.json
https://openexchangerates.org/api/latest.json?app_id={api_key}
- api 문서 페이지
https://docs.openexchangerates.org/reference/latest-json
/latest.json
Get the latest exchange rates available from the Open Exchange Rates API.The most simple route in our API, latest.json provides a standard response object containing all the conversion rates for all of the currently available symbols/currencies, labeled by
docs.openexchangerates.org
- api 사용량 확인 페이지
https://openexchangerates.org/account/usage
Login - Open Exchange Rates
14 June 2022: We have updated our Terms & Conditions and Privacy Policy. Please read and familiarise yourself with these changes, as they apply to your continued use of our site and services.
openexchangerates.org
4. api call에 대한 response 데이터 확인
{
"disclaimer": "Usage subject to terms: https://openexchangerates.org/terms",
"license": "https://openexchangerates.org/license",
"timestamp": 1695265200,
"base": "USD",
"rates": {
"AED": 3.672915,
"AFN": 79.365677,
"ALL": 99.985896,
"AMD": 387.647516,
"ANG": 1.803099,
"AOA": 830,
"ARS": 349.9553,
"AUD": 1.5608,
"AWG": 1.8025,
"AZN": 1.7,
"BAM": 1.82815,
"BBD": 2,
"BDT": 109.803022,
"BGN": 1.83836,
"BHD": 0.376954,
"BIF": 2837.370027,
"BMD": 1,
"BND": 1.363868,
"BOB": 6.91328,
"BRL": 4.8804,
"BSD": 1,
"BTC": 0.000037076379,
"BTN": 83.277732,
"BWP": 13.639614,
"BYN": 2.525366,
"BZD": 2.016685,
"CAD": 1.349857,
"CDF": 2497.786322,
"CHF": 0.900497,
"CLF": 0.031986,
"CLP": 882.6,
"CNH": 7.31192,
"CNY": 7.3003,
"COP": 3958.212951,
"CRC": 531.4893,
"CUC": 1,
"CUP": 25.75,
"CVE": 103.068245,
"CZK": 22.958754,
"DJF": 179.262281,
"DKK": 7.012777,
"DOP": 57.084536,
"DZD": 137.0047,
"EGP": 30.8909,
"ERN": 15,
"ETB": 55.26,
"EUR": 0.940977,
"FJD": 2.27125,
"FKP": 0.812396,
"GBP": 0.812396,
"GEL": 2.655,
"GGP": 0.812396,
"GHS": 11.529075,
"GIP": 0.812396,
"GMD": 61.65,
"GNF": 8644.92804,
"GTQ": 7.868577,
"GYD": 209.307769,
"HKD": 7.82372,
"HNL": 24.82,
"HRK": 7.091352,
"HTG": 135.40636,
"HUF": 362.056766,
"IDR": 15405.05,
"ILS": 3.806885,
"IMP": 0.812396,
"INR": 83.193949,
"IQD": 1318.560282,
"IRR": 42250,
"ISK": 135.6,
"JEP": 0.812396,
"JMD": 154.78232,
"JOD": 0.7092,
"JPY": 148.27483333,
"KES": 146.71,
"KGS": 88.71,
"KHR": 4148.050726,
"KMF": 459.949574,
"KPW": 900,
"KRW": 1340.865511,
"KWD": 0.308883,
"KYD": 0.833781,
"KZT": 477.09184,
"LAK": 20190.751797,
"LBP": 15036.104936,
"LKR": 325.174754,
"LRD": 186.499991,
"LSL": 19.036538,
"LYD": 4.864291,
"MAD": 10.307691,
"MDL": 18.009997,
"MGA": 4492.5,
"MKD": 57.775104,
"MMK": 2100.997679,
"MNT": 3450,
"MOP": 8.061616,
"MRU": 38.21048,
"MUR": 44.85816,
"MVR": 15.375,
"MWK": 1113.865075,
"MXN": 17.142443,
"MYR": 4.6915,
"MZN": 63.950001,
"NAD": 18.93,
"NGN": 781.092745,
"NIO": 36.59,
"NOK": 10.821776,
"NPR": 133.244208,
"NZD": 1.693568,
"OMR": 0.384982,
"PAB": 1,
"PEN": 3.728724,
"PGK": 3.7125,
"PHP": 56.958504,
"PKR": 291.964072,
"PLN": 4.340868,
"PYG": 7279.706823,
"QAR": 3.641,
"RON": 4.6772,
"RSD": 110.387,
"RUB": 96.061479,
"RWF": 1207.260873,
"SAR": 3.751441,
"SBD": 8.415589,
"SCR": 13.009171,
"SDG": 600.5,
"SEK": 11.187843,
"SGD": 1.367849,
"SHP": 0.812396,
"SLL": 20969.5,
"SOS": 575.360442,
"SRD": 38.221,
"SSP": 130.26,
"STD": 22281.8,
"STN": 23.166981,
"SVC": 8.754703,
"SYP": 2512.53,
"SZL": 19.0293,
"THB": 36.2415,
"TJS": 10.990396,
"TMT": 3.51,
"TND": 3.144,
"TOP": 2.388828,
"TRY": 27.036547,
"TTD": 6.783352,
"TWD": 32.131299,
"TZS": 2505,
"UAH": 36.949595,
"UGX": 3746.414199,
"USD": 1,
"UYU": 38.145239,
"UZS": 12195,
"VES": 33.791109,
"VND": 24310.10068,
"VUV": 118.722,
"WST": 2.7185,
"XAF": 617.240311,
"XAG": 0.04326944,
"XAU": 0.00051902,
"XCD": 2.70255,
"XDR": 0.758722,
"XOF": 617.240311,
"XPD": 0.00079465,
"XPF": 112.2884,
"XPT": 0.00108194,
"YER": 250.375049,
"ZAR": 18.95355,
"ZMW": 20.965996,
"ZWL": 322
}
}
5. mongoDB 연동
# vim /main/resources/application.properties
# ...
# mongodb
spring.data.mongodb.uri=mongodb://localhost:27017/test00
# ...
6. 프로젝트 구조 설계
크게 아래 두 구조 형태를 가지고 가는 것을 권장합니다.
entity - repository - service - controller
entity - repository - service - scheduler
7. entity로 data class 만들기
// vim OpenexchangeRateData_raw.kt
package com.dev.kopring00.external.fiat.entities
data class OpenexchangeRateData_raw(
val timestamp: Long, // 정보의 기준 unix timestamp
val disclaimer: String,
val license: String,
val base: String,
val rates: Map<String, Double>
)
- 수집한 모든 데이터를 그대로 저장하는 것이 아니라 필요 형태로 가공해 저장합니다.
// vim OpenexchangeRateData.kt
package com.dev.kopring00.external.fiat.entities
import org.springframework.data.mongodb.core.mapping.Document
@Document(collection = "openexchangeRateData")
data class OpenexchangeRateData(
var timestamp: Long, // 정보의 기준 datetime
val createdAt: Long?, // 정보 저장 일시
val rates: Map<String, Double>
)
8. repository 만들기
// vim OpenexchangeRateDataRepository.kt
package com.dev.kopring00.external.fiat.repositories
import com.dev.kopring00.external.fiat.entities.OpenexchangeRateData
import org.springframework.data.mongodb.repository.MongoRepository
interface OpenexchangeRateDataRepository: MongoRepository<OpenexchangeRateData, String> {
fun findFirstByOrderByTimestampDesc(): OpenexchangeRateData?
}
9. service 만들기
- api call limit이 있기 때문에 isNeedUpdate 로 확인
// vim OpenexchangeRateDataService.kt
package com.dev.kopring00.external.fiat.services
import com.dev.kopring00.external.fiat.entities.OpenexchangeRateData_raw
import com.dev.kopring00.external.fiat.entities.OpenexchangeRateData
import com.dev.kopring00.external.fiat.repositories.OpenexchangeRateDataRepository
import org.springframework.stereotype.Service
import org.springframework.web.client.RestTemplate
import java.time.Instant
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
@Service
class OpenexchangeRateDataService(private val openexchangeRateDataRepository: OpenexchangeRateDataRepository) {
// 외부 api call
fun getDataFromApi(apiUrl: String): OpenexchangeRateData_raw? {
val restTemplate = RestTemplate()
val response = restTemplate.getForEntity(apiUrl, OpenexchangeRateData_raw::class.java)
return response.body
}
// 데이터 저장하기
fun saveDataFromData(openexchangeRateDataRaw: OpenexchangeRateData_raw) {
val unixTimestamp: Long = openexchangeRateDataRaw.timestamp // 정보 기준 일시 unix timestamp
val openexchangeRateData = OpenexchangeRateData(
unixTimestamp,
Instant.now().epochSecond,
openexchangeRateDataRaw.rates
)
openexchangeRateDataRepository.save(openexchangeRateData)
}
// 데이터 최신화 필요 여부 확인
// 날짜, 시간까지만 일치 여부 확인 : "yyyy-MM-dd HH"
fun isNeedUpdate(): Boolean {
val currentTimestamp = Instant.now().epochSecond
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH")
val currentDateHour = formatter.format(Instant.ofEpochSecond(currentTimestamp).atOffset(ZoneOffset.UTC))
val lastSavedDatetime: OpenexchangeRateData? = openexchangeRateDataRepository.findFirstByOrderByTimestampDesc()
println("currentDateHour : $currentDateHour")
// 데이터가 있는 경우, 데이터 최신화 필요한지 체크
if (lastSavedDatetime != null) {
val instant = Instant.ofEpochSecond(lastSavedDatetime.timestamp)
val lastSavedDateHour: String = formatter.format(instant.atOffset(ZoneOffset.UTC))
println("lastSavedDateHour : $lastSavedDateHour")
return lastSavedDateHour != currentDateHour
}
// 데이터가 없으면 즉시 update 실행
return true
}
}
10. scheduler 만들기
// vim OpenexchangeRateDataScheduler.kt
package com.dev.kopring00.external.fiat.schedulers
import com.dev.kopring00.external.fiat.services.OpenexchangeRateDataService
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.beans.factory.annotation.Value
import org.springframework.scheduling.annotation.Scheduled
import org.springframework.stereotype.Component
@Component
class OpenexchangeRateDataScheduler @Autowired constructor(private val openexchangeRateDataService: OpenexchangeRateDataService) {
@Value("\${api.url.openexchangeRate}")
private lateinit var apiUrl: String
@Value("\${api.url.openexchangeKey}")
private lateinit var apiKey: String
fun updateRateData() {
val fullApiUrl = apiUrl+apiKey
val openexchangeRateData = openexchangeRateDataService.getDataFromApi(fullApiUrl)
if (openexchangeRateData != null) {
openexchangeRateDataService.saveDataFromData(openexchangeRateData)
} else {
}
}
// 3600초=1시간마다 실행
@Scheduled(fixedRate = 3600000)
fun scheduledUpdateOpenexchangeRates() {
if (openexchangeRateDataService.isNeedUpdate()) {
updateRateData()
}
}
}
11. 변수 설정
# vim /main/resources/application.properties
# ...
# rates
api.url.openexchangeRate=https://openexchangerates.org/api/latest.json?app_id=
api.url.openexchangeKey=k2p9qt483uvy899qptv28tcuale2
12. 결과 확인
정상적으로 의도된 데이터들이 잘 저장된 것을 확인합니다.