AWS Kotlin SDK - java.lang.NoSuchMethodError

Falcon·2024년 5월 25일

jvm

목록 보기
1/1
post-thumbnail

문제 상황

AWS SDK Kotlin 으로 S3 GetObject 메소드 예제를 그대로 사용했는데 NoSuchMethodError 가 발생했다.

suspend fun getObjectBytes(bucketName: String, keyName: String, path: String) {
    val request = GetObjectRequest {
        key = keyName
        bucket = bucketName
    }

    S3Client { region = "us-east-1" }.use { s3 ->
        s3.getObject(request) { resp ->
            val myFile = File(path)
            resp.body?.writeToFile(myFile)
            println("Successfully read $keyName from $bucketName")
        }
    }
}

12:40:12.053 [Test worker @kotlinx.coroutines.test runner#2] ERROR aws.sdk.kotlin.runtime.auth.credentials.SsoTokenProvider -rpc="S3.GetObject" sdkInvocationId="ff9dc240-6f91-416f-b626-bf086b9675a8"- token refresh failed
java.lang.NoSuchMethodError: 'okhttp3.Request$Builder okhttp3.Request$Builder.tag(kotlin.reflect.KClass, java.lang.Object)'
	at aws.smithy.kotlin.runtime.http.engine.okhttp.OkHttpUtilsKt.toOkHttpRequest(OkHttpUtils.kt:51)
	at aws.smithy.kotlin.runtime.http.engine.okhttp.OkHttpEngine.roundTrip(OkHttpEngine.kt:51)

재현

plugins {
	id("org.springframework.boot") version "3.2.5"
	id("io.spring.dependency-management") version "1.1.4"
	id("com.google.cloud.tools.jib") version "3.4.2"
	kotlin("plugin.spring") version "1.9.24"
	kotlin("jvm") version "1.9.24"
}
java {
	sourceCompatibility = JavaVersion.VERSION_17
	targetCompatibility = JavaVersion.VERSION_17
}

repositories {
	mavenCentral()
}

dependencies {
	implementation("org.springframework.boot:spring-boot-starter-web")
    // Error occurs from here
	implementation("aws.sdk.kotlin:s3:1.2.15")
    
	implementation("org.slf4j:slf4j-api:2.0.12")
	implementation("org.jetbrains.kotlin:kotlin-reflect")
	implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
}

문제 원인

okhttp3 의 Request 클래스 tag 메소드를 찾을 수 없다는 에러다.

 'okhttp3.Request$Builder okhttp3.Request$Builder.tag(kotlin.reflect.KClass, java.lang.Object)'

OkHttpUtils.kt

internal fun HttpRequest.toOkHttpRequest(
    execContext: ExecutionContext,
    callContext: CoroutineContext,
    metrics: HttpClientMetrics,
): OkHttpRequest {
    val builder = OkHttpRequest.Builder()
    // ⚠️ `tag` NoSuchMethod
    builder.tag(SdkRequestTag::class, SdkRequestTag(execContext, callContext, metrics))
    builder.url(url.toString())
    builder.headers(headers.toOkHttpHeaders())

Request.kt

// ⚠️ Error occurs
    open fun <T> tag(type: Class<in T>, tag: T?) = apply {
      if (tag == null) {
        tags.remove(type)
      } else {
        if (tags.isEmpty()) {
          tags = mutableMapOf()
        }
        tags[type] = type.cast(tag)!! // Force-unwrap due to lack of contracts on Class#cast()
      }
    }

문제 원인이 된 클래스 파일은 OKHttpUtils.kt 에서 Request.kttag() 메소드를 찾지 못한다는 메시지로 이렇게 추론할 수 있다.

Request.kt 를 정의한 package 버전(ok과 호환되지 않는다.

해결 방법

❌ (1) Transitive dependency 조건 추가

Gradle Transitive Dependencies

	implementation("aws.sdk.kotlin:s3:1.2.15") {
    // kotlin sdk 에서 의존하는 okhttp 최소 버전을 5.0 버전대로 변경
		constraints {
			implementation("com.squareup.okhttp3:okhttp:5.0.0-alpha.14") {
				because("okhttp3 ~v4 does not support Request builder (kotlin reflect)")
			}
		}
	}

constraints 문으로 최소 의존 버전을 4.12.0 대신 5.0.0-alpha.14 로 변경했다.

❌ 이 방법은 통하지 않는다.
여전히 okhttp:4.12.0 버전을 의존한다.

왜 해결이 안될까?

alpha 버전이기 때문이다.
gradle 은 Transitive dependency resolution 중에는 Stable release 된 버전 중 가장 최신 버전을 우선으로한다.
alpha 버전은 Dependency resolution 과정에서 탈락한다.

4.12.0 이 현재 Stable release 중 가장 최신버전이다.
5.0.x 버전은 alpha 버전으로 stable release 에 해당하지 않아서 탈락된다.

✅ (2) Directive Dependency 명시

Transitive 가 아닌 직접 사용할 okhttp 의 의존 버전을 명시한다.

	implementation("aws.sdk.kotlin:s3:1.2.15") {
		constraints {
			implementation("com.squareup.okhttp3:okhttp:5.0.0-alpha.14") {
				because("okhttp3 ~v4 does not support Request builder (kotlin reflect)")
			}
		}
	}
  // Directive Dependency
  // AWS SDK constraints 와 무관하게 `okhttp3:5.0.0` 버전 사용
	implementation("com.squareup.okhttp3:okhttp:5.0.0-alpha.14")

✅ 이 방법은 통한다.

왜 통했을까?

Directive Dependency Resolution 에서는 Stable release 여부와 관계 없이 직접 해당 버전을 사용한다.

Transitive , Directive Dependency 가 둘다 있는 경우 Directive 가 우선한다.

✅ (3) Resolution Strategy 명시

	implementation("aws.sdk.kotlin:s3:1.2.15") {
    // transitive dependency does not ensure unstable release
		constraints {
			implementation("com.squareup.okhttp3:okhttp:5.0.0-alpha.14") {
				because("okhttp3 ~v4 does not support Request builder (kotlin reflect)")
			}
		}
	}
    // Enforce specific version when to resolve dependencies
	configurations.all {
		resolutionStrategy.eachDependency {
			if (requested.group == "com.squareup.okhttp3" && requested.name == "okhttp") {
				useVersion("5.0.0-alpha.14")
				because("okhttp3 ~v4 does not support Request builder (kotlin reflect) on AWS SDK")
			}
		}
	}

Transitive Dependency 라 하더라도 사용 버전을 강제로 5.0.0-alpha.14 로 맞춘다.
좀더 복잡한 의존성 설정이 가능하다.

Directive (2) vs ResolutionStrategy (3) 중에 뭘 쓰지?

(2) 번이 더 간단하고 명확하다.
(3) 은 Transitive Dependency 까지 복잡하게 설정되어 있을 때 더 정교한 버전 컨트롤이 필요할 때 사용하라.

profile
I'm still hungry

0개의 댓글