Skip to content

Commit

Permalink
feat: webClient Timeout 핸들링 추가
Browse files Browse the repository at this point in the history
  • Loading branch information
seokjin8678 committed Dec 30, 2023
1 parent 77fb208 commit 3634680
Show file tree
Hide file tree
Showing 7 changed files with 150 additions and 44 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package kr.galaxyhub.sc.auth.config

import java.time.Duration
import kr.galaxyhub.sc.auth.domain.OAuth2Client
import kr.galaxyhub.sc.auth.domain.OAuth2Clients
import kr.galaxyhub.sc.auth.infra.DiscordOAuth2Client
Expand Down Expand Up @@ -29,7 +30,8 @@ class OAuth2WebClientConfig(
.build(),
clientId = clientId,
clientSecret = clientSecret,
redirectUri = redirectUri
redirectUri = redirectUri,
timeoutDuration = Duration.ofSeconds(5)
)
}

Expand Down
12 changes: 12 additions & 0 deletions src/main/kotlin/kr/galaxyhub/sc/auth/infra/DiscordOAuth2Client.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package kr.galaxyhub.sc.auth.infra
import com.fasterxml.jackson.databind.PropertyNamingStrategies
import com.fasterxml.jackson.databind.annotation.JsonNaming
import io.github.oshai.kotlinlogging.KotlinLogging
import java.time.Duration
import kr.galaxyhub.sc.auth.domain.OAuth2Client
import kr.galaxyhub.sc.common.exception.BadRequestException
import kr.galaxyhub.sc.common.exception.InternalServerError
Expand All @@ -20,11 +21,14 @@ import reactor.core.publisher.Mono

private val log = KotlinLogging.logger {}

private const val TIMEOUT_MESSAGE = "Discord OAuth2 서버의 응답 시간이 초과되었습니다."

