Skip to content

Commit

Permalink
Merge pull request #179 from YAPP-19th/feature/#140-리팩터링
Browse files Browse the repository at this point in the history
[#140] 리마인드 관련 리팩토링
  • Loading branch information
JuHyun419 authored Sep 12, 2022
2 parents c5057cd + e6aef3f commit 2a55e7d
Show file tree
Hide file tree
Showing 12 changed files with 121 additions and 49 deletions.
15 changes: 11 additions & 4 deletions src/main/kotlin/com/yapp/web2/batch/job/JobCompletionListener.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.yapp.web2.batch.job

import com.yapp.web2.infra.slack.SlackServiceImpl
import com.yapp.web2.infra.slack.SlackService
import org.slf4j.LoggerFactory
import org.springframework.batch.core.BatchStatus
import org.springframework.batch.core.JobExecution
Expand All @@ -9,20 +9,27 @@ import org.springframework.stereotype.Component
import java.util.stream.Collectors

@Component
class JobCompletionListener : JobExecutionListenerSupport() {
class JobCompletionListener(
private val slackApi: SlackService
) : JobExecutionListenerSupport() {

private val log = LoggerFactory.getLogger(javaClass)
private val slackApi: SlackServiceImpl = SlackServiceImpl()

override fun afterJob(jobExecution: JobExecution) {
val jobStatus = jobExecution.status
log.info("Job {} - {}", jobStatus, jobExecution.jobInstance.jobName)

if (jobStatus == BatchStatus.COMPLETED) {
slackApi.sendSlackAlarmToVerbose(
"*`${jobExecution.jobInstance.jobName}`* - executed"
)
}

if (jobStatus != BatchStatus.COMPLETED) {
val errorMessage = String.format(
"*`Batch Error`* - %s", getErrorMessage(jobExecution).replace("\"", "")
)
slackApi.sendMessage(errorMessage)
slackApi.sendSlackAlarm(errorMessage)
}
}

Expand Down
39 changes: 24 additions & 15 deletions src/main/kotlin/com/yapp/web2/batch/job/NotificationJob.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ import org.springframework.batch.core.Job
import org.springframework.batch.core.Step
import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory
import org.springframework.batch.core.configuration.annotation.JobScope
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory
import org.springframework.batch.core.configuration.annotation.StepScope
import org.springframework.batch.core.launch.support.RunIdIncrementer
import org.springframework.batch.item.ItemProcessor
import org.springframework.batch.item.ItemReader
import org.springframework.batch.item.ItemWriter
import org.springframework.batch.item.support.ListItemReader
import org.springframework.context.annotation.Bean
Expand All @@ -21,58 +23,65 @@ import org.springframework.context.annotation.Configuration
class NotificationJob(
private val jobBuilderFactory: JobBuilderFactory,
private val stepBuilderFactory: StepBuilderFactory,
private val notificationService: RemindService,
private val remindService: RemindService,
private val jobCompletionListener: JobCompletionListener
) {

companion object {
const val SEND_NOTIFICATION_JOB = "SEND_NOTIFICATION_JOB"
const val SEND_NOTIFICATION_STEP = "SEND_NOTIFICATION_STEP"
}

private val log = LoggerFactory.getLogger(javaClass)

@Bean("bookmarkNotificationJob")
@Bean
fun bookmarkNotificationJob(): Job {
return jobBuilderFactory.get("bookmarkNotificationJob")
return jobBuilderFactory.get(SEND_NOTIFICATION_JOB)
.start(bookmarkNotificationStep())
.incrementer(RunIdIncrementer())
.listener(jobCompletionListener)
.build()
}

@Bean
@JobScope
fun bookmarkNotificationStep(): Step {
return stepBuilderFactory.get("bookmarkNotificationStep")
return stepBuilderFactory.get(SEND_NOTIFICATION_STEP)
.chunk<Bookmark, Bookmark>(10)
.reader(remindBookmarkReader())
.processor(remindBookmarkProcessor())
.writer(remindBookmarkWriter())
.build()
}

// TODO: ListItemReader의 경우 모든 데이터를 읽고 메모리에 올려두고 사용하기에 데이터가 많을경우 OOM 발생할 가능성이 존재함
@Bean
@StepScope
fun remindBookmarkReader(): ListItemReader<Bookmark> {
val bookmarkOfList = notificationService.getRemindBookmark()

log.info("리마인드 발송할 도토리 갯수: ${bookmarkOfList.size}")

return ListItemReader(bookmarkOfList)
fun remindBookmarkReader(): ItemReader<Bookmark> {
return ListItemReader(remindService.getRemindBookmark())
}

// TODO: Notification 실패 처리 -> Queue(Kafka) 적재 후 Retry 처리
@Bean
@StepScope
fun remindBookmarkProcessor(): ItemProcessor<Bookmark, Bookmark> {
return ItemProcessor {
notificationService.sendNotification(it)
log.info("Bookmark id: ${it.id}, 리마인드 발송 갯수: ${it.remindList.size}")
remindService.sendNotification(it)
it
}
}

// TODO: 코드 수정
@Bean
@StepScope
fun remindBookmarkWriter(): ItemWriter<Bookmark> {
return ItemWriter {
it.stream().forEach { bookmark ->
// bookmark.successRemind()
notificationService.save(bookmark)
log.info("{} -> {} Send Notification", bookmark.userId, bookmark.title)
bookmark.remindList.forEach { remind ->
remind.successRemind()
}
remindService.save(bookmark)
log.info("${bookmark.userId} -> ${bookmark.title} Send Notification")
}
}
}
Expand Down
28 changes: 21 additions & 7 deletions src/main/kotlin/com/yapp/web2/batch/job/TrashRefreshJob.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@ import org.springframework.batch.core.Job
import org.springframework.batch.core.Step
import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory
import org.springframework.batch.core.configuration.annotation.JobScope
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory
import org.springframework.batch.core.configuration.annotation.StepScope
import org.springframework.batch.core.launch.support.RunIdIncrementer
import org.springframework.batch.item.ItemProcessor
import org.springframework.batch.item.ItemReader
import org.springframework.batch.item.ItemWriter
import org.springframework.batch.item.support.ListItemReader
import org.springframework.context.annotation.Bean
Expand All @@ -25,41 +28,52 @@ class TrashRefreshJob(
private val jobCompletionListener: JobCompletionListener
) {

companion object {
const val TRASH_BOOKMARKS_DELETE_JOB = "TRASH_BOOKMARKS_DELETE_JOB"
const val TRASH_BOOKMARKS_DELETE_STEP = "TRASH_BOOKMARKS_DELETE_STEP"
}

private val log = LoggerFactory.getLogger(TrashRefreshJob::class.java)

@Bean("bookmarkTrashRefreshJob")
@Bean
fun bookmarkTrashRefreshJob(): Job {
return jobBuilderFactory.get("bookmarkTrashRefreshJob")
return jobBuilderFactory.get(TRASH_BOOKMARKS_DELETE_JOB)
.start(trashRefreshStep())
.incrementer(RunIdIncrementer())
.listener(jobCompletionListener)
.build()
}

@Bean
@JobScope
fun trashRefreshStep(): Step {
return stepBuilderFactory.get("trashRefreshStep")
.chunk<Bookmark, Bookmark>(10)
return stepBuilderFactory.get(TRASH_BOOKMARKS_DELETE_STEP)
.chunk<Bookmark, Bookmark>(100)
.reader(deleteBookmarkReader())
.processor(deleteBookmarkProcessor())
.writer(NoOperationItemWriter())
.build()
}

@Bean
fun deleteBookmarkReader(): ListItemReader<Bookmark> {
@StepScope
fun deleteBookmarkReader(): ItemReader<Bookmark> {
val deleteCycle = 30L
val deleteBookmarkList = bookmarkRepository.findAllByDeletedIsTrueAndDeleteTimeBefore(
LocalDateTime.now().minusDays(30)
LocalDateTime.now().minusDays(deleteCycle)
)
log.info("휴지통에서 30일이 지난 북마크는 자동으로 삭제합니다. 삭제할 북마크 갯수: ${deleteBookmarkList.size}")

return ListItemReader(deleteBookmarkList)
}

@Bean
@StepScope
fun deleteBookmarkProcessor(): ItemProcessor<Bookmark, Bookmark> {
return ItemProcessor {
log.info("Bookmark to delete info => userId: ${it.userId}, folderId: ${it.folderId}, folderName: ${it.folderName} title: ${it.title}")
log.info("휴지통에서 삭제할 북마크 정보 => " +
"userId: ${it.userId}, folderId: ${it.folderId}, folderName: ${it.folderName} title: ${it.title}"
)
bookmarkRepository.delete(it)
it
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import com.yapp.web2.exception.custom.ExistNameException
import com.yapp.web2.exception.custom.FolderNotRootException
import com.yapp.web2.exception.custom.ImageNotFoundException
import com.yapp.web2.exception.custom.PasswordMismatchException
import com.yapp.web2.infra.slack.SlackService
import com.yapp.web2.security.jwt.JwtProvider
import com.yapp.web2.security.jwt.TokenDto
import com.yapp.web2.util.Message
Expand All @@ -34,7 +35,8 @@ class AccountService(
private val jwtProvider: JwtProvider,
private val s3Client: S3Client,
private val passwordEncoder: PasswordEncoder,
private val mailSender: JavaMailSender
private val mailSender: JavaMailSender,
private val slackApi: SlackService
) {

@Value("\${extension.version}")
Expand Down Expand Up @@ -77,8 +79,16 @@ class AccountService(
isRegistered = false
val newAccount = createUser(account)
folderService.createDefaultFolder(account)

val userCountMessage = """
${newAccount.id}: ${newAccount.name} 님이 회원가입을 진행하였습니다. 현재 회원 수: *`${accountRepository.count()}`*
""".trimIndent()

slackApi.sendSlackAlarmToVerbose(userCountMessage)

newAccount
}

else -> {
log.info("소셜로그인 => ${account.email} 계정이 이미 존재합니다.")
existAccount.fcmToken = account2.fcmToken
Expand All @@ -101,6 +111,12 @@ class AccountService(

log.info("${newAccount.email} account signUp succeed")

val userCountMessage = """
${newAccount.id}: ${newAccount.name} 님이 회원가입을 진행하였습니다. 현재 회원 수: *`${accountRepository.count()}`*
""".trimIndent()

slackApi.sendSlackAlarmToVerbose(userCountMessage)

return Account.AccountLoginSuccess(jwtProvider.createToken(newAccount), newAccount, false)
}

Expand Down Expand Up @@ -200,7 +216,7 @@ class AccountService(
val rootFolder = folderService.findByFolderId(folderId)

if (rootFolder.rootFolderId != folderId) throw FolderNotRootException()
if(!rootFolder.isInviteState()) throw RuntimeException("보관함이 초대잠금상태입니다. 가입할 수 없습니다.")
if (!rootFolder.isInviteState()) throw RuntimeException("보관함이 초대잠금상태입니다. 가입할 수 없습니다.")

val accountFolder = AccountFolder(account, rootFolder)
accountFolder.changeAuthority(Authority.INVITEE)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ class BookmarkController(
@RequestParam(required = false) @ApiParam(value = "북마크가 저장될 폴더 ID", example = "12") folderId: Long?,
@ApiParam(value = "북마크 생성 리스트 정보") @RequestBody dto: BookmarkDto.AddBookmarkListDto
): ResponseEntity<String> {
println(folderId)
val token = ControllerUtil.extractAccessToken(request)
bookmarkService.addBookmarkList(token, folderId, dto)
return ResponseEntity.status(HttpStatus.OK).body(Message.SAVED)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ import io.swagger.annotations.ApiOperation
import io.swagger.annotations.ApiParam
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
import javax.servlet.http.HttpServletRequest

@RestController
Expand All @@ -26,7 +30,6 @@ class BookmarkInterfaceController(
@RequestParam(required = false) @ApiParam(value = "북마크가 저장될 폴더 ID", example = "12") folderId: Long?,
@ApiParam(value = "북마크 생성 리스트 정보") @RequestBody dto: BookmarkDto.AddBookmarkListDto
): ResponseEntity<String> {
println(folderId)
val token = ControllerUtil.extractAccessToken(request)
personalBookmarkService.addBookmarkList(token, folderId, dto)
return ResponseEntity.status(HttpStatus.OK).body(Message.SAVED)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,11 @@ class Remind() {
}

override fun toString(): String {
return StringBuilder().append("Remind [")
return StringBuilder().append("Remind - [")
.append("userId: $userId")
.append("remindTime: $remindTime")
.append("remindCheck: $remindCheck")
.append("remindStatus: $remindStatus")
.append(", remindTime: $remindTime")
.append(", remindCheck: $remindCheck")
.append(", remindStatus: $remindStatus")
.append("]")
.toString()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ interface BookmarkRepository : MongoRepository<Bookmark, String>, MongoTemplateR
fun findByFolderId(id: Long): List<Bookmark>

@Query(value = "{remindList: {\$elemMatch: {remindTime: ?0}}}")
fun findAllBookmarkByRemindTime(today: String): List<Bookmark>
fun findAllBookmarkByRemindTime(today: String): MutableList<Bookmark>

fun findAllByDeletedIsTrueAndDeleteTimeBefore(time: LocalDateTime): List<Bookmark>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import com.yapp.web2.domain.remind.entity.dto.RemindToggleRequest
import com.yapp.web2.exception.custom.AccountNotFoundException
import com.yapp.web2.exception.custom.BookmarkNotFoundException
import com.yapp.web2.infra.fcm.FirebaseService
import com.yapp.web2.infra.slack.SlackService
import com.yapp.web2.security.jwt.JwtProvider
import com.yapp.web2.util.Message
import com.yapp.web2.util.RemindCycleUtil
Expand All @@ -25,7 +26,8 @@ class RemindService(
private val bookmarkRepository: BookmarkRepository,
private val accountRepository: AccountRepository,
private val jwtProvider: JwtProvider,
private val firebaseService: FirebaseService
private val firebaseService: FirebaseService,
private val slackApi: SlackService
) {

companion object {
Expand All @@ -36,24 +38,35 @@ class RemindService(
/**
* 오늘자 기준으로 리마인드 발송 대상인 Bookmark List 조회
*/
fun getRemindBookmark(): List<Bookmark> {
fun getRemindBookmark(): MutableList<Bookmark> {
val today = LocalDate.now().toString()
val remindBookmarkList: List<Bookmark> =
bookmarkRepository.findAllBookmarkByRemindTime(today)
val remindBookmarkList: MutableList<Bookmark> = bookmarkRepository.findAllBookmarkByRemindTime(today)

// 리마인드 발송 시각이 오늘이 아닌 리마인드들은 제거
// 리마인드 발송 대상이 아닌 리마인드 제거(발송 시각이 오늘이 아니거나, 리마인드 발송이 이미 처리된 리마인드)
remindBookmarkList.forEach { bookmark ->
bookmark.remindList.removeIf { remind ->
today != remind.remindTime
(today != remind.remindTime) || remind.remindStatus
}
}
return remindBookmarkList
}

fun sendNotification(bookmark: Bookmark) {
bookmark.remindList.forEach { remind ->
val response = firebaseService.sendMessage(remind.fcmToken, Message.NOTIFICATION_MESSAGE, bookmark.title!!)
log.info("Send notification [userId: ${remind.userId}] succeed, Response => $response")
var response = ""
kotlin.runCatching {
response = firebaseService.sendMessage(remind.fcmToken, Message.NOTIFICATION_MESSAGE, bookmark.title!!)
}.onSuccess {
log.info("리마인드 발송 성공 => [User id: ${remind.userId}], response => $response")
}.onFailure {
val message = """
<!channel> 리마인드 발송 실패 => User id: ${remind.userId}, Bookmark id: ${bookmark.id}\n
response => $response
""".trimIndent()

log.info("리마인드 발송 실패 => [User id: ${remind.userId}, Bookmark id: ${bookmark.id}]\nresponse => $response")
slackApi.sendSlackAlarm(message)
}
}
}

Expand Down
1 change: 1 addition & 0 deletions src/main/kotlin/com/yapp/web2/infra/fcm/FirebaseService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class FirebaseService {

private val log = LoggerFactory.getLogger(javaClass)

@Throws(Exception::class)
fun sendMessage(targetToken: String, title: String, body: String): String {
// 로컬 테스트
// val firebaseInit = FirebaseInit()
Expand Down
6 changes: 4 additions & 2 deletions src/main/kotlin/com/yapp/web2/infra/slack/SlackService.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package com.yapp.web2.infra.slack

interface SlackService {
fun sendMessage(text: String)
fun sendSlackAlarm(text: String)

fun sendMessage(channel: String, text: String)
fun sendSlackAlarmToVerbose(text: String)

fun sendSlackAlarm(channel: String, text: String)
}
Loading

0 comments on commit 2a55e7d

Please sign in to comment.