Java에서 안전한 타입 변환을 위해 선택했던 방법
서버와의 HTTP 통신을 통해 응답을 받아오기 위해 다음과 같은 Response 인터페이스를 상속하는 간단한 클래스 Success와 Fail을 만들었다고 해보자.
sealed interface Response
data class Success(val responseBody: String) : Response
data class Fail(val errorMessage: String) : Response
기존 자바에서는 안전하게 타입을 변환하기 위해 타입을 변환하기 전에 instanceof 함수를 통해 타입을 체크한 후 다시 타입 변환을 해서 메서드를 호출해야 했다.
public class ResponseMain {
public static void main(String[] args) {
Response response = new Success("Success");
if (response instanceof Success) { // 타입 체크
String body = ((Success) response).getResponseBody(); // 타입 변환 후 메서드 호출
System.out.println(body);
}
}
}
더 나은 방법으로는 다음과 같은 방식으로 instanceof 구문에 변수를 캡쳐하고 형변환 하는 것을 한 번에 하는 방식도 있었다.
public class ResponseMain {
public static void main(String[] args) {
Response response = new Success("Success");
if (response instanceof Success successResponse) { // successResponse에 response 캡쳐 후, Success로 형변환
String body = successResponse.getResponseBody();
System.out.println(body);
}
}
}
하지만, 이 두 방식은 모두 변수의 타입을 확인 후, 해당 변수를 다시 타입 캐스팅 하거나, 변수를 다른 변수로 할당하는 작업을 거쳐야 하는 번거로움이 있었다.
이런 문제를 해결하기 위해서는 코틀린은 '스마트 캐스팅'이라는 기능을 제공한다. 지금부터 코틀린의 타입 변환 방법에 대해 알아보자.
코틀린의 타입 변환
코틀린에서도 자바와 마찬가지로 타입을 변환하기 위해 as 구문을 사용할 수 있다.
fun main() {
val response: Response = Success("Success")
val responseBody = (response as Success).responseBody // 자바의 ((Success) response).getResponseBody(); 와 같음
println(responseBody)
}
하지만, 이렇게 타입을 변환하게 되면, response의 타입이 Success가 아닌 경우까지 타입이 변환돼 오류가 발생할 수 있다. 다음의 코드를 살펴보자.
fun main() {
val response: Response = Fail("Fail")
val responseBody = (response as Success).responseBody
println(responseBody)
}
이 코드를 실행해보면, 다음과 같은 타입 캐스팅 오류가 발생한다.
간단한 프로그램에서는 간단히 수정하면 되지만, 프로덕션 레벨의 코드에서는 as를 통해 형변환을 하게 되면, 코드가 불안정하게 된다. 이를 해결하기 위해 코틀린의 스마트 캐스트 기능을 사용할 수 있다.
스마트 캐스트를 사용한 안전한 형변환
Java의 instanceof 와 비슷하게 코틀린에서는 특정한 변수의 타입을 확인하기 위해 is 문을 사용할 수 있다. 하지만, Kotlin의 is 문에는 추가적인 기능이 있는데, 바로 is를 통해 변수가 스마트 캐스팅 된다는 점이다. 스마트 캐스트(Smart Cast)란 타입 검사와 함께 해당 타입으로의 자동 형변환되는 기능으로, 코틀린에서는 is 문을 통해 특정한 타입임이 확인되면 자동으로 형변환된다.
위의 response 변수에 대해 다음과 같이 코드를 만들어보자.
fun main() {
val response: Response = Fail("Fail")
if (response is Fail) {
val errorMessage = response.errorMessage // 여기서 response의 타입은 Fail이 됨
println(errorMessage)
} else if (response is Success) {
val responseBody = response.responseBody // 여기서 response의 타입은 Success가 됨
println(responseBody)
}
}
이 코드에서 볼 수 있듯이 if 문에서 response가 Fail 타입임을 체크하면, if 블록 내부에서 response의 타입은 Fail로 자동으로 타입 캐스팅 돼 errorMessage에 접근할 수 있게 됨을 볼 수 있다. 또한 response가 Success인 경우에는 Success로 형변환돼 responseBody 필드의 접근이 가능해진다.
스마트 캐스트 더욱 우아하게 사용하기
코틀린의 sealed class 혹은 sealed interface, 그리고 when을 함께 사용하면 코틀린의 스마트 캐스트 기능을 더욱 우아하게 사용할 수 있다. sealed class는 자신을 상속하는 클래스만 when 문에 적어도 된다는 점을 기억하자. 그러면 위의 코드는 다음과 같이 만들 수 있다.
fun main() {
val response: Response = Fail("Fail")
when(response) {
is Fail -> {
val errorMessage = response.errorMessage // 여기서 response의 타입은 Fail이 됨
println(errorMessage)
}
is Success -> {
val responseBody = response.responseBody // 여기서 response의 타입은 Success가 됨
println(responseBody)
}
}
}
이렇게 만들면 나중에 Response를 상속하는 클래스가 추가되더라도 해당 when 문에 분기를 추가하지 않으면 컴파일 애러가 발생하기 때문에 더욱 안전한 코드를 만들 수 있다.