ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 아이템5 - 예외를 활용해 코드에 제한을 걸어라
    Kotlin/Effective Kotlin 요약 2023. 1. 4. 00:01
    728x90

    동작 제한 방법

    코틀린에서 코드의 동작을 제한하는 다양한 방법들이 존재합니다.

    - require

    - check

    - assert(in test)

    - return 또는 throw와 함께 활용하는 Elvis 연산자

     

    제한으로 발생하는 다양한 장점

    - 문서를 읽지 않은 개발자도 문제를 확인할 수 있다

    - 문제가 발생할 때 예외를 던집니다.

    - 스마트 캐스트 기능을 활용할 수 있어, 타입 변환을 적게 할 수 있습니다.

    - 코드가 자체적으로 검사되어 단위 테스트를 줄일 수 있다.

     

    아규먼트 제한

    아규먼트에 제한을 거는 다양한 예시를 살펴보겠습니다.

    - 숫자가 양의 정수여야 한다.

    - 비어있지 않은 좌표 목록이 필요하다.

    - 이메일 형식이 올마른지 확인한다.

     

    require(n>=0) // n: Int
    require(points.isNotEmpty()) // points: List<Point>
    requireNotNull(user.email) // user: User
    require(isValidEmail(user.email))

     

    유효성 검사 코드는 함수의 가장 앞부분에 위치하여 읽는 사람도 쉽게 확인할 수 있습니다.

    require 함수는 조건을 만족하지 못할 때 무조건적으로 IllegalArgument Exception을 발생시킵니다.

     

    람다를 활용해 자연 메시지를 정의할 수도 있습니다.

    require(n >= 0) { "Cannot calculate factorial of $n becuase it is smaller than 0"}

     

    상태 제한

    구체적인 조건을 만족할 때만 함수를 사용할 수 있게 하는 상태 제한 예시를 살펴보겠습니다

    - 미리 초기화되어 있어야먄 처리하고 싶은 함수

    - 로그인했을때만 처리하게 하고 싶은 함수

    - 객체를 사용할 수 있는 시점에 사용하고 싶은 함수

    check(isInitialized)
    checkNotNull(token)
    check(isOpen)

    check는 require와 비슷하지만 지정한 예측을 만족하지 못할 때 IllegalStateEception을 throw 합니다.

    보통 상태가 올바른지 확인할 때 사용합니다.

     

     

    Assert 계열 함수 사용

    단위 테스트에서는 Assert를 활용하여 제대로 동작하는지 검증합니다.

     

    nullability와 스마트 캐스팅

    코틀린에서는 require와 check 블록으로 어떤 조건을 확인해서 true가 나왔다면, 해당 조건을 이후로도 true일 거라고 가정합니다.

     

    public inline fun require(value: Boolean, lazyMessage: () -> Any): Unit {
        contract {
            returns() implies value
        }
        if (!value) {
            val message = lazyMessage()
            throw IllegalArgumentException(message.toString())
        }
    }

    따라서 이를 활용하여 타입 비교를 했다면 스마트 캐스트가 작동합니다.

     

    contract는 코드를 컴파일러에게 미리 이해시켜주기 위해 사용합니다.

    contract가 포함된 함수를 호출한 다음 로직부터는 contract에 정의한 조건을 계속 명시하지 않아도 되는 장점이 있습니다.

     

    어떤 사람(Person)의 복장(person.outfit)이 드레스여야 코드가 정상적으로 진행되는 예시

    fun changedDress(person: Person){
        require(person.outfit is Dress)
        val dress: Dress = person.outfit
    }

    이때 outfit 프로퍼티가 final이라면 Dress로 스마트캐스트 됩니다.

     

    이런 특징은 어떤 대상이 null인지 확인할 때 굉장히 유용합니다.

    data class Person(
        val email: String?
    )
    
    fun main(args: Array<String>) {
    
        val person = Person(
            email = null,
        )
        val email: String = person.email //컴파일 에러 : Type mismatch.Required: String Found:String?
        require(person.email != null) //스마트캐스트 발생
        val email: String = person.email //컴파일 에러 X
        println(person.email)
    }

    이때 person이 var이라면 다음과 같은 컴파일 에러가 발생합니다.

    Smart cast to 'String' is impossible, because 'person.email' is a complex expression

     

    requireNotNull, checkNOtNull이라는 특수한 함수를 사용해서 변수를 '언팩'하는 용도로 활용

    data class Person(
        val email: String?
    )
    
    fun main(args: Array<String>) {
    
        val person = Person(
            email = "bababrll@naver.com",
        )
        val email = requireNotNull(person.email)
        println(email)
    }

     

    throw와 return으로 Elvis 연산자를 활용

    data class Person(
        val email: String?
    )
    
    fun main(args: Array<String>) {
    
        val person = Person(
            email = null,
        )
        //val email = person.email ?: return
        val email = person.email ?: throw IllegalArgumentException()
        println(email)
    }

    nullable을 확인할 때 굉장히 많이 사용되는 관용적인 방법입니다.

    이를 적극 활용하는 것이 좋습니다.

    또한 이런 코드는 함수의 앞부분에 넣어서 잘 보이게 만드는 것이 좋습니다.

     

    정리

    - 제한을 훨씬 더 쉽게 확인할 수 있다.

    - 애플리케이션을 더 안정적으로 지킬 수 있디.

    - 코드를 잘못 쓰는 상황을 막을 수 있다.

    - 스마트 캐스팅을 활용할 수 있다.

     

    개인적으로는 Elvis 연산자로 Default값을 주는걸 가장 선호함!

    728x90

    댓글

Designed by Tistory.