상세 컨텐츠

본문 제목

[Kotlin] Null 안전과 예외

Programming language/Kotlin

by choiDev 2020. 9. 24. 16:34

본문

Null이란?

  - var이나 val 변수의 값이 없다는 것을 나타내는 값이다.

  - Java를 포함해 많은 언어에서 null은 치명적 에러를 유발하는 원인이 된다.

  - Kotlin에서 null 값을 가지려면 null타입을 선언해야한다.

 

Kotlin에서의 Null을 선언?

  - Kotlin에서 아래와 같은 구문을 작성하면 3번 line에서 컴파일 에러가 발생한다.

  - String은 null 불가능(non-nullable) 타입 이기 때문이다. 

fun main(args: Array<String>) {
    var name: String = "Choi"
    name = null
}

 

 

명시적 null 타입

  - (타입)?은 해당 타입의 null 가능(nullable)을 뜻합니다. 

  - Kotlin에서의 null 선언은 아래와 같다.

fun main(args: Array<String>) {
    var name:String? = "Choi"
    name = null
    
    pirntln(name)
}

 

에러 검출 시점(컴파일 vs 런타임)

  - 컴파일 언어 특성상 컴파일러가 언어의 요구사항에 맞게 컴파일 에러를 발생시키는 지를 확인 후 처리한다.

  - 컴파일 시점 에러는 프로그램이 실행 전 에러를 발견할 수 있어 큰 장점중 하나다

  - 이와 달리 런타임 에러는 컴파일러가 발견할 수 없어 프로그램 실행 중 발생하는 오류이며
    어느 시점에 발생하는지 확인이 에러 발생전 까지는 모른다

 

null 안전 처리

  - 가장 안전한 방법은 null 불가능 타입을 사용하는 것이지만 그렇지 않은 경우를 대비해 
    3가지 방법이 있습니다.  

 

첫번째 방법: 안전 호출 연산자

  - 아래 코드를 실행하면 컴파일 도중 에러가 난다. 
    readLine()은 null을 허용하지만 capitalize()이 null을 허용하지 않기 때문이다

fun main(args: Array<String>) {
    var beverage = readLine().capitalize()
    println(beverage)
}

 

  - 이런 경우 안전 호출 연산자인 '?'를 사용한다.

fun main(args: Array<String>) {
    var beverage = readLine()?.capitalize()
    println(beverage)
}

  - 이렇게 하면 컴파일 런타임 에러 모두 생기지 않는다.

  - 컴파일러가 안전 호출 연산자를 발견하면 null 값을 검사하는 코드를 자동으로 추가해 준다
    따라서 런타임 시 readLine 함수의 반환 결과가 null이 아니면 capitalize 함수가 호출되어
    첫 자가 대문자로 변환된 반환 문자열이 지정된다.

  - 이처럼 안전 호출 연산자를 사용하면 함수 호출에 사용되는 변수나 다른함수의 반환값이 null이 아닐때만
    다음 함수가 안전하게 호출되므로 NullPointerException을 방지할 수 있다.

 

안전 호출 연산자와 함께 let 함수 사용하기 

  - 안전 호출 연산자를 사용했을때 null에 따라 결과를 처리하고싶을때 사용하면 좋다

  - 아래 코드는 readLine()한 결과가 null인 경우 "bear"를 null이 아닌 경우 it.capitalize()를 실행하는 코드이다.

fun main(args: Array<String>) {
    var beverage = readLine()?.let {
        if(it.isNotBlank()){
            it.capitalize()
        }else{
            "bear"
        }
    }
    println(beverage)
}

 

