코틀린을 배우기 위해서 인프런에서 강의를 구매하고 코틀린과 친해지고 기본기를 다지기 위해서 공부하는 중이다. 글 내용은 배열과 컬렉션 중 Kotlin, Java와 함께 Collection을 다루는 방법이고 최태현님의 자바 개발자를 위한 코틀린 입문(Java to Kotlin Starter Guide) 강의에 소금을 조금 친 내용이다
Kotlin과 Java는 상호 운용성이 뛰어나지만, 컬렉션을 공유할 때는 주의할 점이 있다. Java는 Kotlin처럼 가변성이나 null 가능성을 타입 시스템으로 명시적으로 구분하지 않기 때문이다
Java는 읽기 전용 / 변경 가능, Nullable / Non-nullable 타입을 구분하지 않는다
가변성 문제
- Kotlin은 불변 컬렉션을 Java로 넘길 때 Java 코드에서 이를 변경할 수 있다
// Kotlin: 불변 리스트 생성
val immutableList = listOf("A", "B", "C")
javaMethod(immutableList) // Java로 전달
// Java: 컬렉션 가변성을 구분하지 않음
public void javaMethod(List<String> list) {
list.add("D");
// Kotlin의 불변성 가정을 위반하여 예외 발생 또는 데이터 변경 가능성
// 런타임에 불변 컬렉션인 경우 UnsupportedOperationException 발생
}
Nullability 문제
// Kotlin: non-null 요소만 가질 수 있는 가변 리스트 생성
val nonNullList = mutableListOf("A", "B", "C")
javaMethodForNull(nonNullList) // Java로 전달
// Java: null 안전성을 구분하지 않음
public void javaMethodForNull(List<String> list) {
list.add(null);
// Kotlin의 non-null 가정을 위반
// 컴파일 시점에는 허용, 런타임에 Kotlin에서 접근 시 NullPointerException 위험
}
해결책 – Collections.unmodifiableXXX() 활용
- Kotlin에서 Java로 컬렉션을 전달할 때 java.util.Collections.unmodifiableList(), unmodifiableSet(), unmodifiableMap() 등의 메서드를 사용하여 Java 에서도 변경을 방지할 수 있다
import java.util.Collections
class KotlinService {
private val items = mutableListOf("A", "B", "C")
// Java에 안전하게 불변 리스트를 노출
fun getItemsForJava(): List<String> {
// 변경 불가능한 리스트로 감싸서 반환한다
return Collections.unmodifiableList(items)
}
}
- Java에서 getItemsForJava()로 반환된 리스트에 add(), remove() 등을 호출하면 UnsupportedOperationException이 발생하여 Kotlin을 불변성을 보장할 수 있다
Kotlin에서 Java 컬렉션을 가져다 사용할 때 플랫폼 타입을 신경 써야 한다
- Java 메서드가 반환하는 컬렉션은 Kotlin에서 플랫폼 타입으로 인식된다. List<Int!>, List<Int!>?, List<Int!?>와 같이 null 가능성에 대한 정보가 불분명해진다. !는 Kotlin이 null 가능성 정보를 알 수 없다는 의미이다
public class JavaCollectionService {
public List<Integer> getNumbers() {
// 실제 구현에 따라 null을 반환하거나, null 요소를 포함할 수 있다
// return null;
// return Array.asList(1, null, 3);
return Array.asList(1, 2, 3);
}
}
// Kotlin에서 받을 때 - 어떤 타입인지 불분명
val numbers = javaService.getNumbers() // List<Int!> (플랫폼 타입)
해결 방법
- Java 코드 분석: 가장 확실한 방법은 Java 소스 코드를 직접 확인하여 해당 메서드가 null을 반환하는지, 컬렉션 내부에 null 요소를 포함할 수 있는지 파악하는 것이다
- 적절한 래핑으로 영향 범위 최소화: Java 호출 지점을 Kotlin 클래스 내부에 래핑하여 플랫폼 타입의 모호함을 제거하고 안전한 Kotlin 타입으로 변환하는 것이 좋다
class JavaCollectionWrapper {
private val javaService = JavacollectionService() // Java 서비스 인스턴스
// null 리스트의 null 요소를 모두 처리하여 List<Int>로 변환
fun getNumbers(): List<Int> {
return javaService.getNumbers() // Java에서 Lis<Int!> 변환
?.filterNotNull() // 리스트가 null이 아니면, null 요소 필터링 (List<Int>로 변환)
?. emptyList() // 리스트 자체가 null이면 빈 리스트 변환
}
// null 요소가 있을 수 있는 List<Int?>로 변환
fun getSafeNumbers(): List<Int?> {
// 리스트 자체가 null이면 빈 리스트 변환 (요소는 Int? 유지)
return javaService().getNumbers() ?: emptyList()
}
}
방어적 검증
- 플랫폼 타입을 직접 사용할 때는 항상 null 체크와 null 요소 필터링을 통해 방어적인 코드를 작성해야 한다
fun processJavaList(javaService: JavaCollectionService) {
val javaList = javaService.getNumbers() // List<Int!>
// 안전한 사용을 위한 검증 및 타입 변환
val safeList: List<Int> = when {
javaList == null -> emptyList() // 리스트 자체가 null인 경우
else -> javaList.filterNotNull() // 리스트가 null이 아니면 null 요소 필터링
}
// 이제 safeList는 List<Int> 타입으로, null 걱정 없이 안전하게 사용 가능
safeList.forEach { number ->
println(number * 2)
}
}
플랫폼 타입 대응 원칙
- Java 코드 분석: 실제 Java 구현을 확인하여 null 가능성을 파악한다
- 경계에서 래핑: Kotlrin 코드의 가장자리(Java를 호출하는 지점)에서 플랫폼 타입을 감싸서 Kotlrin 타입 안정성을 확보한다
- 영향 범위 최소화: 플랫폼 타입이 Kotlin 코드 전체로 퍼지지 않도록 호출 지점에서만 처리하고 안전한 Kotlin 타입으로 변환한다
- 방어적 검증: null 체크와 필터링을 통해 안정성을 확보한다
출처 – 인프런 강의 중 자바 개발자를 위한 코틀린 입문(Java to Kotlin Starter Guide)