튼튼발자 개발 성장기🏋️

Java Collections to Kotlin Collections 본문

프로그래밍/Kotlin

Java Collections to Kotlin Collections

시뻘건 튼튼발자 2025. 6. 15. 18:23
반응형

 

자바 가변 컬렉션을 코틀린 컬렉션으로 리팩토링하는 이유

프로그래밍 언어는 진화를 거듭하면서 성능보다 안정성, 생산성을 우선하는 방향으로 변화해 왔다. 특히 자바에서 코틀린으로의 전환은 개발자들에게 익숙한 문법 위에서 더 안전하고 직관적인 코드 작성을 가능하게 해준다. 그 중심에는 바로 코틀린 컬렉션이 있다.

Kotlin Collections은 Java Collections interface에서 상태를 바꾸는 메서드를 제거하고 kotlin.collections package 안에서 많은 Mutable Collections interface로 공개되어있다. 여기에 가변 컬렉션 인터페이스로도 확장하기도했다.

편리하지만 문제 투성이 자바 가변 컬렉션

자바의 컬렉션 프레임워크는 초기까지만 해도 혁신적이었다. Mutable List, Array, Set을 자유롭게 다룰 수 있었고, 정렬, 역순 등 다양한 조작 메서드를 지원하며 유연한 코딩이 가능했다. 그러나 이런 mutable은 예상치 못한 오류와 버그를 유발하기 쉽다.

대표적으로, 특정 컬렉션을 메서드 내부에서 sort()로 정렬하면 호출한 외부 컬렉션의 상태가 바뀌어버리는 일이 발생할 수 있다. 테스트가 통과된 리팩토링 이후 갑자기 버그가 나타나는 이슈 등은 이전 포스팅에서 언급한 공유된 가변 컬렉션 때문일 가능성이 크다.

코틀린으로 어떻게 해결할 수 있는가?

코틀린은 위와 같은 문제를 해결하기 위해 컬렉션을 불변과 가변으로 명확하게 구분한다. 예를 들어 List는 읽기 전용이고, 실제로 수정하려면 MutableList를 사용해야 한다. 이렇게 함으로써 의도치 않은 상태 변경을 방지할 수 있다.

다만 코틀린의 List는 진짜 불변이라기보다 [read-only]다. 내부적으로는 값 변경이 가능하므로, 완벽하게 상태 변경을 막으려면 불변 리스트를 생성하거나 복사해야 한다.

자바에서 가장 흔한 실수 중 하나를 예제 코드로 살펴보자.

List journeys = ...;
journeys.sort(comparing(Journey::getDuration).reversed());

이 방식은 리스트를 직접 변경한다. 하지만 코틀린을 사용한다면 아래와 같이 sortedByDescending()take()를 조합하여 불변 리스트로 정렬된 결과만을 리턴할 수 있다.

fun List.longestJourneys(limit: Int): List =
    sortedByDescending { it.duration }.take(limit)

이처럼 코틀린의 컬렉션은 가독성과 안정성을 모두 갖춘 설계로, 기존 자바 개발자들도 쉽게 적응할 수 있다.

 

코틀린으로 포팅할 자바 코드를 살펴보자.

public class Suffering {

    public static int sufferScoreFor(List<Journey> route) {
        Location start = getDepartsFrom(route);
        List<Journey> longestJourneys = longestJourneysIn(route, 3);
        return sufferScore(longestJourneys, start);
    }

    public static List<Journey> longestJourneysIn(
        List<Journey> journeys,
        int limit
    ) {
        journeys.sort(comparing(Journey::getDuration).reversed());
        var actualLimit = Math.min(journeys.size(), limit);
        return journeys.subList(0, actualLimit);
    }

    public static List<List<Journey>> routesToShowFor(String itineraryId) {
        var routes = routesFor(itineraryId);
        removeUnbearableRoutes(routes);
        return routes;
    }

    private static void removeUnbearableRoutes(List<List<Journey>> routes) {
        routes.removeIf(route -> sufferScoreFor(route) > 10);
    }

    private static int sufferScore(
        List<Journey> longestJourneys,
        Location start
    ) {
        return SOME_COMPLICATED_RESULT();
    }
}

longestJourneysIn() 메서드를 보면 파라미터로 받은 journeys 컬렉션을 변경함으로써 외부의 컬렉션이 변화한다.

removeUnbearableRoutes() 메서드는 void 를 반환함으로써, 컬렉션의 상태를 변환한다. 이를 불변으로 리팩토링해보자.

public class Suffering {

    public static int sufferScoreFor(List<Journey> route) {
        return sufferScore(
            longestJourneysIn(route, 3),
            getDepartsFrom(route));
    }

    static List<Journey> longestJourneysIn(
        List<Journey> journeys,
        int limit
    ) {
        var actualLimit = Math.min(journeys.size(), limit);
        return sorted(
            journeys,
            comparing(Journey::getDuration).reversed()
        ).subList(0, actualLimit);
    }

