Skip to content

Commit

Permalink
[BLOOM-107] 기상청 API 배치 시스템 구현 (#132)
Browse files Browse the repository at this point in the history
* refactor: 날씨 메시지 타입 추가

* feat: WeatherCareMessageRepository 구현

* feat: WeatherCareMessage 배치 구현

* refactor: coroutines를 사용한 비동기 및 병렬 처리

* feat: Redis Repository 작성, Pipeline 사용

* refactor: WeatherCareMessage Serializable 추가

* refactor: 캐시 조회 후 메시지 생성 로직 구현

* refactor: coroutine map 방식에서 list add 방식으로 변경

* style: ktlint 적용

* refactor: batch 의존성 수정

* refactor: batch 의존성 수정
  • Loading branch information
stophwan authored Sep 26, 2024
1 parent 1302f8c commit f692c44
Show file tree
Hide file tree
Showing 22 changed files with 501 additions and 55 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ package dnd11th.blooming.api.controller.weather
import dnd11th.blooming.api.annotation.LoginUser
import dnd11th.blooming.api.annotation.Secured
import dnd11th.blooming.api.dto.weather.WeatherMessageResponse
import dnd11th.blooming.api.service.weather.WeatherMessage
import dnd11th.blooming.api.service.weather.WeatherService
import dnd11th.blooming.core.entity.user.User
import dnd11th.blooming.redis.entity.weather.Message
import dnd11th.blooming.redis.entity.weather.WeatherCareMessage
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
Expand All @@ -21,8 +22,9 @@ class WeatherMessageController(
override fun getWeatherMessage(
@LoginUser loginUser: User,
): List<WeatherMessageResponse> {
val weatherMessages: List<WeatherMessage> =
val weatherCareMessage: WeatherCareMessage =
weatherService.createWeatherMessage(loginUser, LocalDateTime.now())
return weatherMessages.map { weatherMessage -> WeatherMessageResponse.from(weatherMessage) }
val messages: List<Message> = weatherCareMessage.messages
return messages.map { weatherMessage -> WeatherMessageResponse.from(weatherMessage) }
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package dnd11th.blooming.api.dto.weather

import dnd11th.blooming.api.service.weather.WeatherMessage
import dnd11th.blooming.redis.entity.weather.Message
import io.swagger.v3.oas.annotations.media.Schema

@Schema(
Expand All @@ -16,12 +16,16 @@ data class WeatherMessageResponse(
val message: List<String>,
) {
companion object {
fun from(weatherMessages: WeatherMessage): WeatherMessageResponse {
fun from(message: Message): WeatherMessageResponse {
return WeatherMessageResponse(
status = weatherMessages.name,
title = weatherMessages.title,
message = weatherMessages.message,
status = message.status,
title = message.title,
message = splitByNewLine(message.message),
)
}

private fun splitByNewLine(text: String): List<String> {
return text.split("\n")
}
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import dnd11th.blooming.common.exception.ErrorType
import dnd11th.blooming.common.exception.NotFoundException
import dnd11th.blooming.core.entity.user.User
import dnd11th.blooming.core.repository.user.UserRepository
import dnd11th.blooming.redis.entity.weather.WeatherCareMessage
import dnd11th.blooming.redis.entity.weather.WeatherCareMessageType
import dnd11th.blooming.redis.repository.weather.WeatherCareMessageRepository
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.time.LocalDate
Expand All @@ -20,6 +23,7 @@ class WeatherService(
private val openApiProperty: OpenApiProperty,
private val weatherInfoClient: WeatherInfoClient,
private val userRepository: UserRepository,
private val weatherCareMessageRepository: WeatherCareMessageRepository,
) {
companion object {
private const val HUMIDITY_KEY = "REH"
Expand All @@ -37,21 +41,54 @@ class WeatherService(
fun createWeatherMessage(
loginUser: User,
now: LocalDateTime,
): List<WeatherMessage> {
val user: User =
userRepository.findById(loginUser.id!!).orElseThrow {
throw NotFoundException(ErrorType.USER_NOT_FOUND)
}
): WeatherCareMessage {
val user = getUser(loginUser.id!!)
val regionKey = generateRegionKey(user.nx, user.ny)

return findWeatherMessageInCache(regionKey) ?: createAndCacheWeatherMessage(user, now)
}

private fun getUser(userId: Long): User {
return userRepository.findById(userId).orElseThrow {
throw NotFoundException(ErrorType.USER_NOT_FOUND)
}
}

private fun generateRegionKey(
nx: Int,
ny: Int,
): String {
return "nx${nx}ny$ny"
}

val weatherItems: List<WeatherItem> =
weatherInfoClient.getWeatherInfo(
serviceKey = openApiProperty.serviceKey,
base_date = getBaseDate(now),
nx = user.nx,
ny = user.ny,
).toWeatherItems()
private fun findWeatherMessageInCache(regionKey: String): WeatherCareMessage? {
return weatherCareMessageRepository.findByRegion(regionKey)
}

private fun createAndCacheWeatherMessage(
user: User,
now: LocalDateTime,
): WeatherCareMessage {
val weatherItems = fetchWeatherItems(user, now)
val weatherCareMessageTypes = determineWeatherMessages(weatherItems)

val newWeatherMessage = WeatherCareMessage.create(user.nx, user.ny, weatherCareMessageTypes)

return determineWeatherMessages(weatherItems)
weatherCareMessageRepository.save(newWeatherMessage)

return newWeatherMessage
}

private fun fetchWeatherItems(
user: User,
now: LocalDateTime,
): List<WeatherItem> {
return weatherInfoClient.getWeatherInfo(
serviceKey = openApiProperty.serviceKey,
base_date = getBaseDate(now),
nx = user.nx,
ny = user.ny,
).toWeatherItems()
}

/**
Expand All @@ -60,7 +97,7 @@ class WeatherService(
* 한파 -> 최저 온도가 5도 이하로 내려가면 COLD
* 더위 -> 최고 온도가 30도 이상 올라가는 날 있으면 HOT
*/
private fun determineWeatherMessages(items: List<WeatherItem>): List<WeatherMessage> {
private fun determineWeatherMessages(items: List<WeatherItem>): List<WeatherCareMessageType> {
var maxHumidity = MIN_HUMIDITY
var minHumidity = MAX_HUMIDITY
var maxTemperature = MIN_TEMPERATURE
Expand All @@ -81,11 +118,11 @@ class WeatherService(
}
}

return mutableListOf<WeatherMessage>().apply {
if (maxHumidity >= HIGH_HUMIDITY_THRESHOLD) add(WeatherMessage.HUMIDITY)
if (minHumidity <= LOW_HUMIDITY_THRESHOLD) add(WeatherMessage.DRY)
if (minTemperature <= LOW_TEMPERATURE_THRESHOLD) add(WeatherMessage.COLD)
if (maxTemperature >= HIGH_TEMPERATURE_THRESHOLD) add(WeatherMessage.HOT)
return mutableListOf<WeatherCareMessageType>().apply {
if (maxHumidity >= HIGH_HUMIDITY_THRESHOLD) add(WeatherCareMessageType.HUMIDITY)
if (minHumidity <= LOW_HUMIDITY_THRESHOLD) add(WeatherCareMessageType.DRY)
if (minTemperature <= LOW_TEMPERATURE_THRESHOLD) add(WeatherCareMessageType.COLD)
if (maxTemperature >= HIGH_TEMPERATURE_THRESHOLD) add(WeatherCareMessageType.HOT)
}
}

Expand Down
4 changes: 3 additions & 1 deletion batch/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
tasks.jar { enabled = true }
tasks.bootJar { enabled = false }
tasks.bootJar { enabled = true }

dependencies {
implementation(project(":common"))
implementation(project(":client"))
implementation(project(":domain:core"))
implementation(project(":domain:redis"))

implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework:spring-tx")
implementation("org.springframework.boot:spring-boot-starter-batch")
testImplementation("org.springframework.batch:spring-batch-test")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package dnd11th.blooming

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.context.properties.ConfigurationPropertiesScan
import org.springframework.boot.runApplication

@SpringBootApplication
@ConfigurationPropertiesScan
class Dnd11th8BackendBatchApplication

fun main(args: Array<String>) {
runApplication<Dnd11th8BackendBatchApplication>(*args)
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.scheduling.annotation.EnableScheduling

@Configuration
@Configuration(proxyBeanMethods = false)
@EnableScheduling
class BatchConfig {
@Bean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import org.springframework.batch.item.ItemReader
import org.springframework.batch.item.ItemWriter
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Primary
import org.springframework.transaction.PlatformTransactionManager

@Configuration
Expand All @@ -23,6 +24,7 @@ class PlantNotificationJobConfig {
}

@Bean
@Primary
fun notificationJob(
jobRepository: JobRepository,
waterNotificationStep: Step,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package dnd11th.blooming.batch.controller

import dnd11th.blooming.batch.weather.WeatherCareMessageScheduler
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController

@RestController
class DailyWeatherCallController(
private val weatherCareMessageScheduler: WeatherCareMessageScheduler,
) {
@GetMapping("/daily-weather-call")
fun runWeatherCareMessageJob(): ResponseEntity<Void> {
weatherCareMessageScheduler.run()
return ResponseEntity.noContent().build()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package dnd11th.blooming.batch.weather

import dnd11th.blooming.core.entity.region.Region
import org.springframework.batch.core.Job
import org.springframework.batch.core.Step
import org.springframework.batch.core.configuration.annotation.JobScope
import org.springframework.batch.core.job.builder.JobBuilder
import org.springframework.batch.core.launch.support.RunIdIncrementer
import org.springframework.batch.core.repository.JobRepository
import org.springframework.batch.core.step.builder.StepBuilder
import org.springframework.batch.item.ItemReader
import org.springframework.batch.item.ItemWriter
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.transaction.PlatformTransactionManager

@Configuration
class WeatherCareMessageJobConfig {
companion object {
const val CHUNK_SIZE: Int = 1000
}

@Bean
fun weatherCareMessageBatchJob(
jobRepository: JobRepository,
weatherCareMessageBatchStep: Step,
): Job {
return JobBuilder("weatherBatchJob", jobRepository)
.incrementer(RunIdIncrementer())
.start(weatherCareMessageBatchStep)
.build()
}

@Bean
@JobScope
fun weatherCareMessageBatchStep(
jobRepository: JobRepository,
transactionManager: PlatformTransactionManager,
weatherCareMessageItemReader: ItemReader<Region>,
weatherCareMessageItemWriter: ItemWriter<Region>,
): Step {
return StepBuilder("weatherBatchStep", jobRepository)
.chunk<Region, Region>(CHUNK_SIZE, transactionManager)
.reader(weatherCareMessageItemReader)
.writer(weatherCareMessageItemWriter)
.build()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package dnd11th.blooming.batch.weather

import dnd11th.blooming.core.entity.region.Region
import dnd11th.blooming.core.repository.region.RegionRepository
import org.springframework.batch.core.configuration.annotation.StepScope
import org.springframework.batch.item.support.ListItemReader
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

@Configuration
class WeatherCareMessageReader(
private val regionRepository: RegionRepository,
) {
@Bean
@StepScope
fun weatherCareMessageItemReader(): ListItemReader<Region> {
val regions: List<Region> = regionRepository.findAll()
return ListItemReader(regions)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package dnd11th.blooming.batch.weather

import dnd11th.blooming.common.util.Logger.Companion.log
import org.springframework.batch.core.Job
import org.springframework.batch.core.JobParameters
import org.springframework.batch.core.JobParametersBuilder
import org.springframework.batch.core.launch.JobLauncher
import org.springframework.beans.factory.annotation.Qualifier
import org.springframework.stereotype.Component
import org.springframework.util.StopWatch
import java.util.concurrent.TimeUnit

@Component
class WeatherCareMessageScheduler(
private val jobLauncher: JobLauncher,
@Qualifier("weatherCareMessageBatchJob")
private val weatherCareMessageJob: Job,
) {
fun run() {
val jobParameters: JobParameters =
JobParametersBuilder()
.addLong("time", System.currentTimeMillis())
.toJobParameters()
val stopWatch = StopWatch()
stopWatch.start()
jobLauncher.run(weatherCareMessageJob, jobParameters)
stopWatch.stop()
log.info { stopWatch.getTotalTime(TimeUnit.MILLISECONDS) }
}
}
Loading

0 comments on commit f692c44

Please sign in to comment.