Skip to content

Commit

Permalink
♻️ 서킷 브레이커 패턴으로 외부 API 장애 관리 (#206) (#207)
Browse files Browse the repository at this point in the history
* ➕ 서킷 브레이커 관련 의존성 추가 (#206)

* ✨ 서킷 브레이커 관련 환경 설정 추가 (#206)

* ✨ 외부 API, Redis 캐시에 서킷 브레이커 도입 (#206)

* ♻️ 유실물 오프셋 기반으로 다시 변경 (#181)

* 🔧 설정 파일 업데이트 (#206)

* 🐛 컨트롤러 테스트 오류 수정 (#206)
  • Loading branch information
semi-cloud authored Apr 3, 2024
1 parent 14f2564 commit 0b645a4
Show file tree
Hide file tree
Showing 29 changed files with 228 additions and 114 deletions.
Binary file added ahachul_backend/.DS_Store
Binary file not shown.
2 changes: 1 addition & 1 deletion ahachul_backend/ahachul_secret
5 changes: 5 additions & 0 deletions ahachul_backend/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,11 @@ dependencies {
implementation("org.jetbrains.kotlin:kotlin-reflect")
runtimeOnly("com.mysql:mysql-connector-j")
testImplementation("org.springframework.boot:spring-boot-starter-test")

// https://mvnrepository.com/artifact/io.github.resilience4j/resilience4j-spring-boot3
implementation("io.github.resilience4j:resilience4j-spring-boot3:2.0.2")
implementation("org.springframework.boot:spring-boot-starter-actuator")
implementation("org.springframework.boot:spring-boot-starter-aop")
}

tasks.withType<KotlinCompile> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,14 @@ import backend.team.ahachul_backend.api.community.application.port.`in`.Communit
import backend.team.ahachul_backend.common.annotation.Authentication
import backend.team.ahachul_backend.common.response.CommonResponse
import org.springframework.data.domain.Pageable
import org.springframework.web.bind.annotation.DeleteMapping
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PatchMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.bind.annotation.*

@RestController
class CommunityPostController(
private val communityPostUseCase: CommunityPostUseCase
) {

@Authentication
@GetMapping("/v1/community-posts")
fun searchCommunityPosts(
pageable: Pageable,
Expand Down Expand Up @@ -50,4 +45,4 @@ class CommunityPostController(
fun deleteCommunityPost(@PathVariable postId: Long): CommonResponse<DeleteCommunityPostDto.Response> {
return CommonResponse.success(communityPostUseCase.deleteCommunityPost(DeleteCommunityPostCommand(postId)))
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,20 @@ package backend.team.ahachul_backend.api.community.application.service

import backend.team.ahachul_backend.api.community.adapter.web.`in`.dto.post.*
import backend.team.ahachul_backend.api.community.application.port.`in`.CommunityPostUseCase
import backend.team.ahachul_backend.api.community.application.port.out.*
import backend.team.ahachul_backend.api.community.application.port.out.CommunityPostFileReader
import backend.team.ahachul_backend.api.community.application.port.out.CommunityPostHashTagReader
import backend.team.ahachul_backend.api.community.application.port.out.CommunityPostReader
import backend.team.ahachul_backend.api.community.application.port.out.CommunityPostWriter
import backend.team.ahachul_backend.api.community.domain.entity.CommunityPostEntity
import backend.team.ahachul_backend.api.community.domain.entity.CommunityPostFileEntity
import backend.team.ahachul_backend.api.member.application.port.out.MemberReader
import backend.team.ahachul_backend.api.rank.application.service.HashTagRankService
import backend.team.ahachul_backend.api.rank.event.HashTagSearchEvent
import backend.team.ahachul_backend.common.dto.ImageDto
import backend.team.ahachul_backend.common.logging.NamedLogger
import backend.team.ahachul_backend.common.persistence.SubwayLineReader
import backend.team.ahachul_backend.common.support.ViewsSupport
import backend.team.ahachul_backend.common.utils.RequestUtils
import org.springframework.context.ApplicationEventPublisher
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.time.LocalDateTime

@Service
@Transactional(readOnly = true)
Expand All @@ -31,12 +31,13 @@ class CommunityPostService(
private val communityPostHashTagService: CommunityPostHashTagService,
private val communityPostFileService: CommunityPostFileService,

private val viewsSupport: ViewsSupport,
private val publisher: ApplicationEventPublisher
private val viewsSupport: ViewsSupport
): CommunityPostUseCase {

private val logger = NamedLogger("HASHTAG_LOGGER")

override fun searchCommunityPosts(command: SearchCommunityPostCommand): SearchCommunityPostDto.Response {
val memberId = RequestUtils.getAttribute("memberId")!!
val userId = RequestUtils.getAttribute("memberId")!!
val searchCommunityPosts = communityPostReader.searchCommunityPosts(command)
val posts = searchCommunityPosts
.map {
Expand All @@ -50,11 +51,7 @@ class CommunityPostService(
}.toList()

if (isHashTagSearchCond(command.hashTag, command.content)) {
val searchEvent = HashTagSearchEvent(
hashTagName = command.hashTag!!,
userId = memberId
)
publisher.publishEvent(searchEvent)
logger.info("userId = $userId hashtag = ${command.hashTag}")
}

return SearchCommunityPostDto.Response.of(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,10 @@ class LostPostController(

@GetMapping("/v1/lost-posts")
fun searchLostPosts(
pageable: Pageable,
request: SearchLostPostsDto.Request
): CommonResponse<SearchLostPostsDto.Response> {
return CommonResponse.success(lostPostService.searchLostPosts(request.toCommand()))
return CommonResponse.success(lostPostService.searchLostPosts(request.toCommand(pageable)))
}

@Authentication
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import backend.team.ahachul_backend.api.lost.application.service.command.`in`.Se
import backend.team.ahachul_backend.api.lost.domain.model.LostOrigin
import backend.team.ahachul_backend.api.lost.domain.model.LostStatus
import backend.team.ahachul_backend.api.lost.domain.model.LostType
import backend.team.ahachul_backend.common.dto.ImageDto
import org.springframework.data.domain.Pageable

class SearchLostPostsDto {
Expand All @@ -17,14 +16,13 @@ class SearchLostPostsDto {
val lostPostId: Long,
val pageSize: Int,
) {
fun toCommand(): SearchLostPostCommand {
fun toCommand(pageable: Pageable): SearchLostPostCommand {
return SearchLostPostCommand(
lostType = lostType,
subwayLineId = subwayLineId,
lostOrigin = origin,
keyword = keyword,
lostPostId = lostPostId,
pageSize = pageSize
pageable = pageable
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,20 +61,27 @@ class CustomLostPostRepository(
}

fun searchLostPosts(command: GetSliceLostPostsCommand): Slice<LostPostEntity> {
val pageable = PageRequest.of(0, command.pageSize)
val pageable = command.pageable
val response = queryFactory.selectFrom(lostPostEntity)
.where(
ltPostId(command.lostPostId),
lostOriginEq(command.lostOrigin),
subwayLineEq(command.subwayLine),
lostTypeEq(command.lostType),
titleAndContentLike(command.keyword),
)
.orderBy(lostPostEntity.receivedDate.desc())
.limit((command.pageSize + 1).toLong())
.offset(getOffset(pageable).toLong())
.limit((pageable.pageSize + 1).toLong())
.fetch()

return SliceImpl(response, pageable, hasNext(response, command.pageSize))
return SliceImpl(response, pageable, hasNext(response, pageable.pageSize))
}

private fun getOffset(pageable: Pageable): Int {
return when {
pageable.pageNumber != 0 -> pageable.pageNumber * pageable.pageSize
else -> pageable.pageNumber
}
}

private fun hasNext(response: MutableList<LostPostEntity>, pageSize: Int): Boolean {
Expand Down Expand Up @@ -111,9 +118,6 @@ class CustomLostPostRepository(
.fetch()
}

private fun ltPostId(postId: Long?) =
postId?.let { lostPostEntity.id.lt(postId) }

private fun lostOriginEq(lostOrigin: LostOrigin?) =
lostOrigin?.let { lostPostEntity.origin.eq(lostOrigin) }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ package backend.team.ahachul_backend.api.lost.application.service.command.`in`

import backend.team.ahachul_backend.api.lost.domain.model.LostOrigin
import backend.team.ahachul_backend.api.lost.domain.model.LostType
import org.springframework.data.domain.Pageable

class SearchLostPostCommand(
val lostType: LostType,
val lostOrigin: LostOrigin?,
val subwayLineId: Long?,
val keyword: String?,
val lostPostId: Long?,
val pageSize: Int
val pageable: Pageable
)
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ import backend.team.ahachul_backend.api.lost.application.service.command.`in`.Se
import backend.team.ahachul_backend.api.lost.domain.model.LostOrigin
import backend.team.ahachul_backend.api.lost.domain.model.LostType
import backend.team.ahachul_backend.common.domain.entity.SubwayLineEntity
import org.springframework.data.domain.Pageable

class GetSliceLostPostsCommand(
val lostType: LostType,
val lostOrigin: LostOrigin?,
val subwayLine: SubwayLineEntity?,
val keyword: String?,
val lostPostId: Long?,
val pageSize: Int
val pageable: Pageable
) {
companion object {
fun from(command: SearchLostPostCommand, subwayLine: SubwayLineEntity?): GetSliceLostPostsCommand {
Expand All @@ -20,8 +20,7 @@ class GetSliceLostPostsCommand(
lostOrigin = command.lostOrigin,
subwayLine = subwayLine,
keyword = command.keyword,
lostPostId = command.lostPostId,
pageSize = command.pageSize
pageable = command.pageable
)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,31 @@
package backend.team.ahachul_backend.api.rank.application.service

import backend.team.ahachul_backend.api.rank.adapter.web.`in`.dto.GetHashTagRankDto
import backend.team.ahachul_backend.common.constant.CommonConstant.Companion.HASHTAG_REDIS_KEY
import backend.team.ahachul_backend.common.client.RedisClient
import backend.team.ahachul_backend.common.logging.NamedLogger
import backend.team.ahachul_backend.common.config.CircuitBreakerConfig.Companion.CUSTOM_CIRCUIT_BREAKER
import backend.team.ahachul_backend.common.constant.CommonConstant
import backend.team.ahachul_backend.common.constant.CommonConstant.Companion.HASHTAG_REDIS_KEY
import backend.team.ahachul_backend.common.exception.CommonException
import backend.team.ahachul_backend.common.logging.Logger
import backend.team.ahachul_backend.common.response.ResponseCode
import backend.team.ahachul_backend.schedule.job.RankHashTagJob
import com.fasterxml.jackson.core.type.TypeReference
import com.fasterxml.jackson.databind.ObjectMapper
import io.github.resilience4j.circuitbreaker.CallNotPermittedException
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker
import org.springframework.data.redis.RedisConnectionFailureException
import org.springframework.stereotype.Service


@Service
class HashTagRankService(
val redisClient: RedisClient
private val redisClient: RedisClient,
private val rankHashTagJob: RankHashTagJob
){
val logger = NamedLogger("HASHTAG_LOGGER")

fun saveLog(name: String, userId: String) {
logger.info("userId = $userId hashtag = $name")
}
val logger = Logger(javaClass)

@CircuitBreaker(name = CUSTOM_CIRCUIT_BREAKER, fallbackMethod = "fallbackOnRedisCacheGet")
fun getRank(): GetHashTagRankDto.Response {
val mapper = ObjectMapper()

Expand All @@ -30,4 +37,15 @@ class HashTagRankService(

return GetHashTagRankDto.Response(emptyList())
}

fun fallbackOnRedisCacheGet(e: RedisConnectionFailureException): GetHashTagRankDto.Response {
logger.error("circuit breaker opened for redis hashtag cache")
throw CommonException(ResponseCode.FAILED_TO_CONNECT_TO_REDIS, e)
}

fun fallbackOnRedisCacheGet(e : CallNotPermittedException): GetHashTagRankDto.Response {
logger.error("circuit breaker opened for redis hashtag cache")
val rankList = rankHashTagJob.readHashTagLogFile(CommonConstant.HASHTAG_FILE_URL)
return GetHashTagRankDto.Response(rankList)
}
}

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,7 @@ import backend.team.ahachul_backend.api.train.adapter.`in`.dto.GetTrainRealTimes
import backend.team.ahachul_backend.api.train.application.port.`in`.TrainUseCase
import backend.team.ahachul_backend.common.annotation.Authentication
import backend.team.ahachul_backend.common.response.CommonResponse
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.bind.annotation.*

@RestController
class TrainController(
Expand All @@ -21,7 +18,8 @@ class TrainController(
fun getTrain(@PathVariable trainNo: String): CommonResponse<GetTrainDto.Response> {
return CommonResponse.success(trainUseCase.getTrain(trainNo))
}


@Authentication
@GetMapping("/v1/trains/real-times")
fun getTrainRealTimes(request: GetTrainRealTimesDto.Request): CommonResponse<GetTrainRealTimesDto.Response> {
val result = trainUseCase.getTrainRealTimes(request.stationId, request.subwayLineId)
Expand Down
Loading

0 comments on commit 0b645a4

Please sign in to comment.