두 번째 방법 : non-null 단언 연산자

 - non-null 단언 연산자는 '!!'으로 표시한다.

 - 이 연산자는 null이 될 수 없다는 것을 단언하는 연산자다. 

 - 따라서 왼쪽 피연산자 값이 null이 아니면 정상적으로 코드를 수행하고 ,
   null이면 런타임 시에 NullPointerException 예외를 발생시킨다.

 - 단언 연산자는 컴파일러가 null 발생을 미리 알 수 없는 상황이 생길 수 있을 때 사용된다. 

 - 아래 코드를 해석하면 readLine의 반환값이 무엇이든 capitalize를 실행하라는 뜻이다. 
   단 반환값이 null이면 KotlinNullPointerException 예외가 발생된다.

fun main(args: Array<String>) {
    var beverage = readLine()!!.capitalize()
    println(beverage)
}

 

세 번째 방법 : 값이 null인지 if로 검사하기

  - null 값을 if문으로 검사하여 처리하는 것이다.

 

null 복합 연산자 (null coalescing operator)

  - null 복합 연산자는 '?:' 으로 표시한다.

  - 엘비스 연산자라고도 불린다 

  - null 복합 연산자는 검사값이 null일 때 null이 아닌 기본값을 제공하여 결괏값이 null이 되지 않도록 하는
    연산자라고 생각할수 있다.

[null 복합 연산자 예제]

fun main(args: Array<String>) {
    var beverage = readLine()
    beverage = null

    val beverageServed:String = beverage?:"맥주"  //beverage가 null이면 "맥주" ,null이 아니면 beverage를 반환
    println(beverageServed)
}

 

 

[null 복합 연산자 예제 - let과 함께 사용] 

fun main(args: Array<String>) {
    var beverage = readLine()?.let{ //readLine()의 결과가 null인경우 let은 실행하지, 않고 "맥주"를 반환하고, 
        it.capitalize()             //readLine()의 결과가 null이 아니면 let을 실행하여, 입력받은 문자열의 첫글자를 대문자로 변경해 반환한다.
    }?:"맥주"

    println(beverage)
}

 

예외(Exception)

  - 대표적으로 KotlinNullPointerException이 있다.

  - 사용자의 예상외의 값 입력, 개발자의 잘못된 코딩으로 인해 발생한다

  - 일반 예외 : 컴파일 과정에서 체크되어 발생되는 에러

  - 실행 예외 : 런타임 과정에서 발생되는 에러

  - 미처 처리하지 못한 예외를 미처리 예외(unhandled exception),
    프로그램 실행이 중단되는 것을 크래시(crash)라고 한다.

 

예외 던지기 (Exception Throw)

  - 예외를 발생시키는 것을 예외를 던진다(Throw)고 표현한다.

  - 예외를 던지는 이유는 코드가 잘못 작성되었으면 처리해야하는 문제임을 알려주기 위함이다

  - 흔히 발생하는 예외는 IllegalStateException이 있다.

  - 아래 코드는 count가 null일 경우 IllegalStateException를 던집니다

fun main(args: Array<String>) {
    var count:Int? = null
    val isThree = (1..3).shuffled().last() == 3
    if(isThree){
        count = 2
    }

    nullCheck(count)
    println(count)
}

fun nullCheck(count:Int?){
    count?: throw IllegalStateException("count is null!!")
}

  - 이와 같이 예외를 던지면 어느 시점에 예외가 발생하였는지 어떤 에러인지 정확히 알 수 있다.

 

커스텀 예외(Custom Exception)

  - 기존에 정의되어있던 KotlinNullpointException, IllegalstateException등이 아닌 직접 작성한 예외이다

  - 아래 코드에 ChoiException이라는 커스텀 예외를 작성하였다.

fun main(args: Array<String>) {
    val name:String = "Kim"
    checkNameisChoi(name)

    println("He name is $name.")
}

fun checkNameisChoi(name:String) {
    if(name != "Choi") {
        throw ChoiException()
    }
}

//IllegalStateException 상속 및 재정의
class ChoiException(): IllegalStateException("이 분은 Choi가 아닙니다")

 