class DiscordOAuth2Client(
private val webClient: WebClient,
private val clientId: String,
private val clientSecret: String,
private val redirectUri: String,
private val timeoutDuration: Duration,
) : OAuth2Client {

override fun getAccessToken(code: String): String {
Expand All @@ -35,6 +39,10 @@ class DiscordOAuth2Client(
.onStatus({ it.isError }) { handleAccessTokenError(it) }
.bodyToMono<DiscordAccessTokenResponse>()
.handleConnectError("Discord OAuth2 서버와 연결 중 문제가 발생했습니다.")
.timeout(timeoutDuration, Mono.error {
log.info { TIMEOUT_MESSAGE }
InternalServerError(TIMEOUT_MESSAGE)
})
.block()!!
.accessToken
}
Expand Down Expand Up @@ -73,6 +81,10 @@ class DiscordOAuth2Client(
}
.bodyToMono<DiscordUserInfoResponse>()
.handleConnectError("Discord OAuth2 서버와 연결 중 문제가 발생했습니다.")
.timeout(timeoutDuration, Mono.error {
log.info { TIMEOUT_MESSAGE }
InternalServerError(TIMEOUT_MESSAGE)
})
.block()!!
.toUserInfo()
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package kr.galaxyhub.sc.translation.config

import java.time.Duration
import kr.galaxyhub.sc.translation.domain.TranslatorClient
import kr.galaxyhub.sc.translation.domain.TranslatorClients
import kr.galaxyhub.sc.translation.infra.DeepLTranslatorClient
Expand All @@ -23,7 +24,7 @@ class TranslatorClientConfig(
webClient.mutate()
.baseUrl("https://api.deepl.com")
.defaultHeader(HttpHeaders.AUTHORIZATION, "DeepL-Auth-Key $deepLApiKey")
.build()
.build(), Duration.ofSeconds(5)
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package kr.galaxyhub.sc.translation.infra
import com.fasterxml.jackson.databind.PropertyNamingStrategies
import com.fasterxml.jackson.databind.annotation.JsonNaming
import io.github.oshai.kotlinlogging.KotlinLogging
import java.time.Duration
import java.util.UUID
import kr.galaxyhub.sc.common.exception.BadRequestException
import kr.galaxyhub.sc.common.exception.InternalServerError
Expand All @@ -22,6 +23,7 @@ private val log = KotlinLogging.logger {}

class DeepLTranslatorClient(
private val webClient: WebClient,
private val timeoutDuration: Duration,
) : TranslatorClient {

override fun requestTranslate(content: Content, targetLanguage: Language): Mono<Content> {
Expand All @@ -35,6 +37,10 @@ class DeepLTranslatorClient(
}
.bodyToMono<DeepLResponse>()
.handleConnectError("DeepL 서버와 연결 중 문제가 발생했습니다.")
.timeout(timeoutDuration, Mono.error {
log.info { "DeepL 서버의 응답 시간이 초과되었습니다." }
InternalServerError("DeepL 서버의 응답 시간이 초과되었습니다.")
})
.map { it.toContent(content.newsId, targetLanguage) }
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
package kr.galaxyhub.sc.auth.infra

import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.IsolationMode
import io.kotest.core.spec.style.DescribeSpec
import io.kotest.matchers.shouldBe
import io.kotest.matchers.throwable.shouldHaveMessage
import java.time.Duration
import java.util.concurrent.TimeUnit
import kr.galaxyhub.sc.common.exception.BadRequestException
import kr.galaxyhub.sc.common.exception.InternalServerError
import kr.galaxyhub.sc.common.support.enqueue
Expand All @@ -16,29 +17,30 @@ class DiscordOAuth2ClientTest : DescribeSpec({

isolationMode = IsolationMode.InstancePerLeaf

val objectMapper = jacksonObjectMapper()
val mockWebServer = MockWebServer()
val discordOAuth2Client = DiscordOAuth2Client(
webClient = WebClient.builder()
.baseUrl("${mockWebServer.url("/")}")
.build(),
clientId = "client_id",
clientSecret = "client_secret",
redirectUri = "https://sc.galaxyhub.kr"
redirectUri = "https://sc.galaxyhub.kr",
timeoutDuration = Duration.ofSeconds(10)
)

describe("getAccessToken") {
val response = DiscordAccessTokenResponse(
accessToken = "123123",
tokenType = "Bearer",
expiresIn = 3000,
refreshToken = "321321",
scope = "email"
)

context("외부 서버가 200 응답을 반환하면") {
val response = DiscordAccessTokenResponse(
accessToken = "123123",
tokenType = "Bearer",
expiresIn = 3000,
refreshToken = "321321",
scope = "email"
)
mockWebServer.enqueue {
statusCode(200)
body(objectMapper.writeValueAsString(response))
body(response)
}

val actual = response.accessToken
Expand Down Expand Up @@ -81,22 +83,45 @@ class DiscordOAuth2ClientTest : DescribeSpec({

it("InternalServerError 예외를 던진다.") {
val ex = shouldThrow<InternalServerError> { discordOAuth2Client.getAccessToken("code") }
ex shouldHaveMessage "외부 서버에 연결할 수 없습니다."
ex shouldHaveMessage "Discord OAuth2 서버와 연결 중 문제가 발생했습니다."
}
}

context("외부 서버에 지연이 발생하면") {
val delayClient = DiscordOAuth2Client(
webClient = WebClient.builder()
.baseUrl("${mockWebServer.url("/")}")
.build(),
clientId = "client_id",
clientSecret = "client_secret",
redirectUri = "https://sc.galaxyhub.kr",
timeoutDuration = Duration.ofMillis(100)
)
mockWebServer.enqueue {
statusCode(200)
body(response)
delay(10, TimeUnit.SECONDS)
}

it("InternalServerError 예외를 던진다.") {
val ex = shouldThrow<InternalServerError> { delayClient.getAccessToken("code") }
ex shouldHaveMessage "Discord OAuth2 서버의 응답 시간이 초과되었습니다."
}
}
}

describe("getUserInfo") {
val response = DiscordUserInfoResponse(
id = "12345",
username = "seokjin8678",
email = "[email protected]",
avatar = "avatar",
)

context("외부 서버가 200 응답을 반환하면") {
val response = DiscordUserInfoResponse(
id = "12345",
username = "seokjin8678",
email = "[email protected]",
avatar = "avatar",
)
mockWebServer.enqueue {
statusCode(200)
body(objectMapper.writeValueAsString(response))
body(response)
}

val actual = response.toUserInfo()
Expand Down Expand Up @@ -134,7 +159,29 @@ class DiscordOAuth2ClientTest : DescribeSpec({

it("InternalServerError 예외를 던진다.") {
val ex = shouldThrow<InternalServerError> { discordOAuth2Client.getUserInfo("accessToken") }
ex shouldHaveMessage "외부 서버에 연결할 수 없습니다."
ex shouldHaveMessage "Discord OAuth2 서버와 연결 중 문제가 발생했습니다."
}
}

context("외부 서버에 지연이 발생하면") {
val delayClient = DiscordOAuth2Client(
webClient = WebClient.builder()
.baseUrl("${mockWebServer.url("/")}")
.build(),
clientId = "client_id",
clientSecret = "client_secret",
redirectUri = "https://sc.galaxyhub.kr",
timeoutDuration = Duration.ofMillis(100)
)
mockWebServer.enqueue {
statusCode(200)
body(response)
delay(10, TimeUnit.SECONDS)
}

it("InternalServerError 예외를 던진다.") {
val ex = shouldThrow<InternalServerError> { delayClient.getUserInfo("accessToken") }
ex shouldHaveMessage "Discord OAuth2 서버의 응답 시간이 초과되었습니다."
}
}
}
Expand Down
18 changes: 16 additions & 2 deletions src/test/kotlin/kr/galaxyhub/sc/common/support/MockWebServerDsl.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package kr.galaxyhub.sc.common.support

import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import java.util.concurrent.TimeUnit
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import org.springframework.http.HttpHeaders
Expand All @@ -10,30 +12,42 @@ class MockWebServerDsl {
private var statusCode: Int? = null
private var body: String? = null
private var mediaType: String? = null
private var delay: Long? = null
private var timeUnit: TimeUnit? = null

fun statusCode(statusCode: Int) {
this.statusCode = statusCode
}

fun body(body: String) {
this.body = body
fun body(body: Any) {
this.body = objectMapper.writeValueAsString(body)
}

fun mediaType(mediaType: MediaType) {
this.mediaType = mediaType.toString()
}

fun delay(delay: Long, timeUnit: TimeUnit) {
this.delay = delay
this.timeUnit = timeUnit
}

internal fun perform(mockWebServer: MockWebServer) {
val response = MockResponse()
statusCode?.also { response.setResponseCode(it) }
body?.also { response.setBody(it) }
delay?.also { response.setBodyDelay(it, timeUnit!!) }
if (mediaType != null) {
response.addHeader(HttpHeaders.CONTENT_TYPE, mediaType!!)
} else {
response.addHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON)
}
mockWebServer.enqueue(response)
}

companion object {
val objectMapper = jacksonObjectMapper()
}
}

fun MockWebServer.enqueue(dsl: MockWebServerDsl.() -> Unit = {}) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
package kr.galaxyhub.sc.translation.infra

import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import io.kotest.assertions.assertSoftly
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.IsolationMode
import io.kotest.core.spec.style.DescribeSpec
import io.kotest.matchers.shouldBe
import io.kotest.matchers.throwable.shouldHaveMessage
import java.time.Duration
import java.util.concurrent.TimeUnit
import kr.galaxyhub.sc.common.exception.BadRequestException
import kr.galaxyhub.sc.common.exception.InternalServerError
import kr.galaxyhub.sc.common.support.enqueue
Expand All @@ -20,35 +21,36 @@ class DeepLTranslatorClientTest : DescribeSpec({

isolationMode = IsolationMode.InstancePerLeaf

val objectMapper = jacksonObjectMapper()
val mockWebServer = MockWebServer()
val deepLTranslatorClient = DeepLTranslatorClient(
webClient = WebClient.builder()
.baseUrl("${mockWebServer.url("/")}")
.build()
.build(),
timeoutDuration = Duration.ofSeconds(10)
)

describe("requestTranslate") {
context("외부 서버가 200 응답을 반환하면") {
val response = DeepLResponse(
listOf(
DeepLSentenceResponse(
detectedSourceLanguage = "EN",
text = "제목입니다."
),
DeepLSentenceResponse(
detectedSourceLanguage = "EN",
text = "발췌입니다."
),
DeepLSentenceResponse(
detectedSourceLanguage = "EN",
text = "내용입니다."
)
val response = DeepLResponse(
listOf(
DeepLSentenceResponse(
detectedSourceLanguage = "EN",
text = "제목입니다."
),
DeepLSentenceResponse(
detectedSourceLanguage = "EN",
text = "발췌입니다."
),
DeepLSentenceResponse(
detectedSourceLanguage = "EN",
text = "내용입니다."
)
)
)

context("외부 서버가 200 응답을 반환하면") {
mockWebServer.enqueue {
statusCode(200)
body(objectMapper.writeValueAsString(response))
body(response)
}

val expect = deepLTranslatorClient.requestTranslate(ContentFixture.create(), Language.KOREAN).block()!!
Expand Down Expand Up @@ -107,7 +109,29 @@ class DeepLTranslatorClientTest : DescribeSpec({

it("InternalServerError 예외를 던진다.") {
val ex = shouldThrow<InternalServerError> { expect.block() }
ex shouldHaveMessage "번역기 서버에 연결할 수 없습니다."
ex shouldHaveMessage "DeepL 서버와 연결 중 문제가 발생했습니다."
}
}

context("외부 서버에 지연이 발생하면") {
val delayClient = DeepLTranslatorClient(
webClient = WebClient.builder()
.baseUrl("${mockWebServer.url("/")}")
.build(),
timeoutDuration = Duration.ofMillis(100)
)

mockWebServer.enqueue {
statusCode(200)
body(response)
delay(1, TimeUnit.SECONDS)
}

val expect = delayClient.requestTranslate(ContentFixture.create(), Language.KOREAN)

it("InternalServerError 예외를 던진다.") {
val ex = shouldThrow<InternalServerError> { expect.block() }
ex shouldHaveMessage "DeepL 서버의 응답 시간이 초과되었습니다."
}
}
}
Expand Down

0 comments on commit 3634680

Please sign in to comment.