관리 메뉴

HAMA 블로그

[Kotlin 세미나] 실패/예외 대응 방식 본문

Kotlin

[Kotlin 세미나] 실패/예외 대응 방식

[하마] 이승현 (wowlsh93@gmail.com) 2021. 11. 22. 13:54

1. 디폴트 값으로 대처 

interface Transfer {
    fun moveTo(id : String)
}

interface Asset{
    val id: String
    val name: String
    val data: String
}

class Token(
    override val id: String,
    override val name: String,
    override val data: String) : Asset,Transfer{
    override fun moveTo(id: String) {
        TODO("Not yet implemented")
    }

}
class NFT(
    override val id: String,
    override val name: String,
    override val data: String) : Asset,Transfer {
    override fun moveTo(id: String) {
        TODO("Not yet implemented")
    }

}

class Account(val id: String, val name: String?): Transfer {
    val assets = mutableListOf<Asset>()
    override fun moveTo(id: String) {
        TODO("Not yet implemented")
    }
}

먼저 이런 코드가 있다고 가정 해 본다.

@Test
fun setUp() {
    val account = Account("1", null)
    val name = account.name?:"default_name"
    Assertions.assertEquals("default_name", name )
}


해당 Account의 name을 가져왔을 때 만약 null이라면 "default_name"을 넣어 줄 수 있다.
많은 경우 이렇게 처리 할 수 있을 것이다. 특히 설정값을 처리할 때 많이 사용되는데 특별히 설정된 값이 없을 경우 디폴트 값을 넣어주는 경우가 그럴 것이다. 

2. nullable  

그리고 위에 보듯이 특정 값이 없을 경우 우리는 null을 리턴 할 수 있다. 보통 null을 리턴하는 경우는 일상적으로 벌어지는 문제일 경우이다. 즉 어떤 컬렉션에 특정 값이 없을 경우엔 null을 리턴하는게 정상적이다. 

class Bank{
    val accounts = mutableMapOf<String, Account>()
    fun createAccount(account: Account){
        accounts.put(account.id, account)
    }
    
    fun getAccount(id: String): Account? {
        return accounts.get(id) 
    }
}

위에서 getAccount를 호출할 경우 해당 정보가 없을 수 도 있는데, 이것은 예외라고 보긴 어려울 것이다. (기술적으로 예외로 처리할 수도 있지만, 나는 그렇게 처리하지 않는다) 

 @Test
 fun getAccount() {
   val bank = Bank()
   bank.createAccount(Account("1", "hama"))
   val result = bank.getAccount("1")?.name

  Assertions.assertEquals("hama", result)
}

만약 "2"를 넣어서  null을 리턴한다면 아무것도 처리하지 않으며, "1"을 넣는다면 name을 출력 할 것이다.  
? 는 null이 아닐경우만 처리하라는 의미로, 위의 코드에서 ? 없이 name을 코딩 할 순 없다. 이렇게 코틀린은 NPE을 강제로 막아준다.

3. Execption으로 전파 

null로 처리하게 된 다면, 호출당한 함수내부에서 어떤 이유로 null이 리턴 됬는지 궁금 할 수가 있다. 일반적으로 예상되는 경우라면 호출한 측에서 이유를 작성 할 수도 있지만, 그렇지 않다면 호출당한 함수에서 이유를 알려주는 것이 일반적이다. 

class Bank{
    val accounts = mutableMapOf<String, Account>()
    fun createAccount(account: Account){
        accounts.put(account.id, account)
    }
    fun getAccount(id: String): Account? {
        return accounts.get(id)
    }

