코틀린 타입 시스템

Null Safety

nullability 은 NullPointerException 어레러르 피하게 해주는 간단한 코틀린 타입 시스템의 특성이다 요 Exception 은 자바에서는 정말 예상하지 못하는 곳에서 넘어와서 해당 로직을 삑나게 하는 아주 나쁜놈이다 물론 이걸 방지하기 위해서 모든 가능성을 생각하면서 null 조건 체크를 해주는 것도 방법인데.. 이게 정말 말처럼 쉽지가 않다 에이 설마 여기서도 나겠어? 하던게 실제로 그렇게 나와버리는.. 그런 상황이 나온다 그래서 코틀린에서는 이러한 문제를 찾아내는 시간을 런타임에 체크하는 것이 아니라 컴파일 시점에 체크를 해준다 널이 될 수 있는지의 여부를 타입 시스템에 추가함으로써 컴파일러가 미리 감지해서 미연에 방지가 되도록 만들었다

널이 될 수 있는 타입은 코틀린에서 명시적으로 지원하고 있다 물론 nullable 하게 만드는 것도 가능하다 nullable 하게 만드는 방법은 타입을 선언하면서 그 뒤에 ?을 붙히는 것이다 그럼 기존에 사용하던 방법으로 생각해보면 ?을 붙히지 않은 기본적인 타입은 모두 nullable 하지 않고 컴파일러가 null이 들어갈 수 있다면 빨간줄로 경고한다 그럼 nullable한 코드는 어떻게 null을 처리할까? -> 이것에 대한 대답은 컴파일러에 있다 기존에 자바에서 하는 것과 같이 null과 비교하는 과정, 비교하려는것 != null 을 통해서 검사를 해줘면 컴파일러가 아 비교하려는 객체는 null 을 비교하는 부분을 확인하고 null이 아님을 확실하는 영역에서 작성되도록 도와준다 이외에도 코틀린에서는 널이 될 수 있는 값을 안전하게 다루도록 도와주는 특별한 연산자들이 많다

안전한 호출 연산자 ?. 이 있다 ?. 은 null 검사와 메소드호출을 한 번의 연산으로 수행한다 즉 s?.toUpperCase() 이렇게 작성하면 if(s!=null) s.toUpperCase() else null 이 것과 같은 의미이다 만약 호출하려는 값이 null 이면 해당 호출이 무시되고 null 값이 되지만 만약 호출하려는 값이 null 이 아니면 메소드 호출처럼 작동한다는 것이다 그리고 이것의 포인트는 연쇄해서 사용하는 것도 가능하다는 의미이다 예를 들어서 메뉴 밑에 메뉴옵션 밑에 메뉴옵션아이템이 있을 때 메뉴 옵션아이템에 접근하고 싶다 그럼 this.menu?.menuOption?.menuOptionItem 이렇게 접근하면 menu, menuOption에 대한 null 추가 검사 없이 menuOption을 바로 가져오는 것이 가능하다 이런 식으로 간결하게 null 체크를 할 수 있다는 점이 정말 좋은 것 같다 불필요한 if문 같은 것들도 줄일 수 있는 것이다

엘비스 연산자라고 불리우는 ?: 이 있다 null 인경우에 null 대신 default 값을 명시해주는 연산자이다 val s: String?: "ddd" 이건 만약에 s 변수에 정상적으로 String 값이 들어왔으면 그 값을 적용시키고 s 변수에 null 이 들어왔으면 "ddd" 가 나온다는 것이다 아까 위에서 메뉴.메뉴옵션.메뉴옵션아이템 요 구조에서 메뉴옵션아이템마저도 null 이면 안되니까 null인 케이스를 고려해줘야 했다면 이걸 사용하면 fun getMenuOptionItem() = menu?.menuOption?.menuOptionItem?: "no val" 이렇게 안전한 메뉴옵션아이템의 게터함수를 만드는 것이 가능하다 이외에 코틀린에서 return 이나 throw 같은 연산도 식으로 치기 때문에 엘비스 연산자 우항에 return 이나 throw 와 같은 연산도 넣을 수 있다 이러한 패턴은 함수의 전제조건을 검사할때 유용하다

class Menu(val menuId: String, val menuVersion: MenuVersion)
calss MenuOption(val menuOptionId: String, val menuId: String, val menuOptionVersion)
class MenuVersion(val version: String, val yyyymmdd: String)

