diff --git a/src/main/kotlin/kr/galaxyhub/sc/translation/infra/DeepLTranslatorClient.kt b/src/main/kotlin/kr/galaxyhub/sc/translation/infra/DeepLTranslatorClient.kt index 9241d3d..246a26d 100644 --- a/src/main/kotlin/kr/galaxyhub/sc/translation/infra/DeepLTranslatorClient.kt +++ b/src/main/kotlin/kr/galaxyhub/sc/translation/infra/DeepLTranslatorClient.kt @@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.annotation.JsonNaming import io.github.oshai.kotlinlogging.KotlinLogging import java.util.UUID import kr.galaxyhub.sc.common.exception.BadRequestException +import kr.galaxyhub.sc.common.exception.GalaxyhubException import kr.galaxyhub.sc.common.exception.InternalServerError import kr.galaxyhub.sc.news.domain.Content import kr.galaxyhub.sc.news.domain.Language @@ -28,11 +29,12 @@ class DeepLTranslatorClient( .uri("/v2/translate") .bodyValue(DeepLRequest(content.language.shortName, targetLanguage.shortName, content.toText())) .retrieve() - .onStatus({ it.is4xxClientError || it.is5xxServerError }) { + .onStatus({ it.isError }) { log.info { "DeepL ErrorResponse=${it.bodyToMono().block()}" } // 에러 응답 확인용. 추후 불필요하면 삭제 - throw handleError(it) + Mono.error(handleError(it)) } .bodyToMono() + .onErrorResume { Mono.error(handleConnectError(it)) } .map { it.toContent(content.newsId, targetLanguage) } } @@ -41,14 +43,16 @@ class DeepLTranslatorClient( */ private fun handleError(clientResponse: ClientResponse): Exception { val statusCode = clientResponse.statusCode() - return when(statusCode.value()) { + return when (statusCode.value()) { HttpStatus.TOO_MANY_REQUESTS.value() -> { BadRequestException("단기간에 너무 많은 요청을 보냈습니다.") } + 456 -> { log.error { "DeepL 할당량이 초과되었습니다." } InternalServerError("할당량이 초과되었습니다. 관리자에게 문의하세요.") } + else -> { log.warn { "DeepL 서버에 일시적 문제가 발생했습니다." } InternalServerError("번역기 서버에 일시적 문제가 발생했습니다.") @@ -56,6 +60,16 @@ class DeepLTranslatorClient( } } + private fun handleConnectError(ex: Throwable): Exception { + return when (ex) { + is GalaxyhubException -> ex + else -> { + log.error(ex) { "DeepL 서버에 연결할 수 없습니다." } + InternalServerError("번역기 서버에 연결할 수 없습니다.") + } + } + } + override fun getProvider(): TranslatorProvider { return TranslatorProvider.DEEPL } @@ -76,7 +90,7 @@ private data class DeepLRequest( val text: List, ) -private data class DeepLResponse( +internal data class DeepLResponse( val translations: List, ) { @@ -102,7 +116,7 @@ private data class DeepLResponse( } @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy::class) -private data class DeepLSentenceResponse( +internal data class DeepLSentenceResponse( val detectedSourceLanguage: String, val text: String, ) diff --git a/src/test/kotlin/kr/galaxyhub/sc/news/fixture/ContentFixture.kt b/src/test/kotlin/kr/galaxyhub/sc/news/fixture/ContentFixture.kt index 0dfb718..078ce8e 100644 --- a/src/test/kotlin/kr/galaxyhub/sc/news/fixture/ContentFixture.kt +++ b/src/test/kotlin/kr/galaxyhub/sc/news/fixture/ContentFixture.kt @@ -14,7 +14,7 @@ object ContentFixture { ), language: Language = Language.ENGLISH, content: String = "blah blah", - newsId: UUID, + newsId: UUID = UUID.randomUUID(), ): Content { return Content( newsInformation = newsInformation, diff --git a/src/test/kotlin/kr/galaxyhub/sc/translation/infra/DeepLTranslatorClientTest.kt b/src/test/kotlin/kr/galaxyhub/sc/translation/infra/DeepLTranslatorClientTest.kt new file mode 100644 index 0000000..8851265 --- /dev/null +++ b/src/test/kotlin/kr/galaxyhub/sc/translation/infra/DeepLTranslatorClientTest.kt @@ -0,0 +1,114 @@ +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 kr.galaxyhub.sc.common.exception.BadRequestException +import kr.galaxyhub.sc.common.exception.InternalServerError +import kr.galaxyhub.sc.common.support.enqueue +import kr.galaxyhub.sc.news.domain.Language +import kr.galaxyhub.sc.news.domain.NewsInformation +import kr.galaxyhub.sc.news.fixture.ContentFixture +import okhttp3.mockwebserver.MockWebServer +import org.springframework.web.reactive.function.client.WebClient + +class DeepLTranslatorClientTest : DescribeSpec({ + + isolationMode = IsolationMode.InstancePerLeaf + + val objectMapper = jacksonObjectMapper() + val mockWebServer = MockWebServer() + val deepLTranslatorClient = DeepLTranslatorClient( + webClient = WebClient.builder() + .baseUrl("${mockWebServer.url("/")}") + .build() + ) + + describe("requestTranslate") { + context("외부 서버가 200 응답을 반환하면") { + val response = DeepLResponse( + listOf( + DeepLSentenceResponse( + detectedSourceLanguage = "EN", + text = "제목입니다." + ), + DeepLSentenceResponse( + detectedSourceLanguage = "EN", + text = "발췌입니다." + ), + DeepLSentenceResponse( + detectedSourceLanguage = "EN", + text = "내용입니다." + ) + ) + ) + mockWebServer.enqueue { + statusCode(200) + body(objectMapper.writeValueAsString(response)) + } + + val expect = deepLTranslatorClient.requestTranslate(ContentFixture.create(), Language.KOREAN).block()!! + + it("번역된 응답이 반환된다.") { + assertSoftly { + expect.newsInformation shouldBe NewsInformation("제목입니다.", "발췌입니다.") + expect.content shouldBe "내용입니다." + } + } + } + + context("외부 서버가 429 응답을 반환하면") { + mockWebServer.enqueue { + statusCode(429) + } + + val expect = deepLTranslatorClient.requestTranslate(ContentFixture.create(), Language.KOREAN) + + it("BadRequestException 예외를 던진다.") { + val ex = shouldThrow { expect.block() } + ex shouldHaveMessage "단기간에 너무 많은 요청을 보냈습니다." + } + } + + context("외부 서버가 456 응답을 반환하면") { + mockWebServer.enqueue { + statusCode(456) + } + + val expect = deepLTranslatorClient.requestTranslate(ContentFixture.create(), Language.KOREAN) + + it("InternalServerError 예외를 던진다.") { + val ex = shouldThrow { expect.block() } + ex shouldHaveMessage "할당량이 초과되었습니다. 관리자에게 문의하세요." + } + } + + context("외부 서버가 그 외 응답을 반환하면") { + mockWebServer.enqueue { + statusCode(500) + } + + val expect = deepLTranslatorClient.requestTranslate(ContentFixture.create(), Language.KOREAN) + + it("InternalServerError 예외를 던진다.") { + val ex = shouldThrow { expect.block() } + ex shouldHaveMessage "번역기 서버에 일시적 문제가 발생했습니다." + } + } + + context("외부 서버에 연결할 수 없으면") { + mockWebServer.shutdown() + + val expect = deepLTranslatorClient.requestTranslate(ContentFixture.create(), Language.KOREAN) + + it("InternalServerError 예외를 던진다.") { + val ex = shouldThrow { expect.block() } + ex shouldHaveMessage "번역기 서버에 연결할 수 없습니다." + } + } + } +})