1. 이전 포스팅
https://growingsaja.tistory.com/981
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
- api 사용량 확인 페이지
https://openexchangerates.org/account/usage
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. 결과 확인
정상적으로 의도된 데이터들이 잘 저장된 것을 확인합니다.