목차
코틀린은 정적타입 언어입니다. 항상 타입은 고정되어 있고 그를 통해 잠재적인 오류가 발생하지 않도록 도와줍니다.
또한 기본적으로 nullsafe 를 지원하는 언어로 모든 타입은 기본적으로 null 을 사용할 수 없게 되어 있습니다.
1 | val s: String = 1 // ERROR |
이러한 특징 때문에 개발자는 함수의 형태 혹은 변수의 형태만 보고도 null에 대한 처리여부를 결정할 수 있기 때문에 코드를 작성하는데에 많은 이점이 생깁니다.
최상위 Any, 최하위 Nothing
Any
코틀린은 기본적으로 Any라는 타입을 상속받는 형태를 가집니다. 이는 자바의 Object와 같은 역할이라고 생각하시면 됩니다. 덕분에 모든 클래스는 자바처럼 equals, toString 같은 유틸리티 메소드를 가지게 됩니다. 그리고 추가적으로 지원하는 extention 함수를 통해 여러가지 기본함수 역시 지원합니다.
Nothing
Nothing은 자바에는 없는 코틀린 자체 클래스 입니다. 이름 그대로 아무런 값도 가지지 않는 타입으로 void 혹은 Unit 과 겹쳐 보일 수 있지만 이들과는 조금 다르다고 할 수 있습니다. 또한 Nothing 은 다른 클래스의 대체제로 사용 가능합니다.
1 | sealed class Either<out L, out R> { |
위 Either 코드 처럼 제네릭 L, R을 필요에 따라 Nothing 으로 교체하여 사용하지 않을 타입에 대해선 무시할 수 있습니다.
Nullable
null
null
은 비어있는 변수(혹은 아무것도 가리키지 않는 레퍼런스)를 표현하는데에 최고의 표현식이지만 그로 인해 개발자가 고려해야 할 경우 수가 늘어납니다.
간단한 예로 특정 사용자가 인증을 거쳤는지 확인하는 코드를 봅시다.
1 | class UserRepository { |
위 예제에서 사용자의 인증여부를 확인하기 위해 조회를 하는데, 존재하지 않는 ID로 요청하는 경우 isUserAuthroized
메소드에서 NullPointerException이 발생 할 수 있습니다. Spring Framework 처럼 탄탄한 백그라운드가 있을 경우 ExceptionHandler 같은 어플리케이션 전반적으로 통제되는 에러핸들러를 통해 간단하게 처리가 가능하지만 그렇지 않은 경우엔 이런 예상하지 못한 에러를 안정적으로 처리하기 위해 더 많은 코드가 필요하게 됩니다. 따라서 대부분의 경우 아래와 같이 null 포인터 인지 확인 코드를 추가하게 됩니다.
1 | public boolean isUserAuthorized(User user) { |
위 예제는 하나의 객체를 대상으로 하는 메소드이기 때문에 단일 조건을 통한 if 문을 통해 스크리닝 가능하지만 여러객체를 취급하는 경우엔 모든 객체에 대한 null 체크가 필요하기 때문에, 조건문이 복잡해지게 됩니다.
자바에서는 null 에 대응하기 위해 두 가지 방법이 있습니다.
- Optional
Optional은 데이터가 존재하지 않을 수도 있다. 를 표현해주는 클래스로 자바 8 버전에서 새롭게 추가된 클래스입니다. Optional을 이용해 위 예제를 아래와 같이 수정 할 수 있습니다.
1 | public Optional<User> getUser(String id) { |
1 |
|
1 | // v2 - modify when isUserAuthorized call |
getUser 에서 Optional로 감싼 User 객체를 리턴하게 변경했습니다.
첫번째 방식은 isUserAuthroized 메소드의 파라메터를 바꾼 형태, 두번째는 isUserAuthroized 메소드를 호출할 때의 형태를 바꾼 경우 입니다.
두 방식 모두 어떻게든 optional 을 풀어내 원하는 비지니스 로직을 실행해야 합니다. (다른 방식으로 isPresent를 이용한 방식도 있지만 코드 구성자체는 null 을 사용할 때와 동일하기 때문에 따로 기술하지 않았습니다.)
위 코드 처럼 Optional 을 적용 할 경우 추가 메소드 호출이 필요하며 호출 방식이 바뀐다는 점으로 앞서 if 문으로 null 체크로 하는 것보다 코드가 더 복잡해지며, 객체를 추가로 감싸는 형태이기 때문에 이를 처리하기 위한 약간의 오버헤드가 필요하며, 결국엔 객체이기 때문에 실수로 null 을 리턴하게 하더라도 컴파일러가 확인하지 않아 잠재적인 버그 포인트가 될 수 있습니다.
- requireNonNull
requireNonNull
은 자바의 Objects
(s 주의) 컴패니온 클래스에 있는 유틸리티 메소드로 파라메터가 null 일 경우 NPE를 바로 발생시키는 메소드 입니다.
1 | User userMustExist = Objects.requireNonNull(userRepository.getUser(id)); |
메소드가 실행된 이후에는 해당 객체가 무조건 객체가 존재함을 보장하며 별도의 객체를 사용하는것 아니기 때문에 앞서 살펴본 Optional 보다 간편하다는 장점이 있지만 결국 NPE를 일으킨다는 점에서 경우에 따라 해결책으로 보기는 어려워 보입니다.
코틀린은 어떨까요?
코틀린에서는 기본적으로 null을 사용할 수 없습니다. 따라서 모든 변수엔 무조건 객체가 할당되어야 합니다.
따라서 코틀린에서는 아래와 같이 해결할 수 있습니다.
1 | object NonExistUser: User(authorized = false/*, ...*/) |
object
는 코틀린에서 제공하는 싱글톤 생성 지시어로 특정 클래스를 싱글톤 형태로 만들어줍니다. 여기서는 존재하지 않는 사용자에 대해 임의의 싱글톤 객체 NonExistUser
를 이용해 사용자가 없을 때 null 대신 사용하게 할 더미 객체로서 사용됩니다. 이로 인해 getUser 는 무조건 객체를 리턴함을 보장받고 getUser의 호출 부에는 별도의 null 체크 없이 객체 사용이 가능합니다.
nullable
그렇다고 코틀린이 무조건 null 을 배척하지는 않습니다. Map 에서 키를 통해 데이터를 추출하거나, 자바와 연동되는 경우 null을 사용 할 수 도 있습니다. 그런 경우를 위해 코틀린에서는 별도의 Nullable 타입을 지원합니다. 사용법은 기존 타입에서 ?
가 추가된 형태로 사용됩니다.
1 | val nullStr: String? = null |
자바의 Optional과는 다르게 Nullable 타입은 컴파일 타임에 검사하기 때문에 좀 더 안전합니다.
위 사용자 인증 예제를 nullable 타입으로 교체하면 어떨까요?
1 | fun getUser(id: String): User? = connection.callQuery(selectUserQuery, id) |
위에서 선언한 default객체는 제거하고 사용하는 메소드의 파라메터를 nullable하게 수정했습니다.
그리고 nullable 객체를 사용할 때 기존의 .
가 아닌 ?.
인 다른 형태로 프로퍼티를 실행하는 것을 볼 수 있습니다. 이렇게 실행할 경우 null인 경우 실제 해당 객체에 접근하는것이 아닌 바로 null로 처리합니다. 아마 자바였다면 이렇게 표시되게 됩니다.
1 | public Boolean isUserAuthorized(User u) { |
만약 isUserAuthorized가 무조건 null이 아님을 보장하고 싶으면 어떻게 하면 될까요? 이럴 땐 첫번째 코틀린 예제에 나왔던 ?: 연산자(Elvis operator)를 사용하면 됩니다.
1 | fun isUserAuthorized(u: User?):Boolean = u?.authorized ?: false |
위 예제를 통해 u가 null일 때 false를 리턴하는것을 확인 할 수 있습니다.
when
when을 이용하여 null값을 처리할 수도 있습니다.
1 | fun getUser(id: String): User? = connection.callQuery(selectUserQuery, id) |
타입체크와 캐스팅
일반적인 객체지향 설계에서 타입체크를 한다는건 그렇게 좋은 신호는 아닙니다. 특정 상황에 특정 케이스에 대한 예외처리를 한다는 의미이기 때문에, 좋은 설계라고 보기도 힘들고 유지보수 부분에서도 이러한 복잡성을 고려해야하기 때문에 어렵다고 볼 수 있습니다.
그럼 우리는 무조건 타입을 지정하고 사용할까요? 원청에서 내려준 API의 결과가 복잡한 구조지만 결과가 제멋대로 바뀌는 JSON 문서를 처리할 때 우리는 Map<String, Any>
으로 받아 처리하는 방향으로 화를 삭힙니다. 그리고 객체지향에서 벗어났을 때 타입체크는 또다른 사용성을 보여줍니다.
단순체크
단순히 특정 객체의 타입을 체크하기 위해서는 is
라는 연산자를 사용합니다.
1 | val o: Any = "any string" |
그리고 스마트 캐스팅
코틀린의 컴파일러는 자바의 것보다 똑똑합니다.
1 | val o: Any = something |
자바였으면 o를 사용하기 전에 확인된 타입으로 타입캐스팅이 필요했지만 코틀린에서는 알아서 컴파일러가 해당 타입을 추론하여 변수의 타입을 특정 타입으로 인정해줍니다.
이는 아래의 when 문으로 정리할 수 있습니다.
1 | val o: Any = something |
각 세부 조건 절의 조건인 타입체크문을 통해서 해당 절의 when의 검사 대상 객체의 타입을 임의로 바꾸어 실행됨을 알 수 있습니다.
명시적 타입 캐스팅
명시적인 타입캐스팅은 as
연산자를 통해 이루어집니다.
일반적으로 스마트 캐스팅을 적용하기 어려울 때 사용합니다.
1 | val o: Any = something |
만약 위 예제에서 something이라는 변수가 SomeClass가 아니라면 ClassCaseException이 발생합니다.
만약 이를 회피하고 싶다면 as?
연산자를 사용할 수 있습니다. 이 연산자는 물음표에서 예상 할 수 있듯이 타입이 맞지 않을 경우 null을 리턴합니다.
제네릭
kotlin도 여타 다른 언어처럼 제네릭을 지원합니다.
기존 자바에서 지원하는 공변성(<? extends T>)은 out 이라는 키워드로 정의할 수 있고
반공변성(<? super T>)의 경우 in 이라는 키워드로 정의 할 수 있습니다.
타입 불변성
일반적인 클래스를 받는 메소드를 작성했을 때, 해당 클래스를 상속받은 클래스를 메소드로 넘기는 것엔 크게 문제가 되지 않습니다. 다만 제네릭에서는 조금 다릅니다.
List
Array를 예로 들면
1 | fun process(objs: Array<Any>) {} |
Array<Any>
와 Array<String>
에서 String은 Any의 자식 클래스이기 때문에 가능할 것 같지만 타입불변성에 의해 불가능합니다. 참고로 자바 배열은 경우에도 컴파일러가 막지 않습니다.
만약 위를 허용하게 된다면 파라메터로 넘어간 Array 내부에 기존에 정의된 타입과 다른 타입이 할당 될 수 있다는 문제가 있습니다. 자바의 경우 이럴 때 ArrayStoreException
이 런타임에 발생하게 됩니다. 네 런타임에 발생하기 때문에 잠재적인 오류 포인트가 될 수 있습니다.(물론 이렇게 쓰는 사람은 별로 없죠)
List는 어떨까요? 자바에서는 당연히 막힙니다. 하지만 코틀린은 다릅니다.
1 | fun process(objList: List<Any>) {} |
코틀린은 위 상황에서 오류를 발생하지 않습니다. 코틀린의 List는 내부 데이터가 변할일이 없기 때문에 이를 허용하도록 선언되어 있습니다. 따라서 데이터 수정이 가능한 MutableList는 타입 불변성을 유지합니다.
공변성
공변성은 위에서 하지못했던 일을 가능하게 해줍니다. 또한 List의 비밀도 여기서 밝혀집니다.
1 | fun process(objs: Array<out Any>) {} |
선언시에 타입 파라메터에 out 지시어를 추가함으로서 이 타입에 공변성을 추가합니다. 따라서 Any의 자식 클래스인 String의 Array도 받을 수 있게 됩니다. 하지만 여기에 process 함수 내에서 데이터 수정하려고 하면 실패하게 됩니다. 타입을 확인해보면 Nothing타입만 받을 수 있다고 나옵니다. 네 이렇게 받은 파라메터는 내부 데이터를 수정할 수 없게 됩니다.
반공변성
공변성에 반대되는 반공변성은 in이라는 지시어로 표시 되며, 특정클래스의 상위타입들을 타입 파라메터으로 가지고 있는 객체들을 받을 수 있습니다.
1 | fun process2(objs: MutableList<in Int>): MutableList<in Int> { |
또한 위 예제처럼 객체 내부 데이터도 수정할 수 있습니다. 이때 추가할 수 있는 객체는 타입 파라메터입니다. (위 예시에서는 Int) 다만 이상태에서 get등을 이용하여 데이터를 사용할 때는 Any 타입으로 인식 됩니다. 어디까지나 제네릭타입이 상속받는 클래스을 나타내기 때문입니다.
where를 이용한 타입 제한
자바에서 다중 타입에 대한 타입 파라메터 제한은 & 라는 키워드로 표현됩니다. 코틀린에서는 where이라는 키워드로 표현됩니다.
1 | fun <T> process(t: T) where T: Appendable, T: Closable { |
where 키워드 이후에 타입에 대한 제한조건을 콤마로 나누어 정의하면 됩니다.
raw타입과 와일드카드
자바는 제네릭타입을 가지는 클래스에 아무것도 지정하지 않는 방식으로 객체를 초기화 할 수 있습니다. raw타입이라고 불리는 이 타입은 프로젝트 내부에 타입안정성을 해치는 방식이기 때문에 코틀린에서는 따로 지원하지 않습니다. 대신 자바의 ?
처럼 *
라는 와일드카드를 지원합니다. 와일드카드를 통해 모든 타입을 받을 수 있지만 공변성처럼 데이터를 추가할 수 없는 상태가 되며, 내부 데이터 역시 Any로 처리 됩니다.
구체화된 타입 파라메터
자바에서 jackson을 통해 json 데이터를 특정 객체로 변환할 때 우리는 클래스의 구조를 내부 메소드로 전달하기 위해 클래스의 메타데이터 객체를 같이 전달합니다.
코틀린에서는 이와 같은 불필요한 데이터 전달을 막기 위해 구체화된 타입 파라메터 기능을 제공합니다.
1 | inline fun <reified T> parse(json: String): T = objectMapper.readValue(json, T::class) |
타입 파라메터 앞에 reified
라는 지시어를 추가하여 함수 내부에서 구체화된 파라메터 T를 선언합니다. 그리고 메소드 내부에 보시면 T에서 해당 클래스의 메타데이터를 추출할 수 있습니다.
구체화는 인라인 함수에서만 쓸 수 있는데, 이는 컴파일 타임에 해당 타입을 추론하여 함수 호출 부에 함수 내용을 변환하여 사용하는 형태를 가지게 됩니다.
출처
- 다재다능 코틀린 프로그래밍
- 코틀린 공식 문서