🚫예외를 활용해 코드에 제한을 걸어라..
이번 안정성 챕터에서는 이전에서도 보았듯이 예상치 못한 동작을 막고 앱의 안정성을 높이는 방법을 코드에 여러 제한을 걸어 앱이 예상치 못한 동작을 하지 못하도록 막는 방법과 이렇게 막을 경우 얻는 여러 장점들에 대해서 얘기합니다.
방법 1. 아규먼트(argument)
require 블록: 아규먼트에 제한할 수 있음
require함수 같은 경우 값이 참인지 체크하고 거짓일 경우 IllegalArgumentException을 throw 시킵니다.
이와 비슷하게 requireNotNull함수가 존재하는데 값이 Null일 경우 IllegalArgumentException을 throw 시킵니다.
public inline fun require(value: Boolean): Unit {
contract {
returns() implies value
}
require(value) { "Failed requirement." }
}
public inline fun require(value: Boolean, lazyMessage: () -> Any): Unit {
contract {
returns() implies value
}
if (!value) {
val message = lazyMessage()
throw IllegalArgumentException(message.toString())
}
}
public inline fun <T : Any> requireNotNull(value: T?): T {
contract {
returns() implies (value != null)
}
return requireNotNull(value) { "Required value was null." }
}
public inline fun <T : Any> requireNotNull(value: T?, lazyMessage: () -> Any): T {
contract {
returns() implies (value != null)
}
if (value == null) {
val message = lazyMessage()
throw IllegalArgumentException(message.toString())
} else {
return value
}
}
위코드처럼 값을 체크하여 값이 참인지 확인합니다. require블록은 함수의 가장 앞부분에 넣어 유효성 검사를 실시합니다.
이를 가장 잘 보여주는 예는 팩토리얼입니다.
fun factorial(n:Int):Long{
require (n>=0)
return if(n<=1) 1 else factorial(n-1) * n
}
위 로직은 팩토리얼을 구현한 코드입니다. 기존의 팩토리얼의 의도대로 값을 집어넣을 경우 n*(n-1)*(n-2) *.... * 1 이런 방식으로 값을 구합니다 그렇지만 팩토리얼은 0 이상의 값을 받지만 임의로 음의 정수를 넣을 경우 1이 리턴됩니다. 이것은 위에서 말했듯 예상치 못한 동작을 발생시켰습니다. 이경우 위코드처럼 require을 사용한다면 예상치 못한 동작을 피할 수 있을 것입니다. 또한 함수의 시작 부분에 이렇게 require로 제한을 둔다면 코드를 읽을 때 쉽게 확인할 수 있을 것입니다. 또한 require안에 있는 contact덕분에 스마트캐스트 기능을 제공하여 캐스틀 적게 할 수 있습니다.
방법 2. 상태(state)
check 블록: 상태와 관련된 동작을 제한할 수 있음
동작은 위의 require와 비슷합니다. 대신 참이 아닐 경우 IllegalStateException을 throw 합니다.
public inline fun check(value: Boolean): Unit {
contract {
returns() implies value
}
check(value) { "Check failed." }
}
public inline fun check(value: Boolean, lazyMessage: () -> Any): Unit {
contract {
returns() implies value
}
if (!value) {
val message = lazyMessage()
throw IllegalStateException(message.toString())
}
}
public inline fun <T : Any> checkNotNull(value: T?): T {
contract {
returns() implies (value != null)
}
return checkNotNull(value) { "Required value was null." }
}
public inline fun <T : Any> checkNotNull(value: T?, lazyMessage: () -> Any): T {
contract {
returns() implies (value != null)
}
if (value == null) {
val message = lazyMessage()
throw IllegalStateException(message.toString())
} else {
return value
}
}
이 check블록 같은 경우 클래스의 파라미터의 상태를 이용할 때 좋아 보입니다.
var isConnected = false
fun connect() {
check(!isConnected) { "이미 연결되어 있습니다" }
...
위의 require처럼 함수의 시작 부분에 이렇게 check로 제한을 둔다면 코드를 읽을 때 쉽게 확인할 수 있을 것입니다. 또한 check안에 있는 contact덕분에 스마트캐스트 기능을 제공하여 캐스틀 적게 할 수 있습니다.
방법 3. Assert
assert 블록: 어떤 것이 true인지 확인할 수 있음 (assert 블록은 테스트 모드에서만 작동함)
이 assert 또한 참이 아닐 경우 AssertionError를 throw 합니다.
public inline fun assert(value: Boolean) {
assert(value) { "Assertion failed" }
}
public inline fun assert(value: Boolean, lazyMessage: () -> Any) {
if (_Assertions.ENABLED) {
if (!value) {
val message = lazyMessage()
throw AssertionError(message)
}
}
}
단위테스트 대신 함수에서 assert를 사용하게 되면 특정 상황이 아닌 모든 상황에 대한 테스트를 할 수 있고 실행 시점에 정확하게 어떻게 되는지 확인 가능합니다 또한 더 빠르게 실제 코드를 실패하게 만들 수 있어 예상치 못한 동작이 언제 어디서 실행됐는지 빠르게 찾을 수 있습니다. 이런 장점이 있더라도 단위테스트를 안 해도 된다라는 것이 아닙니다.
Nullabilty와 스마트 캐스팅
위에서 말한 대로 if를 쓰지 않고 require나 check를 활용한다면 contract로 해당 조건을 확인하여 스마트캐스팅이 적용됩니다. 이점은 매번 스마트 캐스팅을 하지 않아도 된다는 장점이 있습니다. null 같은 경우에도 requireNotNull이나 checkNotNull을 이용하여 스마트 캐스팅도 할 수 있지만 엘비스연산자로 throw 나 return을 걸어준다면 더욱 다양한 상황에 사용할 수 있을 것입니다.
참고
https://seosh817.tistory.com/155
https://seedpotato.tistory.com/234
'코틀린 > Effective Kotlin' 카테고리의 다른 글
[Effective Kotlin] 7. 안정성 - 결과 부족이 발생할 경우 null과 Failure를 사용하라 (0) | 2023.07.24 |
---|---|
[Effective Kotlin] 6. 안정성 - 사용자 정의 오류보다는 표준 오류를 사용 하라 (0) | 2023.07.21 |
[Effective Kotlin] 4. 안정성 - inferred 타입으로 리턴하지 말라 (0) | 2023.07.17 |
[Effective Kotlin] 3. 안정성 - 최대한 플랫폼 타입을 사용하지 말라 (0) | 2023.07.14 |
[Effective Kotlin] 2. 안정성 - 변수의 스코프를 최소화 하라 (0) | 2023.07.11 |