From f670970cc7e5faa1e5b9f273fd2d7bcc8e1959ea Mon Sep 17 00:00:00 2001 From: Stark-Industries0417 <59994664+Stark-Industries0417@users.noreply.github.com> Date: Fri, 3 Jan 2025 17:38:16 +0900 Subject: [PATCH 1/7] =?UTF-8?q?DELETE=20-=20jvm-sdk/=20=EC=BD=94=EB=A3=A8?= =?UTF-8?q?=ED=8B=B4=20=EB=9D=BC=EC=9D=B4=EB=B8=8C=EB=9F=AC=EB=A6=AC=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- jvm-sdk/build.gradle.kts | 2 -- 1 file changed, 2 deletions(-) diff --git a/jvm-sdk/build.gradle.kts b/jvm-sdk/build.gradle.kts index b11ed8b..fa8268e 100644 --- a/jvm-sdk/build.gradle.kts +++ b/jvm-sdk/build.gradle.kts @@ -15,6 +15,4 @@ dependencies { implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:$jacksonVersion") implementation("com.fasterxml.jackson.module:jackson-module-kotlin:$jacksonVersion") - // coroutine - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3") } \ No newline at end of file From b938674931afcbd9c589dad130f027771bedacd9 Mon Sep 17 00:00:00 2001 From: Stark-Industries0417 <59994664+Stark-Industries0417@users.noreply.github.com> Date: Fri, 3 Jan 2025 17:40:13 +0900 Subject: [PATCH 2/7] =?UTF-8?q?FEAT=20-=20jvm-sdk/=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EC=9E=90=EC=97=90=EA=B2=8C=20=EB=84=A4=ED=8A=B8=EC=9B=8C?= =?UTF-8?q?=ED=81=AC=20=EC=84=A4=EC=A0=95=20=EA=B8=B0=EB=8A=A5=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gamedatahub/network/JvmNetworkClient.kt | 89 ++++++++++--------- 1 file changed, 49 insertions(+), 40 deletions(-) diff --git a/jvm-sdk/src/main/kotlin/com/gamedatahub/network/JvmNetworkClient.kt b/jvm-sdk/src/main/kotlin/com/gamedatahub/network/JvmNetworkClient.kt index 781190b..8b9dbf5 100644 --- a/jvm-sdk/src/main/kotlin/com/gamedatahub/network/JvmNetworkClient.kt +++ b/jvm-sdk/src/main/kotlin/com/gamedatahub/network/JvmNetworkClient.kt @@ -1,62 +1,71 @@ package com.gamedatahub.network -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory +import com.fasterxml.jackson.module.kotlin.KotlinModule import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient import okhttp3.Request -import kotlinx.coroutines.suspendCancellableCoroutine -import okhttp3.* import okhttp3.RequestBody.Companion.toRequestBody +import java.io.File import java.io.IOException -import kotlin.coroutines.resume -import kotlin.coroutines.resumeWithException -class JvmNetworkClient( - private val client: OkHttpClient = OkHttpClient() +data class NetworkClientConfig( + var isRetryEnabled: Boolean = true, + var maxRetries: Int = 2, + var retryDelayMillis: Long = 1000, + var backoffFactor: Double = 2.0 +) + +class JvmNetworkClient private constructor( + private val client: OkHttpClient, + val config: NetworkClientConfig, ) : NetworkClient { - private val scope = CoroutineScope(Dispatchers.IO) override fun postDataAsync(url: String, data: String) { - scope.launch { - try { - val response = client.makePostRequest(url, data) - TODO("성공 핸들링") - } catch (e: Exception) { - TODO("실패 핸들링") + client.makePostRequest(url, data) + } + + private fun OkHttpClient.makePostRequest(url: String, data: String) { + val requestBody = data.toRequestBody("application/json".toMediaType()) + + val request = Request.Builder() + .url(url) + .post(requestBody) + .build() + + this.newCall(request).execute().use { response -> + if (!response.isSuccessful) { + throw IOException("HTTP: ${response.code} - ${response.message}") } } } -} - -private suspend fun OkHttpClient.makePostRequest(url: String, data: String): String { - val requestBody = data.toRequestBody("application/json".toMediaType()) - val request = Request.Builder() - .url(url) - .post(requestBody) - .build() - - return suspendCancellableCoroutine { continuation -> - val call = this.newCall(request) - - call.enqueue(object : Callback { - override fun onResponse(call: Call, response: Response) { - if (response.isSuccessful) { - continuation.resume(response.body?.string() ?: "") + + class Builder { + private var client: OkHttpClient = OkHttpClient() + private var config: NetworkClientConfig = NetworkClientConfig() + + private val yamlMapper: ObjectMapper = ObjectMapper(YAMLFactory()) + .registerModule(KotlinModule()) + + fun loadFromYaml(filePath: String = "config.yml") = + apply { + val file = File(filePath) + config = if (!file.exists()) { + NetworkClientConfig() } else { - continuation.resumeWithException(Exception("Error: ${response.code} - ${response.message}")) + yamlMapper.readValue(file, NetworkClientConfig::class.java) } - response.close() } - override fun onFailure(call: Call, e: IOException) { - continuation.resumeWithException(e) - } - }) + fun httpClient(client: OkHttpClient) = apply { this.client = client } + fun enableRetry(isEnabled: Boolean) = apply { this.config = this.config.copy(isRetryEnabled = isEnabled) } + fun maxRetries(maxRetries: Int) = apply { this.config = this.config.copy(maxRetries = maxRetries) } + fun retryDelayMillis(delayMillis: Long) = apply { this.config = this.config.copy(retryDelayMillis = delayMillis) } + fun backoffFactor(factor: Double) = apply { this.config = this.config.copy(backoffFactor = factor) } - continuation.invokeOnCancellation { - call.cancel() + fun build(): JvmNetworkClient { + return JvmNetworkClient(client, config) } } } \ No newline at end of file From bc1a912105a52639150e10961942a6418a49250c Mon Sep 17 00:00:00 2001 From: Stark-Industries0417 <59994664+Stark-Industries0417@users.noreply.github.com> Date: Fri, 3 Jan 2025 17:40:23 +0900 Subject: [PATCH 3/7] =?UTF-8?q?FEAT=20-=20jvm-sdk/=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EC=9E=90=EC=97=90=EA=B2=8C=20=EB=84=A4=ED=8A=B8=EC=9B=8C?= =?UTF-8?q?=ED=81=AC=20=EC=84=A4=EC=A0=95=20=EA=B8=B0=EB=8A=A5=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dummy/src/test/kotlin/MainIntegrationTest.kt | 32 +++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/dummy/src/test/kotlin/MainIntegrationTest.kt b/dummy/src/test/kotlin/MainIntegrationTest.kt index db13750..b3ec55d 100644 --- a/dummy/src/test/kotlin/MainIntegrationTest.kt +++ b/dummy/src/test/kotlin/MainIntegrationTest.kt @@ -8,6 +8,7 @@ import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import serialization.JacksonSerializer +import java.io.File class MainIntegrationTest { @@ -21,7 +22,7 @@ class MainIntegrationTest { mockWebServer = MockWebServer() mockWebServer.start() - networkClient = JvmNetworkClient() + networkClient = JvmNetworkClient.Builder().build() jsonSerializer = JacksonSerializer(clazz = User::class.java) dataCollector = DataCollector(networkClient, jsonSerializer) } @@ -55,4 +56,33 @@ class MainIntegrationTest { assertEquals("POST", request.method, "HTTP 메서드가 올바르지 않습니다.") assertTrue(request.body.readUtf8().contains("Test User"), "전송된 데이터가 올바르지 않습니다.") } + + @Test + fun `config yml 파일 설정이 제대로 적용된다`() { + val expectedEnabled = true + val expectedRetries = 3 + val expectedDelayMillis = 2000L + + val networkYml = """ + isRetryEnabled: $expectedEnabled + maxRetries: $expectedRetries + retryDelayMillis: $expectedDelayMillis + backoffFactor: 1.5 + """.trimIndent() + + val configFile = File("network.yml") + configFile.writeText(networkYml) + + val client = JvmNetworkClient.Builder() + .loadFromYaml("network.yml") + .build() + + val config = client.config + + assertEquals(expectedEnabled, config.isRetryEnabled, "재시도 설정이 올바르게 설정되지 않았습니다.") + assertEquals(expectedRetries, config.maxRetries, "재시도 횟수가 올바르게 설정되지 않았습니다.") + assertEquals(expectedDelayMillis, config.retryDelayMillis, "재시도 딜레이 값이 올바르게 설정되지 않았습니다.") + + configFile.delete() + } } \ No newline at end of file From 0f2f5cd04f1a8d3c31510c665dc750ea93c59476 Mon Sep 17 00:00:00 2001 From: Stark-Industries0417 <59994664+Stark-Industries0417@users.noreply.github.com> Date: Tue, 7 Jan 2025 13:55:51 +0900 Subject: [PATCH 4/7] =?UTF-8?q?FEAT=20-=20jvm-sdk/=20OkHttpClient=20http?= =?UTF-8?q?=20request=20=EB=B9=84=EB=8F=99=EA=B8=B0=20=ED=98=B8=EC=B6=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gamedatahub/network/JvmNetworkClient.kt | 37 +++++++++++++++---- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/jvm-sdk/src/main/kotlin/com/gamedatahub/network/JvmNetworkClient.kt b/jvm-sdk/src/main/kotlin/com/gamedatahub/network/JvmNetworkClient.kt index 8b9dbf5..ab7958a 100644 --- a/jvm-sdk/src/main/kotlin/com/gamedatahub/network/JvmNetworkClient.kt +++ b/jvm-sdk/src/main/kotlin/com/gamedatahub/network/JvmNetworkClient.kt @@ -23,22 +23,45 @@ class JvmNetworkClient private constructor( ) : NetworkClient { override fun postDataAsync(url: String, data: String) { - client.makePostRequest(url, data) + client.makePostRequestAsync(url, data) { success, error -> + if (success != null) { + println("Request succeeded: $success") + } else { + println("Request failed: ${error?.message}") + } + } } - private fun OkHttpClient.makePostRequest(url: String, data: String) { - val requestBody = data.toRequestBody("application/json".toMediaType()) + private fun OkHttpClient.makePostRequestAsync( + url: String, + data: String, + callback: (success: String?, error: Throwable?) -> Unit + ) { + val requestBody = data.toRequestBody("application/json".toMediaType()) val request = Request.Builder() .url(url) .post(requestBody) .build() - this.newCall(request).execute().use { response -> - if (!response.isSuccessful) { - throw IOException("HTTP: ${response.code} - ${response.message}") + this.newCall(request).enqueue(object : okhttp3.Callback { + override fun onFailure(call: okhttp3.Call, e: IOException) { + callback(null, e) } - } + + override fun onResponse(call: okhttp3.Call, response: okhttp3.Response) { + response.use { + if (response.isSuccessful) { + callback(response.body?.string(), null) + } else { + callback( + null, + IOException("HTTP ${response.code}: ${response.message}") + ) + } + } + } + }) } class Builder { From 078981bb356cecc3e52260855ebf69ac789c3790 Mon Sep 17 00:00:00 2001 From: Stark-Industries0417 <59994664+Stark-Industries0417@users.noreply.github.com> Date: Tue, 7 Jan 2025 14:30:50 +0900 Subject: [PATCH 5/7] =?UTF-8?q?FEAT=20-=20jvm-sdk/=20=EB=84=A4=ED=8A=B8?= =?UTF-8?q?=EC=9B=8C=ED=81=AC=20=EC=9A=94=EC=B2=AD=20=EC=8B=A4=ED=8C=A8=20?= =?UTF-8?q?=EC=8B=9C=20=EC=9E=AC=EC=8B=9C=EB=8F=84=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EA=B0=9C=EB=B0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gamedatahub/network/JvmNetworkClient.kt | 35 +++++++++---------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/jvm-sdk/src/main/kotlin/com/gamedatahub/network/JvmNetworkClient.kt b/jvm-sdk/src/main/kotlin/com/gamedatahub/network/JvmNetworkClient.kt index ab7958a..d7bd8a3 100644 --- a/jvm-sdk/src/main/kotlin/com/gamedatahub/network/JvmNetworkClient.kt +++ b/jvm-sdk/src/main/kotlin/com/gamedatahub/network/JvmNetworkClient.kt @@ -23,20 +23,12 @@ class JvmNetworkClient private constructor( ) : NetworkClient { override fun postDataAsync(url: String, data: String) { - client.makePostRequestAsync(url, data) { success, error -> - if (success != null) { - println("Request succeeded: $success") - } else { - println("Request failed: ${error?.message}") - } - } + client.makePostRequestAsync(url, data) } - private fun OkHttpClient.makePostRequestAsync( url: String, - data: String, - callback: (success: String?, error: Throwable?) -> Unit + data: String ) { val requestBody = data.toRequestBody("application/json".toMediaType()) val request = Request.Builder() @@ -44,21 +36,26 @@ class JvmNetworkClient private constructor( .post(requestBody) .build() + val maxAttempts = config.maxRetries + var attempt = 0 + var delay = config.retryDelayMillis + this.newCall(request).enqueue(object : okhttp3.Callback { override fun onFailure(call: okhttp3.Call, e: IOException) { - callback(null, e) + attempt++ + if (attempt <= maxAttempts && config.isRetryEnabled) { + Thread.sleep(delay) + delay = (delay * config.backoffFactor).toLong() + makePostRequestAsync(url, data) + } else { + println("Request failed after ${attempt} attempts: ${e.message}") + } } override fun onResponse(call: okhttp3.Call, response: okhttp3.Response) { response.use { - if (response.isSuccessful) { - callback(response.body?.string(), null) - } else { - callback( - null, - IOException("HTTP ${response.code}: ${response.message}") - ) - } + if (!response.isSuccessful) + onFailure(call, IOException("HTTP ${response.code}: ${response.message}")) } } }) From 01a9ce3619423100d678ad5e9169877127257e03 Mon Sep 17 00:00:00 2001 From: Stark-Industries0417 <59994664+Stark-Industries0417@users.noreply.github.com> Date: Tue, 7 Jan 2025 14:31:43 +0900 Subject: [PATCH 6/7] =?UTF-8?q?UPDATE=20-=20jvm-sdk/=20=EB=84=A4=ED=8A=B8?= =?UTF-8?q?=EC=9B=8C=ED=81=AC=20config=20=EB=8F=99=EC=8B=9C=EC=84=B1=20?= =?UTF-8?q?=EC=9D=B4=EC=8A=88=EB=A1=9C=20=EA=B0=9D=EC=B2=B4=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=ED=8D=BC=ED=8B=B0=20=EB=B6=88=EB=B3=80=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/com/gamedatahub/network/JvmNetworkClient.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/jvm-sdk/src/main/kotlin/com/gamedatahub/network/JvmNetworkClient.kt b/jvm-sdk/src/main/kotlin/com/gamedatahub/network/JvmNetworkClient.kt index d7bd8a3..3b96776 100644 --- a/jvm-sdk/src/main/kotlin/com/gamedatahub/network/JvmNetworkClient.kt +++ b/jvm-sdk/src/main/kotlin/com/gamedatahub/network/JvmNetworkClient.kt @@ -11,10 +11,10 @@ import java.io.File import java.io.IOException data class NetworkClientConfig( - var isRetryEnabled: Boolean = true, - var maxRetries: Int = 2, - var retryDelayMillis: Long = 1000, - var backoffFactor: Double = 2.0 + val isRetryEnabled: Boolean = true, + val maxRetries: Int = 2, + val retryDelayMillis: Long = 1000, + val backoffFactor: Double = 2.0 ) class JvmNetworkClient private constructor( From b67c54ef44e7f6371ec5d569b9a173cd90b7fca1 Mon Sep 17 00:00:00 2001 From: Stark-Industries0417 <59994664+Stark-Industries0417@users.noreply.github.com> Date: Wed, 8 Jan 2025 14:44:45 +0900 Subject: [PATCH 7/7] =?UTF-8?q?FEAT=20-=20jvm-sdk/=20=EC=9E=AC=EC=8B=9C?= =?UTF-8?q?=EB=8F=84=20=EB=A1=9C=EC=A7=81=20=EB=B0=8F=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EA=B8=B0=EB=8A=A5=20=EA=B0=9C=EB=B0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dummy/src/test/kotlin/MainIntegrationTest.kt | 52 ++++++++++++++++++- .../gamedatahub/network/JvmNetworkClient.kt | 17 +++--- 2 files changed, 60 insertions(+), 9 deletions(-) diff --git a/dummy/src/test/kotlin/MainIntegrationTest.kt b/dummy/src/test/kotlin/MainIntegrationTest.kt index b3ec55d..f7baada 100644 --- a/dummy/src/test/kotlin/MainIntegrationTest.kt +++ b/dummy/src/test/kotlin/MainIntegrationTest.kt @@ -1,6 +1,7 @@ import com.gamedatahub.network.JvmNetworkClient import com.gamedatahub.datacollection.DataCollector import com.gamedatahub.serialization.JsonSerializer +import okhttp3.OkHttpClient import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockWebServer import org.junit.jupiter.api.AfterEach @@ -9,6 +10,7 @@ import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import serialization.JacksonSerializer import java.io.File +import java.util.concurrent.TimeUnit class MainIntegrationTest { @@ -22,7 +24,12 @@ class MainIntegrationTest { mockWebServer = MockWebServer() mockWebServer.start() - networkClient = JvmNetworkClient.Builder().build() + networkClient = JvmNetworkClient.Builder() + .enableRetry(true) + .maxRetries(3) + .retryDelayMillis(10) + .backoffFactor(2.0) + .build() jsonSerializer = JacksonSerializer(clazz = User::class.java) dataCollector = DataCollector(networkClient, jsonSerializer) } @@ -85,4 +92,47 @@ class MainIntegrationTest { configFile.delete() } + + @Test + fun `서버 실패 시 정해진 횟수만큼 재시도 후 실패한다`() { + repeat(networkClient.config.maxRetries) { + mockWebServer.enqueue(MockResponse().setResponseCode(500)) + } + + val serverUrl = mockWebServer.url("/test").toString() + networkClient.postDataAsync(serverUrl, "{ \"key\": \"value\" }") + + Thread.sleep(200) + + assertEquals(4, mockWebServer.requestCount, "요청 횟수가 재시도 설정에 맞지 않습니다.") + } + + @Test + fun `재시도 중 서버가 성공하면 요청이 종료된다`() { + repeat(networkClient.config.maxRetries - 1) { + mockWebServer.enqueue(MockResponse().setResponseCode(500)) + } + mockWebServer.enqueue(MockResponse().setResponseCode(200)) + + val serverUrl = mockWebServer.url("/test").toString() + networkClient.postDataAsync(serverUrl, "{ \"key\": \"value\" }") + + Thread.sleep(200) + + assertEquals(3, mockWebServer.requestCount, "재시도 중간에 요청이 성공하지 않았습니다.") + } + + @Test + fun `재시도 설정이 비활성화된 경우 한 번만 요청한다`() { + val clientWithoutRetry = JvmNetworkClient.Builder() + .enableRetry(false) + .build() + mockWebServer.enqueue(MockResponse().setResponseCode(500)) + + val serverUrl = mockWebServer.url("/test").toString() + clientWithoutRetry.postDataAsync(serverUrl, "{ \"key\": \"value\" }") + + Thread.sleep(200) + assertEquals(1, mockWebServer.requestCount, "비활성화된 재시도에서 요청 횟수가 잘못되었습니다.") + } } \ No newline at end of file diff --git a/jvm-sdk/src/main/kotlin/com/gamedatahub/network/JvmNetworkClient.kt b/jvm-sdk/src/main/kotlin/com/gamedatahub/network/JvmNetworkClient.kt index 3b96776..042510f 100644 --- a/jvm-sdk/src/main/kotlin/com/gamedatahub/network/JvmNetworkClient.kt +++ b/jvm-sdk/src/main/kotlin/com/gamedatahub/network/JvmNetworkClient.kt @@ -11,8 +11,8 @@ import java.io.File import java.io.IOException data class NetworkClientConfig( - val isRetryEnabled: Boolean = true, - val maxRetries: Int = 2, + val isRetryEnabled: Boolean = false, + val maxRetries: Int = 1, val retryDelayMillis: Long = 1000, val backoffFactor: Double = 2.0 ) @@ -28,7 +28,8 @@ class JvmNetworkClient private constructor( private fun OkHttpClient.makePostRequestAsync( url: String, - data: String + data: String, + attempt: Int = 0 ) { val requestBody = data.toRequestBody("application/json".toMediaType()) val request = Request.Builder() @@ -37,18 +38,18 @@ class JvmNetworkClient private constructor( .build() val maxAttempts = config.maxRetries - var attempt = 0 var delay = config.retryDelayMillis this.newCall(request).enqueue(object : okhttp3.Callback { override fun onFailure(call: okhttp3.Call, e: IOException) { - attempt++ - if (attempt <= maxAttempts && config.isRetryEnabled) { + var tmpAttempt = attempt + tmpAttempt++ + if (tmpAttempt <= maxAttempts && config.isRetryEnabled) { Thread.sleep(delay) delay = (delay * config.backoffFactor).toLong() - makePostRequestAsync(url, data) + makePostRequestAsync(url, data, tmpAttempt) } else { - println("Request failed after ${attempt} attempts: ${e.message}") + println("Request failed after ${tmpAttempt} attempts: ${e.message}") } }