fun printMenuVersion(menuOption: MenuOption){
    val getMenuVersion = menuOption.menuId?.menuVersion?: throw IllegalArgumentException("No Version")
        with(getMenuVersion){
            println(yyyymmdd)
            println(version)
        }
}

이런 식으로 사용할 수 있다

타입 캐스팅이 존재하고 as? 이렇게 사용한다 자바의 타입 캐스트처럼 as로 타입을 변경하는 것이 가능하다 근데 만약에 타입캐스팅에 문제가 있으면 당연하게 타입캐스팅예외가 나올 것이다 그러면 is을 통해서 타입 검사를 먼저 진행하고 as 으로 변환해야하는데 좀 귀찮은 방식이다 그런데 as? 이렇게 하면 알아서 처리해주는 것 만약에 타입 캐스팅이 안되면 예외를 던지는게 아니라 null 을 던지게 되어있다 이렇게 안전하게 캐스트를 수행하고 엘비스 연산자를 자주 사용하는 패턴이 자주 보인다

널 아님 단언 이라는데.. 그냥 !! 이 느낌표 2개를 의미하는 것 같다 !! 은 코틀린에서 널이 될 수 있다는 타입의 값을 다룰때 사용한다 이걸 붙히면 어떤 값이든 널이 될 수 없는 타입으로 강제로 변환해준다 그래서 만약에 실제 null에 대해서 !!을 적용해주면 널에러가 발생한다

fun ignoreNulls(s: String?){
    //s 같은경우는 nullable인데 null이 들어갈 수 없는 변수에 넣었기 때문에 발생?
    val sNotNull: String = s!!
    println(sNotNull.length)
}

!!은 컴파일러에게 null 값이 들어갈 수 없는 변수이지만 null이 들어가도록 하겠다 라는 의미이다 문제가 발생할 수도 있다는 것을 의미하기 떄문에 약간 눈에 잘 보이도록 일부러 이렇게 만들었다고 한다 근데 좋은 방법일때도 있는게 컴파일러 기준으로 함수 내부의 값이 null이 아님을 체크했더라도 안전하다고 판단하지 못하는 경우가 있다 그래서 호출된 함수가 언제나 널이 아님이 확실하다면 사용하기 딱이다 그리고 참고?로 !! 이걸 한 라인에서 연속해서 사용하면 어디서 문제가 났는지 파악이 힘들다 그냥 어디줄에서만 에러났다고 나와서 파악하기 힘들기 때문에 연속으로 사용하는건 좋지 않다는 점

let 함수 let 함수는 원하는 식을 평가해서 결과가 널인지 검사한 다음에 그 결과를 변수에 넣는 작업을 간단한 식을 사용해서 한꺼번에 처리하는 것이 가능하다 가장 자주 사용하는 방식은 널이 될 수 있는 값을 널이 아닌 값만 인자로 받는 함수에 넘기는 경우이다 그러한 방식은 if(객체 != null) myFun(객체) 이런식으로 했었을 것이다 위의 식을 객체?.let { 객체 -> myFun(객체) }, 객체?.let{myFun(객체)} 이렇게 변경해주는 것이 가능하다 이것의 의미는 어떠한 객체가 null 이면 아무런 작업을 하지 않지만, let 람다 내부에서는 해당 객체가 Null이 아니라고 가정하고 식이 진행된다 아주 편리하게 보이는데, 이게 null 체크를 여러개 해야하는 경우에는 조금 코드가 복잡해져서 알아보기가 어렵다 그래서 null 체크가 많이 필요한거면 따로 if 으로 뺴서 검사하는게 좋긴하다

lateinit -> 나중에 초기화 코틀린에서는 일반적으로 생성자에서 모든 파라미터에 대한 초기화를 해줘야하며, 프로퍼티 타입이 널이 될 수 없는 타입이라면 반드시 널이 아닌 값으로 그 프로퍼티는 초기화 해줘야 한다 근데 초기화 값을 제공받으면 null이 될 수 없는 타입을 사용할 수 밖에 없다, 그럼 맨날 null 체크하던가 !! 을 써줘야 한다 그래서 사용하는게 lateinit으로 나중에 초기화하는 것이 가능하다 근데 val은 final이기 때문에 요건 적용못하고 var에서만 사용하는 것이 가능하다 이걸사용하고 만약에 초기화되기 전에 해당 프로퍼티에 접근하면 그냥 initialized 에러가 발생해서 찾기가 더 쉽다 애초에 nullpointerexception 보다는 바로 어디가 문제인지 파악하기 쉽다