    public static List<List<Journey>> routesToShowFor(String itineraryId) {
        return bearable(routesFor(itineraryId));
    }

    private static List<List<Journey>> bearable
        (List<List<Journey>> routes
    ) {
        return routes.stream()
            .filter(route -> sufferScoreFor(route) <= 10)
            .collect(toUnmodifiableList());
    }

    private static int sufferScore(
        List<Journey> longestJourneys,
        Location start
    ) {
        return SOME_COMPLICATED_RESULT();
    }
}

 

자! 이제 Suffering java classKotlin으로 포팅해보자.

object Suffering {
    @JvmStatic
    fun sufferScoreFor(route: List<Journey>): Int {
        return sufferScore(
            longestJourneysIn(route, 3),
            Routes.getDepartsFrom(route)
        )
    }

    @JvmStatic
    fun longestJourneysIn(
        journeys: List<Journey>,
        limit: Int
    ): List<Journey> {
        val actualLimit = Math.min(journeys.size, limit)
        return sorted(
            journeys,
            comparing { obj: Journey -> obj.duration }.reversed()
        ).subList(0, actualLimit)
    }

    fun routesToShowFor(itineraryId: String?): List<List<Journey>> {
        return bearable(Other.routesFor(itineraryId))
    }

    private fun bearable(routes: List<List<Journey>>): List<List<Journey>> {
        return routes.stream()
            .filter { route -> sufferScoreFor(route) <= 10 }
            .collect(Collectors.toUnmodifiableList())
    }

    private fun sufferScore(
        longestJourneys: List<Journey>,
        start: Location
    ): Int {
        return SOME_COMPLICATED_RESULT()
    } 
}

 

자바 코드를 설명할 때와 마찬가지다. sorted() 함수는 코틀린에서 sortedByDescending으로 대체하고 subListtake로 바꿀 수 있다. 그리고 longestJourneysIn() 함수를 확장함수로 만들수도 있다!

longestJourneys가 파라미터를 변경하지 않고 있는걸 볼 수 있는데, 이 단일식 함수로 만들 수 있다.

object Suffering {
    @JvmStatic
    fun sufferScoreFor(route: List<Journey>): Int =
        sufferScore(
            route.longestJourneys(limit = 3),
            Routes.getDepartsFrom(route)
        )

    @JvmStatic
    fun List<Journey>.longestJourneys(limit: Int): List<Journey> =
        sortedByDescending { it.duration }.take(limit)

    fun routesToShowFor(itineraryId: String?): List<List<Journey>> =
        bearable(routesFor(itineraryId))

    private fun bearable(routes: List<List<Journey>>): List<List<Journey>> =
        routes.filter { sufferScoreFor(it) <= 10 }

    private fun sufferScore(
        longestJourneys: List<Journey>,
        start: Location
    ): Int = SOME_COMPLICATED_RESULT()
}

 

공유 데이터로 인한 버그 줄이기: 불변 리스트 전략

bearable을 살펴보자.

가변 컬렉션의 가장 큰 문제는 공유 상태다. 다양한 함수나 컴포넌트가 동일한 컬렉션을 공유할 경우, 그 중 하나라도 컬렉션을 수정하면 전체 흐름에 영향을 미칠 수 있다는 것을 명심하자.

코틀린에서는 아래와 같은 리팩토링을 통해 이를 방지할 수 있다.

fun bearable(routes: List<List>): List<List> =
    routes.filter { sufferScoreFor(it) <= 10 }

이렇게 불변 필터링 방식으로 처리하면, 외부 컬렉션은 전혀 손대지 않고도 원하는 결과만을 얻을 수 있다. 이는 유지 보수와 디버깅에서도 엄청난 이점을 제공한다.

 

자바 개발자가 코틀린으로 전환하는 업무를 맡았다면...

  • 가변 컬렉션은 반드시 범위를 제한하자
  • 공유되는 컬렉션에는 불변 리스트를 사용하자.
  • 불변 컬렉션이 비용이 크다고 느껴질 수 있으나, 디버깅 시간은 훨씬 더 크다.
  • 코틀린의 확장 함수와 고차 함수를 적극적으로 활용하자.

무엇보다도 중요한 건, [불변성은 코드를 더욱 직관적이고 안정적으로 만들어 준다.]는 점이다.

반응형

'프로그래밍 > Kotlin' 카테고리의 다른 글

Java Bean to Kotlin Value  (1) 2025.06.14
Java Optional to Kotlin Nullable Type  (1) 2025.06.04
Java Class to Kotlin Class  (0) 2025.06.04
[Kotlin] 리스트  (1) 2024.12.08
[Kotlin] 재귀와 공재귀  (0) 2024.12.07