DevLog ๐ถ
[Kotlin & Spring] Amazon S3 ์ ๋ก๋ - ๊ณตํต ์๋ฌ ์ฒ๋ฆฌ ํธ๋ค๋ง, runCatching ๋ณธ๋ฌธ
[Kotlin & Spring] Amazon S3 ์ ๋ก๋ - ๊ณตํต ์๋ฌ ์ฒ๋ฆฌ ํธ๋ค๋ง, runCatching
dolmeng2 2023. 3. 30. 23:41์ ๋ง ์์ ์ญํ ์ด์ง๋ง ์กฐ๊ธ์ฉ ์ฌ์ด๋ ํ๋ก์ ํธ๋ฅผ ์งํํ๊ณ ์๋๋ฐ, ์ฝํ๋ฆฐ ๋ฌธ๋ฒ์ ๊ณต๋ถํ๋ฉด์ ์๋ก์ด ์ ์ ์๊ฒ ๋์ด ๊ธฐ๋กํ๊ณ ์ ํ๋ค! (์ฝํ๋ฆฐ... ์ต์ํด์ง๋ฉด ์ ๋ง ํธํ ๊ฒ ๊ฐ์ง๋ง ์์ง์ ์ ๋ชจ๋ฅด๊ฒ ๋ค. ์ด๋ ต๋ค!)
โ๏ธ Amazon S3 with Kotlin
์ฝํ๋ฆฐ๊ณผ s3๋ฅผ ์ฐ๋ํ๊ฒ ๋๋ฉด, ์๊ธฐ์น ๋ชปํ ์๋ฒ ์ค๋ฅ์ ๋๋นํ์ฌ ํ์ผ์ ์ฝ์ ํ๊ฑฐ๋ ์ญ์ ํ ๋, ํน์ url ์ ๋ณด๋ฅผ ๋ฐ์์ฌ ๋ ๋ค์๊ณผ ๊ฐ์ด Exception์ด ๋ฐ์ํ ์ ์๊ฒ ๋๋ค.
ํ ๊ฐ์ง ๊ถ๊ธํ ์ ์, AmazonServiceException์ ๊ฒฝ์ฐ ๋ถ๋ชจ ํ์ ์ด SdkClientException์ธ๋ฐ ์ ๊ตฌ๋ถํด๋์์๊น...
์๋ฌดํผ, ๊ธฐ์กด์๋ ์๋ฒ ์๋ฌ์ ๋ํด ์ ํ ๊ณ ๋ คํ์ง ์์ ์ํ๋ก ๋ค์๊ณผ ๊ฐ์ ์ฝ๋๋ฅผ ์์ฑํ์์๋ค.
fun deleteFile(fileKey: String) {
val bucketName = s3Properties.bucketName
s3Client.deleteObject(bucketName, fileKey)
}
์ธ๋ถ api๋ฅผ ์ฌ์ฉํ๋ ๋งํผ, ๋น์ฐํ๊ฒ๋ ์ธ๋ถ ์ค๋ฅ์ ๋ํด์ ์ฒ๋ฆฌ๋ฅผ ํ์ด์ผ ํ๋๋ฐ ์ด๋ฅผ ๊ฐ๊ณผํด๋ฒ๋ ธ๋ค ๐ฅฒ
์ฝ๋ ๋ฆฌ๋ทฐ๋ฅผ ํตํด์ ํด๋น ๋ถ๋ถ์ ๋ํด์ ์ง์ ์ ๋ฐ์๊ณ , ๊ทธ๋์ ์ฒ์์๋ ๋ค์๊ณผ ๊ฐ์ด ์ฝ๋๋ฅผ ์์ฑํ์๋ค.
class S3Exception(cause: Throwable? = null) :
PureureumException(cause = cause, errorCode = ErrorCode.S3_UPLOAD_FAILED)
fun deleteFile(fileKey: String) {
val bucketName = s3Properties.bucketName
try {
s3Client.deleteObject(bucketName, fileKey)
} catch (e: SdkClientException) {
throw S3Exception(e)
}
}
S3Exception์ ์์ฒด์ ์ผ๋ก ์ ์ํ Exception์ด๋ค.
ํ์ง๋ง, ๋๋ฌด ์๋ฐ์ค๋ฝ๊ฒ ์ฝ๋๋ฅผ ์์ฑํ ๊ฒ์ด ์๋๊น? ๋ผ๋ ์๋ฌธ์ด ๋ค์ด์ ์ด๊ฒ์ ๊ฒ ์ฐพ์๋ณด์๋ค.
๊ทธ๋ฆฌ๊ณ ์ป์ด๋ธ ๊ฒ์ด, ์ฝํ๋ฆฐ์๋ runCatching์ด๋ผ๋ ๊ฒ ์กด์ฌํ์๋ค! ๐ฒ
โ๏ธ runCatching
@InlineOnly
@SinceKotlin("1.3")
public inline fun <T, R> T.runCatching(block: T.() -> R): Result<R> {
return try {
Result.success(block())
} catch (e: Throwable) {
Result.failure(e)
}
}
Calls the specified function block with this value as its receiver and returns its encapsulated result if invocation was successful, catching any Throwable exception that was thrown from the block function execution and encapsulating it as a failure.
ํจ์๋ธ๋ก์ receiver์๊ฒ ํธ์ถํ๊ณ , ํธ์ถ์ด ์ฑ๊ณตํ๋ฉด ๊ฒฐ๊ณผ๋ฅผ ๋ฐํํ๋ค. ์ฑ๊ณตํ๋ฉด ์ฑ๊ณต์ ๋ํด์ ์บก์ํ๋ฅผ ์งํํ๊ณ , ์์ธ๊ฐ ๋ฐ์ํ๋ฉด ์คํจ๋ก ์บก์ํ๋ฅผ ์งํํ๋ค. ์ฌ๊ธฐ์ 'Result'๋ผ๋ ์น๊ตฌ๊ฐ ์ฌ์ฉ๋๋ ๊ฒ์ ๋ณผ ์ ์๋ค. (์๋ฌ ๋ฐ์ ์ Result.failure, ์๋๋ผ๋ฉด Result.success)
@SinceKotlin("1.3")
@JvmInline
public value class Result<out T> @PublishedApi internal constructor(
@PublishedApi
internal val value: Any?
) : Serializable {
// discovery
}
public companion object {
/**
* Returns an instance that encapsulates the given [value] as successful value.
*/
@Suppress("INAPPLICABLE_JVM_NAME")
@InlineOnly
@JvmName("success")
public inline fun <T> success(value: T): Result<T> =
Result(value)
/**
* Returns an instance that encapsulates the given [Throwable] [exception] as failure.
*/
@Suppress("INAPPLICABLE_JVM_NAME")
@InlineOnly
@JvmName("failure")
public inline fun <T> failure(exception: Throwable): Result<T> =
Result(createFailure(exception))
}
์ฝ๋ ๋ด๋ถ๋ฅผ ํ์ธํด๋ณด๋ฉด, ์ฑ๊ณต์ผ ๊ฒฝ์ฐ T ํ์ ์ ๊ฐ์ ๊ฐ์ง๊ฒ ๋๊ณ , ์คํจ์ผ ๊ฒฝ์ฐ exception์ ๊ฐ์ผ๋ก ๊ฐ์ง๋ ๊ฒ์ ๋ณผ ์ ์๋ค.
โ๏ธ Result๋ฅผ ์ฌ์ฉํ๋ฉด ๋ญ๊ฐ ์ข์๋ฐ?
Result๋ ๋ด๋ถ์ ์ผ๋ก ๊ฝค๋ ๋ค์ํ ๋ฉ์๋๋ค์ ์ ๊ณตํ๊ณ ์๋ค.
๐ฌ getOrNull
@InlineOnly
public inline fun getOrNull(): T? =
when {
isFailure -> null
else -> value as T
}
์๋ฌ๊ฐ ๋ฐ์ํ๋ฉด ํด๋น ์๋ฌ๋ฅผ ๋ฌด์ํ๊ณ , null์ ๋ฐํํ๋๋ก ํ ์ ์๋ค.
๐ฌ getOrDefault
@InlineOnly
@SinceKotlin("1.3")
public inline fun <R, T : R> Result<T>.getOrDefault(defaultValue: R): R {
if (isFailure) return defaultValue
return value as T
}
์๋ฌ๊ฐ ๋ฐ์ํ๋ฉด ์ง์ ํ ๊ธฐ๋ณธ๊ฐ์ ๋ฐํํ๋๋ก ์ค์ ํ๋ค.
๐ฌ getOrElse
@InlineOnly
@SinceKotlin("1.3")
public inline fun <R, T : R> Result<T>.getOrElse(onFailure: (exception: Throwable) -> R): R {
contract {
callsInPlace(onFailure, InvocationKind.AT_MOST_ONCE)
}
return when (val exception = exceptionOrNull()) {
null -> value as T
else -> onFailure(exception)
}
}
์๋ฌ๊ฐ ๋ฐ์ํ๋ฉด ํน์ ํ ๋์์ ์ํํ๋ค.
์ฌ๊ธฐ์ ๋ ๊ฐ์ ์ธ์๋ฅผ ๋ฐ๋๋ฐ, ์ฒซ ๋ฒ์งธ๋ ๊ฐ์ฒด๊ฐ ์ฑ๊ณต์ ์ผ๋ก ๊ฐ์ ๊ฐ์ง๊ณ ์์ ๋ ํด๋น ๊ฐ์ ๋ฐํํ๋ ๋๋ค ํจ์, ๋ ๋ฒ์งธ๋ ๊ฐ์ฒด๊ฐ ์คํจํ์ ๊ฒฝ์ฐ Throwable ๊ฐ์ฒด๋ฅผ ์ฒ๋ฆฌํ์ฌ ๋ฐํํ ๋์ฒด ๊ฐ์ ์์ฑํ๋ ๋๋ค ํจ์์ด๋ค.
์ฌ๊ธฐ์ ๋ด๋ถ์ ์ผ๋ก contract ๋ธ๋ก์ ํ์ฉํ๋ฉด (์ธ๋ผ์ธ ํจ์์์๋ง ์ฌ์ฉ ๊ฐ๋ฅ) ํจ์ ํธ์ถ์ ์ ์ ์กฐ๊ฑด, ํน์ ์ฌํ ์กฐ๊ฑด ๋ฑ์ ์ ์ํ ์ ์๋๋ฐ, ์ฌ๊ธฐ์๋ callInPlace ํจ์๋ฅผ ํ์ฉํ์ฌ onFailure ํจ์๊ฐ ์ต๋ 1๋ฒ๋ง ํธ์ถ๋๋ ๊ฒ์ ๋ณด์ฅํ๋ค.
๊ทธ๋ฆฌ๊ณ , when์ ์ ํตํด์ ๊ฐ์ฒด๊ฐ ๊ฐ์ง๊ณ ์๋ ๊ฐ์ ๊ฐ์ ธ์ค๊ฑฐ๋, ์คํจํ์ ๊ฒฝ์ฐ ๋์ฒด ๊ฐ์ ๋ฐํํ๊ฒ ๋๋ค. (exceptionOrNull ํจ์๋ฅผ ํตํด ์คํจํ ๊ฒฝ์ฐ Throwable์, ๊ทธ๋ฆฌ๊ณ onFailure๋ฅผ ํตํด ๋์ฒด๊ฐ์ ์์ฑํ๊ฒ ๋๋ค.)
๐ซ ์ธ๋ผ์ธ ํจ์
// ์ผ๋ฐ ํจ์
fun add(a: Int, b: Int): Int {
return a + b
}
// ์ธ๋ผ์ธ ํจ์
inline fun add(a: Int, b: Int): Int {
return a + b
}
์ธ๋ผ์ธ ํจ์๋ผ๋ ๊ฒ์ ์ฒ์ ๋ค์ด๋ด์ ๊ฐ๋จํ๊ฒ ์์ฑํด๋ณด๊ณ ์ ํ๋ค.
์ธ๋ผ์ธ ํจ์๋ ํธ์ถ๋๋ ์์น์ ํจ์์ ๋ด์ฉ์ ๋ณต์ฌํ์ฌ ํจ์ ํธ์ถ ๋น์ฉ์ ์ค์ด๋๋ฐ, inline ํค์๋๋ฅผ ์ฌ์ฉํด ์ ์ธํ๋ค.
์ผ๋ฐ ํจ์๋ ํธ์ถํ ๋๋ง๋ค ํด๋น ํจ์์ ๋ํ ์คํ ํ๋ ์์ ์์ฑ๋๊ณ , ํจ์๊ฐ ์คํ์ด ๋๊ณ , ๊ฒฐ๊ณผ๊ฐ์ ๋ฐํํ ๋ค ์ค์ฝํ๊ฐ ๋๋๋ฉด ์คํ ํ๋ ์์ด ์ ๊ฑฐ๋๋ค. ํ์ง๋ง, ์ธ๋ผ์ธ ํจ์๋ ํธ์ถ ์์ ์ ํธ์ถ๋ถ์์ ํจ์๊ฐ ์ง์ ์ฝ์ ๋๊ธฐ ๋๋ฌธ์, ํจ์ ํธ์ถ์ ์ค๋ฒ ํค๋๋ฅผ ์ค์ด๊ฒ ๋๋ค.
๐ฌ exceptionOrNull
public fun exceptionOrNull(): Throwable? =
when (value) {
is Failure -> value.exception
else -> null
}
์๋ฌ๊ฐ ๋ฐ์ํ ๊ฒฝ์ฐ์๋ง ํด๋น ์๋ฌ ๊ฐ์ฒด๋ฅผ ๋ฐํํ๊ณ , ์๋๋ผ๋ฉด null์ ๋ฐํํ๋ค.
โ๏ธ runCatching ์ ์ฉํ๊ธฐ
์๋ฌดํผ, ์์ ๋์จ ๋ค์ํ ๋ฉ์๋๋ฅผ ํ์ฉํ์ฌ ๋ด ์ฝ๋์ ์ ์ฉํด๋ณด์๋ค.
fun deleteFile(fileKey: String) {
val bucketName = s3Properties.bucketName
return runCatching {
s3Client.deleteObject(bucketName, fileKey)
}.getOrElse {
throw S3Exception(it)
}
}
ํ์คํ ์ด์ ๋ณด๋ค ์ฝํ๋ฆฐ์ค๋ฝ๊ฒ ์์ฑํ ๊ฒ ๊ฐ์์ ์ข์๋ค.
ํ์ง๋ง, ๋ฌธ์ ์ ์ด ์์๋ค. ํ์ผ ์ญ์ , ์์ฑ, ์กฐํ ์์๋ ๋ชจ๋ runCatching ๋ธ๋ก์ด ๋ค์ด๊ฐ๋ค ๋ณด๋ ์ค๋ณต์ด ๋๋ ๊ฒ์ด์๋ค.
๊ทธ๋์, ํจ์ํ ์ธํฐํ์ด์ค๋ฅผ ํ์ฉํด๋ณด๊ณ ์ ํ๋ค.
private fun <T> execute(operation: () -> T): T {
return runCatching { operation() }
.getOrElse { throw S3Exception(it) }
}
๋ณ๊ฑฐ ์๋ค. ๊ทธ๋ฅ ์คํํด์ผ ํ๋ ๋ก์ง์ ๋ํด์ ๋งค๊ฐ๋ณ์๋ก ๋๋ค๋ก ๋๊ฒจ์ฃผ๋ ๊ฒ์ด๋ค.
์์ ํธ์ถ๋ถ๋ ๋ค์๊ณผ ๊ฐ์ด ์ค์ผ ์ ์๊ฒ ๋๋ค.
override fun deleteFile(fileKey: String) {
val bucketName = s3Properties.bucketName
return execute {
s3Client.deleteObject(bucketName, fileKey)
}
}
๋๋ถ์ s3 ๊ด๋ จ ์๋ฌ ๋ก์ง๋ค์ ํ ๋ฒ์ ์ฒ๋ฆฌํ ์ ์๊ฒ ๋์๋ค ๐
์ผ๋ฅธ ์ฝํ๋ฆฐ ์ํ๊ณ ์ถ๋ค... ์์ง ๋๋ฌด ์ด๋ ต๋ค ๐ฅฒ
์คํ๋ง๋, JPA๋ ๋ค ๋ค์ ๊ณต๋ถํด์ผ๊ฒ ๋ค... ๋ค ๊น๋จน์๋ค... ใ ํ๋ด์...