null 이 될 수 있는 타입 확장 null이 될 수 있는 타입에 대한 확장 함수를 정의해두면 그냥 메소드 호출을 통해서 확장함수인 메소드가 알아서 널을 처리해준다 예를 들면 String에서 isEmpty나 isBlank이라는 함수가 존재하는데 각각의 메소드는 null로부터 안전한 메소드가 아닌다 null에 대한 체크가 없기 때문이다 근데 사실 isNullorEmpty, isNullorBlank 이러한 메소드가 있다 두 메소드의 역할은 다음과 같다 null이면 true를 반환하고 null이 아니면 isBlank을 호출한다 이렇게 null이 될 수 있는 타입에 대한 확장을 정의해주면 널이 될 수 있는 값에 대해 그 확장 함수를 호출하는 것이 가능하다 만약에 사용하는 것이 아니라 직접 확장함수를 작성한다면 그 확장 함수를 null이 될 수 있는 타입에 대해 정의할지에 대한 여부를 고민하고 내부에서 꼭 null에 대한 처리를 확인하자

타입 파라미터의 nullable 코틀린에서 함수나 클래스의 모든 타입 파라미터는 기본적으로 nullable이다 그리고 nullable 인 타입을 포함하는 어떤 타입이라도 타입 파라미터르 대신하는 것이 가능하다 타입 파라미터 T를 클래스나 함수 안에서 타입 이름으로 사용하면 ?이 없더라도 T는 nullable 이다 만약에 타입 파라미터가 null이 아닌 것을 확실히 하려면 null이 될 수 없는 타입 상한을 지정해야한다 근데 요 부분은 제네릭할때 다시 잘 봐보자

자바와 nullable 자바와 코틀린 사이에서는 코드의 막힘없이 서로 공유가 가능하고 잘 사용되는데, 코틀린은 널 세이프하지만 자바에서는 그걸 어떻게 처리할 수 있냐 자바에서도 @NotNull, @Nullable 이라는게 있으니까 자바와 코틀린을 혼재해서 사용할때는 같이 잘 사용해야할 듯

플랫폼 타입 플랫폼 타입이란 컴파일러가 널 관련 정보를 알 수 없는 타입이다 만약 해당 타입이 널이 될 수 있는 타입으로 처리해도 되고, 널이 될 수 없는 타입으로 처리해도 된다 약간 null에 대한 설정을 해주는 보다는 정말 자바에서처럼 널에 대한 위험성을 가지고 사용하는 느낌이다 그래서 null 체크를 2번하든 null safe 하지 않든 컴파일러가 전혀 신경쓰지 않는 타입이다 근데 안전하게 개발하도록 도와주는 걸 포기하고 이러한 타입을 사용할 때가 있는가에 대한 대답은 필요없는 null 체크가 있는 경우가 존재한다 이게 뭐 딱히 정해진 타입이 이러한 타입이에요 이런건 아니고 자바에서 @NotNull, @Nullable과 같은 null에 대한 애노테이션이 붙어있지 않은 그러한 소스를 코틀린에서 볼때 플랫폼 타입이다 이렇게 이야기하는 것이다 상속할 때도 자바의 메소드를 상속받을 때 메소드의 파라미터가 nullable일때와 notnull 일때의 두 가지 가능성을 고려해서 구현하는 것이 가능하다

코틀린의 원시 타입

자바에서 원시타입(primitive type), 참조타입(reference type) 이렇게 나누어지고, 프리미티브 타입에는 직접 값이 들어가지만 참조 타입은 메모리상의 객체 위치에 들어간다 원시 타입은 컬렉션에 넣거나 원시타입에 대한 메소드호출이 없기 때문에 만약에 사용하기 위해서는 굳이 또 Wrapper 타입을 통해서 프리미티브 타입을 감싸서 사용하곤 한다 근데 코틀린에서는 따로 구분하지 않고 공통 Int을 사용하기 때문에 간단하게 사용할 수 있다 더 신기한건 그냥 다 무시하고 항상 같은 타입을 사용하는 것이 아닌 실행 시점에 가장 효율적인 방식으로 표현된다