    fun dipositToken(accountId: String, token: Token){
        accounts.get(accountId)?.dipositToken(token)
    }

만약 위의 dipositToken을 호출했는데 accountId에 해당하는 계좌가 없으면 아무일도 안 일어날 것이다. 뭐 이렇게 되도 상관없을 수도 있겠다. 일관성이 깨지는 것 같은 큰 문제는 일어나지 않았으니깐, 대신 이 함수를 호출한 측에서는 diposit이 잘 됬는지 아닌지 알수가 없다. 잘 됬다고 생각하고 다음 단계를 밟을 수도 있는데 그렇게 되면 문제가 된다. 따라서 이 경우에는 리턴 값으로 문제 상황을 알리거나 예외를 전파해야하는데, 예외로 전파하는것을 추천한다. 나는 command 행동에서 문제가 생기면 예외를 던지고, query행동에서문제가 생기면 null (일반적인 상황일때) 혹은 예외(진짜 예외상황일때)를 전파하는 컨벤션을 갖는다.

@Throws(NoAccountException::class)
fun createToken(accountId: String, token: Token){
    accounts.get(accountId)?.createToken(token) ?: throw NoAccountException("there is no $accountId in our bank")
}

이렇게 예외를 던저주고 함수에 명시해 둔다. (호출하는 클라이언트에 대한 배려) 

@Test
fun dipositToken() {
    Assertions.assertThrows(NoAccountException::class.java, {
        bank.createToken("2", exception.Token("token_1", "opusm", 1000))
    })
}

Junit5를 통해서 테스트를 성공한다. 

4. runCatching 

위에서 예외를 통해서 전파했는데, 전파된 예외를 단순한 try catch 방식이 아닌  좀 더 매끄럽게 아래와 같이 처리 할 수 있다. 

account)

fun withrawToken(id: String, balance: Int): Int {
    val token = assets.find { it -> it.id == id  } as Token
    require(token.balance > balance )
    token.balance = token.balance - balance
    return balance
}

bank)

fun withrawToken(accountId: String, tokenId: String, balance: Int): Int{
    val result = kotlin.runCatching {
        accounts.get(accountId)?.withrawToken(tokenId, balance)
    }

    return result.getOrDefault(0)!!
}

test)

@Test
fun withrawToken() {
    bank.createToken("1", exception.Token("token_1", "opusm", 1000))
    val result = bank.withrawToken("1", "token_1", 5000)
    Assertions.assertEquals(0, result)
}


아래와 같이 runCatching 문은 좀 더 다양한 방식으로 처리 될 수 있다. 

fun withrawToken(accountId: String, tokenId: String, balance: Int):String{
    return  kotlin.runCatching { accounts.get(accountId)?.withrawToken(tokenId, balance)}
    .onFailure { println(" withraw token failure") } // 특정 예외를 구분 할 수도 있다.
    .onSuccess { println(" withraw token success") }
    .mapCatching { it -> it.toString() }
    .getOrDefault("0") // getOrNull, getOrThrows
}

/////// 아래와 같이 특정 예외별로 구분해서 처리 할 수도 있다.
getOrElse {
    when(it) {
        is LowBalanceException -> "LowBalanceException"
        is IllegalStateException -> "IllegalStateException"
        is NullPointerException -> "null"
        else -> throw it
    }
}

 

전체 코드)


package exception

import exception.bankexception.LowBalanceException
import exception.bankexception.NoAccountException
import kotlin.jvm.Throws


interface Transfer {
    fun moveTo(id : String)
}

interface Asset{
    val id: String
    val name: String
}

enum class AssetKind{
    Token,
    NFT
}

data class Token(
    override val id: String,
    override val name: String,
    var balance: Int) : Asset,Transfer{
    override fun moveTo(id: String) {
        TODO("Not yet implemented")
    }

}
data class NFT(
    override val id: String,
    override val name: String) : Asset,Transfer {
    override fun moveTo(id: String) {
        TODO("Not yet implemented")
    }

}

data class Account(val id: String, val name: String?): Transfer {
    val assets = mutableListOf<Asset>()
    override fun moveTo(id: String) {
        TODO("Not yet implemented")
    }

    fun createToken(token: Token) {
        assets.add(token)
    }

    fun withrawToken(id: String, balance: Int): Int {
        val token = assets.find { it -> it.id == id  } as Token
        require(token.balance > balance )
        token.balance = token.balance - balance
        return balance
    }
}

class Bank{
    val accounts = mutableMapOf<String, Account>()
    fun createAccount(account: Account){
        accounts.put(account.id, account)
    }
    fun getAccount(id: String): Account? {
        return accounts.get(id)
    }

    @Throws(NoAccountException::class)
    fun createToken(accountId: String, token: Token){
        accounts.get(accountId)?.createToken(token) ?: throw NoAccountException("there is no $accountId in our bank")
    }

    fun withrawToken(accountId: String, tokenId: String, balance: Int):Int{
        return  kotlin.runCatching { accounts.get(accountId)?.withrawToken(tokenId, balance)}
        .onFailure { println(" withraw token failure") }
        .onSuccess { println(" withraw token success") }
        .getOrDefault(0)!!
    }
}
0 Comments
댓글쓰기 폼