예외 처리 (Try/Catch/Finally)

  - Try 블록에선 실행할 코드를 Catch 블록에선 실행도중 예외발생시 처리할 코드를 작성한다.

  - Try 블록은 항상 실행하지만 Catch 블록은 예외 발생시만 실행한다.

  - finally 블록은 예외가 발생해도 가장 마지막에 실행됩니다.
    예외가 발생해도 처리해야할 일이 있다면 finally에 작성하는것이 좋습니다.

import java.lang.Exception

fun main(args: Array<String>) {
    val count:Int? = null

    try{
        count!!.plus(1)			//null + 1 을 하여 에러가 발생한다.
    }catch (e: Exception){
        e.printStackTrace()		//에러 메시지를 출력한다.
    }finally {
        println("예외가 발생하던 안하던 전 실행됩니다.")
    }

    println("He name is $count.")
}

 

 

전제 조건 함수 (Precondition Function)

  - Kotiln 표준 라이브러리의 일부로 편의 함수에 속한다.

  - 이 함수들을 사용하면 커스텀 메시지와 함께 예외를 던질 수 있다.

  - 아래 코드에 checkNotNull()이 전제 조건 함수이다. 

import java.lang.Exception

fun main(args: Array<String>) {
    val count: Int? = null

    try {
        checkNotNull(count,{ "해당 변수는 null 입니다." })  
    } catch (e: Exception) {  //count가 null인경우 예외를 발생시키고 메세지를 실행합니다. 
        e.printStackTrace()
    } finally {
        println("예외가 발생하던 안하던 전 실행됩니다.")
    }

    println("He name is $count.")
}

 

[전제 조건 함수 종류]

함수 설명
checkNotNull 첫번째 인자값이 null이면 IllegalStateException을 던지며, 그렇지 않으면 첫번째 인자 값을 반환한다.
require 첫번째 인자값이 false면 IllegalArgumentException을 던진다.
requireNotNull 첫번째 인자값이 null이면 IllegalArgumentException을 던지며, 그렇지 않으면
첫번째 인자값을 반환한다.
error 첫 번째 인자값이 null이면 제공된 메시지와 함께 IllegalStateException을 던지며, 그렇지 않으면 첫번째 인자값을 반환한다.
assert 인자값이 false이면 AssertionError를 던진다. 그리고 컴파일러의 assertion 플래그가 활성화 된다.

 

null에 관하여

  - 실제로 null은 다른언어에서 폭넓게 사용되고 있다, 아직 지정되지 않은 변수의 초깃값으로 말이다
    예를 들어 userName이라는 사용자 이름을 입력하는 변수가 있다면
    사용자가 입력하지 않으면 그의 이름을 null로 지정해놓는 방식으로 말이다.

  - 이처럼 null이 기본값으로 지정되는 방식으로 인해 다른 언어에서는 종종 NullPointerException이 발생될 수 있다.
    이것이 Kotlin이 null의 처리를 중요시하는 이유이다

checked 예외와 unchecked예외

  - Kotiln에서는 모든 예외가 unchecked 예외이다, 즉 예외가 생길 수 있는 모든 코드를 우리가
    try/catch 문으로 반드시 처리하도록 컴파일러가 강요하지 않는다는 뜻이다.

  - 예를 들어 자바는 checked와 unchecked 예외 타입이 구분되어 있다.
    checked 예외의 경우 try/catch문으로 처리하는지 컴파일러가 확인하고, 만일 처리하지 않으면 컴파일 에러를 
    발생시킨다. 이것은 프로그래머가 예외 처리를 정확하게 하도록 한 것이다.

  - 하지만 대부분의 프로그래머들은 catch문에 에러를 처리하기 보단 e.printStackTrace()만 기재하여 에러를 넘겨 
    프로그램을 정상 실행하도록 하여 에러를 무시하므로, 추후에 더 큰 문제로 발전 할 수 있다.

  - 위 이유들로 인해 checked예외는 문제 해결보다 더 많은 문제를 야기하므로 Kotlin은 unchecked 예외를 지원하고잇다. 

관련글 더보기