타입의 변환 코틀린에서는 하나의 타입의 숫자를 다른 타입의 숫자로 자동 변환해주지 않는다 대신 모든 원시 타입에 대한 변환함수를 to원시타입 이렇게 생긴 변환 함수를 제공해주기 때문에 타입 변환을 명시를 통해서 사용할 수 있으며 자바에서와 같이 overflow 에러도 날 수 있으니 유의해서 타입변환을 해야하며, 자바에서 사용했던 것 처럼 리터럴(long이면 L, float이면 F 등) 이렇게 리터럴을 허용한다

최상위 타입 Any, Any? 자바에서는 Object가 클래스의 최상위 타입인 것 처럼 코틀린에서는 Any가 모든 클래스의 최상위 타입이다 근데 자바에서는 int 같은건 원시타입이기 때문에 Integer로 감싸야 그제서야 Object을 상속한 개념이 되었다면 코틀린에서는 그냥 원시타입까지 한데 모아서 Any를 조상타입으로 가지고 있다 any는 널이 될 수 없는 타입이고, 널을 포함하는 모든 값을 대입할 변수를 선언하기 위해서는 Any? 이렇게 사용한다 그리고 내부에서 Any 타입은 java.lang.Object에 대응하기 때문에 자바 메소드에서 Object을 인자로 받거나 반환하면 코틀린에서는 Any로 그 타입을 취급하는 것이 가능하다

코틀린에서의 void타입인 Unit 코틀린에서 void을 표현할 때는 명시적으로 void 이렇게 선언하던가 아무런 키워드를 안적었었는데, Unit을 작성하는 것이 void을 선언하는 의미이다 코틀린에서 Unit으로 선언하고 그 함수가 제네릭 함수를 따로 상속받지 않는다면 그 함수는 내부적으로 void으로 컴파일된다 unit은 모든 기능을 갖는 일반적인 타입이고, unit을 인자로도 사용하는 것이 가능하다 이외에도 코틀린에서는 결코 성공적으로 값을 돌려주는 일이 없기 때문에 반환 값이라는 개념 자체가 의미 없는 함수가 있는데, Nothing 이라는 타입을 사용해주면 아무런 값도 포함하기 않아서 함수의 반환 타입이나 반환 타입으로 쓰일 타입 파라미터로만 사용될 수 있다 컴파일러는 Nothing이 반환 타입인 함수가 정상종료되지 않는 것을 알고 분석하기 쉽게 찾는 것이 가능한 것이다

컬렉션과 배열

타입 인자의 널 가능성에 대해서는 간단하게 봤었지만 이건 타입 시스템 일관성을 지키기 위해 필수적으로 확인해야하는 사항이다 컬렉션 안에 null 을 넣을 수 있는지에 대한 여부는 어떤 변수의 값이 널이 될 수 있는지 여부와 마찬가지로 중요하다 리스트에서 null을 체크하기 위해서 타입에서 ?을 통해서 null을 허용하거나 허용하지 않거나 작업할 수 도 있으며, filterNotNull 을 통해서 컬렉션안에 널이 없다는 것을 확인하는 것 등 이렇게 진행할 수 있다 코틀린에서의 컬렉션은 읽기 전용 컬렉션과 변경 가능 컬렉션이 존재한다 그래서 컬렉션을 생성할 때 방식이 다르다 List -> 읽기전용타입은 listOf 을 통해서 만들고 사용하거나 조회하는 것이 가능 -> 변경가능타입은 mutableListOf, arrayListOf Set -> 읽기전용타입은 setOf 을 통해서 -> 변경가능타입은 mutableSetOf, hashSetOf, linkedSetOf, sortedSetOf Map -> 읽기전용타입은 mapOf 을 통해서 -> 변경가능타입은 mutableSetOf, hashMapOf, linkedMapOf, sortedMapOf

리스트말고 배열은 어떻게 사용하나 코틀린에서 배열을 만드는 방법은 arrayOf 함수에 원소를 넘겨서 배열을 만드는 방식, arrayOfNulls 함수에 정수값을 인자로 넘겨서 모든 원소가 null이고 인자로 넘긴 값과 크기가 같은 배열을 만들 수 있다 그리고 배열 크기와 람다를 인자로 받아서 람다를 호출해서 각 배열 원소를 초기화하는 방식도 존재한다

Last updated

Was this helpful?