Kotlin – Java와 함께 컬렉션 사용하기

코틀린을 배우기 위해서 인프런에서 강의를 구매하고 코틀린과 친해지고 기본기를 다지기 위해서 공부하는 중이다. 글 내용은 배열과 컬렉션 중 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)