๋ค์ด๊ฐ๋ฉฐ
Kotlin์ ํตํด ๊ฐ๋ฐ์ ํ ์ดํ๋ก ์๋ฌ๊ฐ ๋ฐ์ํ ์ ์์์ ์๋ฆฌ๊ณ ์ถ์ ๋ Result๋ฅผ ์ ๊ทน์ ์ผ๋ก ํ์ฉํ๊ณ ์์ต๋๋ค.
Java์ Checked Exception๊ณผ ๊ฐ์ด ์ฌ์ฉํ๋ ์ธก์์ ์์ธ๊ฐ ๋ฐ์ํ ์ ์์์ ์ธ์งํ ์ ์์ผ๋ฉด์๋, ๊ฐ์ผ๋ก ์ฑ๊ณต/์คํจ๋ฅผ ์ ๋ฌํ๋ ํน์ง ๋๋ถ์ LSP๋ฅผ ์๋ฐํ์ง ์๋ ์ฅ์ ์ด ์๋๋ฐ์. ์ด ๋๋ถ์ ํน์ ๋น์ฆ๋์ค ๋ก์ง์ด ์๋๋ผ ๊ณตํต์ผ๋ก ์ฌ์ฉํ๋ ์ฝ๋์ ๊ฐ๊น์ธ์๋ก ๊ฐ์ธ์ ์ผ๋ก Result๋ฅผ ๋ ์ ๊ทน์ ์ผ๋ก ํ์ฉํ๊ณ ์์ต๋๋ค.
์ต๊ทผ์ Result์์ ์ ๊ณตํ๋ ๊ธฐ๋ณธ ๋ฉ์๋ ์ค ํ๋์ธ onFailure์์ ์์ธ๋ฅผ ๋์ก๋๋ ๋ฐ์ํ๋ ์ผ๊ณผ, ์ด๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ํํ ๊ฐ๋จํ ํด๊ฒฐ์ฑ
์ ๋ค๋ค๋ณด๋ ค ํฉ๋๋ค.
ํ๋ถํ๊ณ ๋ค์ํ ์์ธ
์ต๊ทผ์ ๋ค์ด ์์ธ๋ฅผ ์ ์์ํ์ฌ ๋ค์ํ ์ํฉ์ ๋ง์ถฐ ํ์ฉํ๊ณ , ๊ทธ ๋ด์ฉ์ ํ๋ถํ๊ฒ ์ ์งํ๊ธฐ ์ํด cause ํ๋๋ฅผ ํ์ฉํ๋ ๊ฒ์ ์ ๊ฒฝ์ ์ฐ๋ฉฐ ์ฝ๋๋ฅผ ์์ฑํ๋ ค๊ณ ๋
ธ๋ ฅํ๊ณ ์์ต๋๋ค.
ํนํ Sentry์ ๊ฐ์ ๋๊ตฌ๋ฅผ ์ฌ์ฉํ ๋ ์์ธ์ Stack Trace๋ฅผ ์ ์ชผ๊ฐ์ ๋ณด์ฌ์ฃผ๊ธฐ ๋๋ฌธ์ ์๋ฌ ์ถ์ ์ ์์ด ์ ์ฉํ๊ณ , ์ฝ๋์ ์ค๋ณต์ ์ผ๋ก ๋ก๊น ์ ํ๋ ๊ฒฝ์ฐ๋ฅด ์ค์ผ ์ ์์ด ๋ง์ ์ฅ์ ์ ๋๋ผ๊ณ ์์ต๋๋ค.
๊ทธ๋ฌ๋ค ๋ณด๋ฉด ์ฌ์ฉํ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ์ํ ์์ธ๋ ์ฐ๋ฆฌ๊ฐ ์ ์ํ์ง ์์ ์ํฉ์ ๋ํ ์์ธ๊ฐ ๋ฐ์ํ์ ๋, cause์ ์ ๋ด๊ณ , ์ฐ๋ฆฌ๊ฐ ์ ์ํ ์์ธ๋ฅผ ๋์ง๊ณ ์ถ์ ์ํฉ์ ์์ฃผ ๋ง์ฃผํ๊ฒ ๋ฉ๋๋ค.
try-catch๋ฅผ ํ์ฉํ๋ฉด ๋ค์๊ณผ ๊ฐ์ด ์ฒ๋ฆฌํ๊ฒ ๋ ๊ฒ ๊ฐ์ต๋๋ค.
fun retrieveFoo(): Foo {
// Prepare values to retrieve foo
// ...
return try {
fooClient.getFoo()
} catch (e: Exception) {
throw CustomOnlyForFooException(cause = e)
}
}
fooClient๊ฐ Spring WebClient, Ktor Client์ ๊ฐ์ด ์ธ๋ถ์ ์์กด์ฑ์ด์ด์ ์ฐ๋ฆฌ๊ฐ ์์ง ๋ชปํ๋ ์์ธ๋ฅผ ๋์ง ์ ์๊ฒ ์ง๋ง, CustomOnlyForFooException์ด๋ผ๋ Foo๋ง์ ์ํ ์์ธ๋ฅผ ์ ์ํด์ ์กฐ๊ธ ๋ ์ํฉ์ ๋ถํฉํ๋ ํ๋ถํ ์์ธ๋ฅผ ๋์ง๋ ์ํฉ์
๋๋ค. ์ง๊ธ์ ์์์ฌ์ cause๋ง ๋ด์์ง๋ง, Foo์ id, type ๊ฐ์ ์ ๋ณด๋ฅผ ๋ด์์ ๋ ์ํฉ์ ์ ํํํ๋ ์์ธ๋ก ๋ง๋ค ์๋ ์๊ฒ ์ฃ .
๊ทธ๋ฆฌ๊ณ ์ด retrieveFoo()๋ฅผ ์ฌ์ฉํ๋ ์ฝ๋๊ฐ ์๋ค๊ณ ๊ฐ์ ํด๋ณด๊ฒ ์ต๋๋ค. retrieveFoo()์์ ์์ธ๊ฐ ๋ฐ์ํ ์ ์๋ค๋ ์ฌ์ค์ ์๊ณ ์์ผ๋ฉด ์๋ง try-catch๋ก ๊ฐ์ธ์ ํธ์ถ์ ํ๊ฒ ๋ ๊ฒ์
๋๋ค. ๊ทธ๋ฆฌ๊ณ ์ฌ๊ธฐ์ ๋ฐ์ํ ์์ธ๋ฅผ ๋ช
์์ ์ผ๋ก ์ฒ๋ฆฌํด์ฃผ๋ ค๋ฉด ๋ค์๊ธ CustomOnlyForFooException์ cause์ ๋ด์ ์๋ก์ด ์์ธ๋ฅผ ๋์ ธ์ค์ผ๊ฒ ์ฃ . ์ฌ๊ธฐ์ CustomProcessFooException์ด๋ผ๊ณ ๊ฐ์ ํ๊ฒ ์ต๋๋ค.
fun processFoo(): ProcessFooResponse {
return try {
ProcessFooResponse.of(retrieveFoo())
} catch (e: Exception) {
throw CustomProcessFooException(cause = e)
}
}
์ง๊ธ์ ์์ ๊ฐ ๋จ์ํ๊ณ ์ํฉ์ ๋ง๋ค๊ธฐ ์ํ ๋ถ๋ถ์ด ์๋ค๋ณด๋ ๊ณผํ ์ฒ๋ฆฌ๋ก ๋ณด์ผ ์ ์์ง๋ง, ๋ ์ด์ด๊ฐ ๋ง์์ง๊ณ ๊ณตํต์ผ๋ก ์ฌ์ฉํ๋, ํน์ ์ฌ์ฉ๋๋ ์ฝ๋๊ฐ ๋ง์์ง๋ค ๋ณด๋ฉด ์ด๋ฐ ์ ๊ทผ์ด ์ ์ฉํ๋ค๊ณ ๋๋ผ๋ ๊ฒฝ์ฐ๊ฐ ์ข ์ข ์์ต๋๋ค.
Result ์ ์ฉํ๊ธฐ
์ ์ฝ๋๋ ๋ง์ฝ ์์ธ๊ฐ ๋ฐ์ํ ๊ฒฝ์ฐ ์ฌ์ฉํ๋ ์ชฝ์์๋ ์์ธ๊ฐ ๋ฐ์ํจ์ ์ ์ ์๋ค๋ ๋ฌธ์ ๊ฐ ์์ต๋๋ค. Kotlin์๋ ๋ชจ๋ ์์ธ๋ฅผ Unchecked๋ก ๋ค๋ฃจ๊ธฐ ๋๋ฌธ์, ์ฌ์ฉํ๋ ์ธก์์ ์์ธ๊ฐ ๋ฐ์ํ ์ ์์์ ์๋ ค์ฃผ๊ธฐ ์ํด Result๋ฅผ ์ฌ์ฉํ๊ฒ ๋ฉ๋๋ค.
๋ง์ฝ ์ง์ ์ ์ํ ์์ธ๋ฅผ ์ฌ์ฉํ์ง ์๋๋ค๋ฉด ๋ค์๊ณผ ๊ฐ์ด ์ฝ๋๋ฅผ ์์ฑํ๊ฒ ๋ ๊ฒ์ ๋๋ค.
fun retrieveFoo(): Result<Foo> = runCatching {
// Prepare values to retrieve foo
// ...
fooClient.getFoo()
}
fun processFoo(): Result<ProcessFooResponse> {
return retrieveFoo().map { foo ->
ProcessFooResponse.of(foo)
}
}
์ง๊ธ๊น์ง๋ ํฐ ๋ฌธ์ ๊ฐ ์์ด ๋ณด์
๋๋ค. Result๋ฅผ ํ์ฉํ์ฌ ์ฑ๊ณตํ ๊ฒฝ์ฐ ๋ณํ์ ์ํํ๊ณ , ๊ทธ๋๋ก Result์ ํํ๋ก ๋ฐํํ๋ ์ฝ๋๋ค์.
๊ทธ๋ฆฌ๊ณ ๊ฒฐ๋ก ์ ์ผ๋ก processFoo()๋ฅผ ์ฌ์ฉํ๋ ์ชฝ์์๋ ๋ค์๊ณผ ๊ฐ์ด ์ฝ๋๋ฅผ ์ฌ์ฉํ๊ฒ ๋ ๊ฒ์
๋๋ค. ์๋ฒ์๋ ๊ธฐ๋ณธ๊ฐ ์ฒ๋ฆฌ๋ฅผ ํ๋ ๊ฒฝ์ฐ๋ก ์๊ฐํด๋ณผ๊ฒ์.
fun handleProcessFoo(): ProcessFooResponse {
processFoo().getOrElse {
logger.error(it) { "some error occurred" }
ProcessFooResponse.DEFAULT
}
}
๋ณด์๋ค์ํผ Result์ ํจ๊ป๋ผ๋ฉด ํ์ํ๋ค๋ฉด ๊ธฐ๋ณธ๊ฐ ์ฒ๋ฆฌ๋ฅผ ํ ์๋, ํ์ํ์ง ์๋ค๋ฉด getOrThrow()๋ก ์์ธ๋ฅผ ๋์ ธ๋ฒ๋ฆด ์๋ ์์ด ํ์ธต ์ ์ฐํ ์ฒ๋ฆฌ๊ฐ ๊ฐ๋ฅํฉ๋๋ค.
์ง์ ์ ์ํ ์์ธ ์ ์ฉ, ๊ทธ๋ฆฌ๊ณ ๋ฌธ์ ๋ฐ์
๊ทธ๋ฌ๋ฉด ๋ง์ฝ try-catch๋ฅผ ์ธ ๋์ฒ๋ผ ์ฐ๋ฆฌ๊ฐ ์ ์ํ ์์ธ๋ฅผ ๋์ง๊ณ ์ถ๋ค๋ฉด ์ด๋ป๊ฒ ํ ์ ์์๊น์?
๋๋ค๋ฅผ ํตํด ์คํจ ์ ์ฒ๋ฆฌ๋ฅผ ์ ์ํ ์ ์๋ onFailure๋ฅผ ํ์ฉํ๋ฉด ์ด๋จ๊น์?
ํ๋ฒ ์ ์ฉํด๋ณด๊ฒ ์ต๋๋ค.
fun retrieveFoo(): Result<Foo> = runCatching {
// Prepare values to retrieve foo
// ...
fooClient.getFoo()
}
fun processFoo(): Result<ProcessFooResponse> {
return retrieveFoo().map { foo ->
ProcessFooResponse.of(foo)
}.onFailure { throw CustomProcessFooException(cause = it) }
}
fun handleProcessFoo(): ProcessFooResponse {
processFoo().getOrElse {
logger.error(it) { "some error occurred" }
ProcessFooResponse.DEFAULT
}
}
์ปดํ์ผ ์๋ฌ๋ ๋ฐ์ํ์ง ์๊ณ , ์ค์ ๋ก ๊ตฌ๋๋ ์ ๋ฉ๋๋ค. ํ์ง๋ง ์ด ์ฝ๋์๋ ๋ฌธ์ ๊ฐ ์์ต๋๋ค.
๋ฐ๋ก onFailure์์ ๋์ง CustomProcessFooException์ด processFoo()์ ๊ฒฐ๊ณผ๊ฐ์ธ Result.failure<ProcessFooResponse>()๊ฐ ์๋๋ผ ์์ processFoo()์์ ๋์ ธ์ง ์์ธ๊ฐ ๋๋ค๋ ๊ฒ์
๋๋ค.
๊ทธ๋ ๊ธฐ ๋๋ฌธ์ handleProcessFoo()์์ getOrElse๋ฅผ ํตํ ํธ๋ค๋ง์ ํด๋ ์ ํ ์ ์ฉ๋์ง ์๊ฒ ๋ฉ๋๋ค. ์ ์ด์ Result๊ฐ ๋ฐํ๋๋ ๊ฒ์ด ์๋ ์์ธ๊ฐ ํฐ์ ธ๋ฒ๋ฆฌ๊ธฐ ๋๋ฌธ์ด์ฃ .
๊ทธ๋ ๋ค๊ณ ํด์ ๋ค์๊ณผ ๊ฐ์ด ์ธ ์๋ ์์ต๋๋ค.
fun processFoo(): Result<ProcessFooResponse> {
return retrieveFoo().map { foo ->
ProcessFooResponse.of(foo)
}.onFailure { Result.failure(CustomProcessFooException(cause = it)) }
}
onFailure์ ์๊ทธ๋์ฒ๋ ๋ค์๊ณผ ๊ฐ์๋ฐ์.
inline fun <T> Result<T>.onFailure(action: (Throwable) -> Unit): Result<T>
action ๋๋ค์ ๋ฐํํ์ด Unit์ด๊ธฐ ๋๋ฌธ์ Result์ ์ฒด์ด๋ ๊ฒฐ๊ณผ์ ์ํฅ์ ์ค ์ ์๊ฒ ๋์ด ์๊ธฐ ๋๋ฌธ์
๋๋ค. ๊ตณ์ด ํด๊ฒฐํด๋ณด๋ ค๋ฉด inline fun์ด๋ผ๋ ์ ์ ์ด์ฉํด์ ๋ค์๊ณผ ๊ฐ์ด ์ง์ ๋ฐํ์ ํด์ฃผ๋ฉด ๋ฉ๋๋ค.
fun processFoo(): Result<ProcessFooResponse> {
return retrieveFoo().map { foo ->
ProcessFooResponse.of(foo)
}.onFailure { return Result.failure(CustomProcessFooException(cause = it)) }
}
ํ์ง๋ง onFailure ๋ค์ ์ฒด์ด๋์ผ๋ก ํ๋ฆ์ด ๋ ์๋ค๊ณ ๊ฐ์ ํ๋ฉด ์ฝ๋์ ๊ฐ๋
์ฑ์ด ๋จ์ด์ง๋ ๋ถ๋ถ์ด ๋๊ธฐ ์ฝ์ต๋๋ค.
์ด๋ป๊ฒ ๋ณด๋ฉด ์๋ ์ฒด์ธ์ ํ๋ฆ์ ์ํฅ์ ๋ง๋ค์ง ์๊ธฐ๋ก ๊ท์ ๋์ด ์๋ ๊ณณ์์ ์๊ธฐ์น ์์ ์ํฅ์ด ๋ฐ์ํ๋ ๊ฒ์ด๋๊น์.
ํด๊ฒฐ์ฑ
์ ์ ๋ ๊ฒ (fold)
์ด๋ฐ ๋ฌธ์ ๋ fold๋ผ๋ ๋ฉ์๋๋ฅผ ํตํด ํด๊ฒฐํ ์ ์์ต๋๋ค. (์ฌ์ค ์ ๋๋ค๊ธฐ ๋ณด๋ค๋ ์์ธ๊ฐ๊ณผ ์ ์๊ฐ์ ํ๋์ ํ๋ฆ์ผ๋ก ์ค์ฌ๋ด๋ ๊ฒ์ ๊ฐ๊น์ต๋๋ค.)
fold์ ์๊ทธ๋์ฒ๋ ๋ค์๊ณผ ๊ฐ์๋ฐ์.
inline fun <R, T> Result<T>.fold(onSuccess: (value: T) -> R, onFailure: (exception: Throwable) -> R): R
fold๋ ์ ์ ์ผ์ด์ค์ ์คํจ ์ผ์ด์ค์ ๋ํด ์ด๋ค ๊ฐ์ ๋ฐํํ ๊ฒ์ธ์ง๋ฅผ ๋๋ค๋ฅผ ํตํด ์ ์ํ ์ ์์ต๋๋ค.
๊ตณ์ด Result๋ฅผ ๋ฐํํ๋๋ก ๋ช
์๋ ๊ฒ์ ์๋๋ค ๋ณด๋, ๋ค์๊ณผ ๊ฐ์ด Result.success()์ Result.failure()๋ฅผ ๋ช
์์ ์ผ๋ก ์จ์ฃผ๊ธด ํด์ผํฉ๋๋ค.
fun retrieveFoo(): Result<Foo> = runCatching {
// Prepare values to retrieve foo
// ...
fooClient.getFoo()
}
fun processFoo(): Result<ProcessFooResponse> {
return retrieveFoo().fold(
onSuccess = { foo -> Result.success(ProcessFooResponse.of(foo)) },
onFailure = { Result.failure(CustomProcessFooException(cause = it)) }
)
}
fun handleProcessFoo(): ProcessFooResponse {
processFoo().getOrElse {
logger.error(it) { "some error occurred" }
ProcessFooResponse.DEFAULT
}
}
ํ์ง๋ง ์ฝ๋๊ฐ ํจ์ฌ ๋ช
์์ ์ด๊ฒ ๋๊ณ , Result์ ์ฒด์ด๋ ํ๋ฆ์๋ ์ํฅ์ ์ฃผ์ง ์์ต๋๋ค. ์์ฐ์ค๋ฝ๊ฒ ๊ณ์ ํ๋ฌ๊ฐ๊ฒ ๋์ฃ .
์ข ๋ ์งง๊ฒ!
fold๋ฅผ ์ข ๋ค์ฌ๋ค๋ณด๊ณ ์์ผ๋ฉด ๊ธฐ๋ณธ์ผ๋ก ์ ๊ณต๋๋ getOrThrow ์ญ์๋ fold๋ก ํํํ ์ ์์์ ์ ์ ์์ต๋๋ค.
inline fun <T> Result<T>.getOrThrowFold(): T =
fold(
onSuccess = { it },
onFailure = { throw it }
)
์ด์ ๋น์ทํ๊ฒ ์กฐ๊ธ๋ง ๋ ์์ฉํด๋ณด๋ฉด ์ฑ๊ณต ๊ฐ์ ๊ทธ๋๋ก ๋๊ณ , ์คํจ๊ฐ๋ง ์ฐ๋ฆฌ๊ฐ ์ํ๋ ์์ธ๋ก ๋ณํํด์ ๋์ง๋ ํ์ฅํจ์๋ ๊ตฌํํด๋ณผ ์ ์๊ฒ ๋ฉ๋๋ค.
inline fun <T, E : Throwable> Result<T>.mapFailure(
transform: (Throwable) -> E
): Result<T> =
fold(
onSuccess = { Result.success(it) },
onFailure = { Result.failure(transform(it)) }
)
์์ธ๋ฅผ ๋ณํํ๋ transform์ ๋๋ค๋ก ๋ฐ๋ ํ์ฅํจ์๋ฅผ ๊ตฌํํ์ต๋๋ค.
์ด๋ฅผ ์ฝ๋์ ์ ์ฉํ๋ฉด ์ต์ข
์ ์ผ๋ก ๋ค์๊ณผ ๊ฐ์ด ์ ์ฉํ ์ ์์ต๋๋ค.
fun retrieveFoo(): Result<Foo> = runCatching {
// Prepare values to retrieve foo
// ...
fooClient.getFoo()
}.mapFailure { CustomOnlyForFooException(cause = it) }
fun processFoo(): Result<ProcessFooResponse> {
return retrieveFoo().map { foo ->
ProcessFooResponse.of(foo)
}.mapFailure { CustomProcessFooException(cause = it) }
}
fun handleProcessFoo(): ProcessFooResponse {
processFoo().getOrElse {
logger.error(it) { "some error occurred" }
ProcessFooResponse.DEFAULT
}
}
์ ์ ์ฝ๋์ ๋ณํ๊ณผ ์์ธ ์ฝ๋์ ๋ณํ์ ํ์ธต ๋ ๊น๋ํ๊ฒ ํํํ ์ ์๊ฒ ๋์๋ค์. ์ฒ์ try-catch๋ฅผ ํ์ฉํ ๊ฒ์ฒ๋ผ retrieveFoo()์๋ ์ ์ฉํด๋ดค์ต๋๋ค.
๋ง์น๋ฉฐ
์ด๋ ์ ๊ฐ์?
ํฌ๊ฒ ์ด๋ ต์ง ์์ ํ์ฅ ํจ์ ๊ตฌํ์ ํตํด Result์ ์ฒด์ด๋์ ์ง์ผ๋ด๋ฉด์ ์ฝ๋๋ฅผ ๋ช
์์ ์ผ๋ก ์์ฑํ ์ ์๊ฒ ๋์์ต๋๋ค.
onFailure์์ ์์ธ๋ฅผ ๋ฐ์์ํค๊ฒ ๋๋ฉด ํจ์ ์ ์ฒด์ ํธ์ถ ์คํ์ผ๋ก๋ถํฐ ์์ธ๊ฐ ๋ฐ์ํ๊ฒ ๋๋ ๋ถ๋ถ์ ์๊ธฐ์น ๋ชปํ ๋ฌธ์ ๋ฅผ ๋ถ๋ฌ์ฌ ์ ์๋๋ฐ์. ํนํ Coroutines ํ๊ฒฝ์์ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๋ฉด Coroutines ํน์ ์ ์๋ฌ ์ ํ๋ก ์ธํด ์๋นํ ์ถ์ ํ๊ธฐ ๊น๋ค๋ก์ด ๋ฌธ์ ๋ก ๋ฒ์ง๊ฒ ๋ฉ๋๋ค.
๊ทธ๋ฐ ์ํฉ์ด ๋ฐ์ํ์ง ์๋๋ก ํ๋ ค๋ฉด onFailure์ ์ญํ ์ ๋ช
ํํ๊ฒ ์ดํดํ๊ณ , ํ์ํ ๊ฒฝ์ฐ์ fold์ ๊ฐ์ ๋ค๋ฅธ ๋ฉ์๋๋ฅผ ํ์ฉํด๋ณด๋๊ฒ ์ข์ ๊ฒ ๊ฐ์ต๋๋ค.
์ด ๊ธ์ด Result๋ฅผ ์ฌ๊ธฐ๋กญ๊ฒ ์ฌ์ฉํ๊ณ ์ ํ๋ ๋ถ๋ค๊ป ๋์์ด ๋์์ผ๋ฉด ํฉ๋๋ค.
๊ฐ์ฌํฉ๋๋ค.