map 함수 안에서 외부 변수를 바꿔도 괜찮을까?

순순·2025년 4월 7일

코틀린

목록 보기
8/8

요즘 알고리즘 문제를 풀어보기 시작했다. 그러면서 새로 들인 습관이 있다. 내가 생각한 코드로 정답을 맞춘 뒤 GPT에게 코드를 더 짧게 리팩토링 해달라고 하는 것이다. 코드가 짧아지는 대신 실행 시간이 늘어나는 경우도 있고 아닌 경우도 있고, 미처 생각지 못한 접근법을 제시해주기도 한다. 이것저것 차이를 관찰하는 재미가 나름 쏠쏠...

궁금한 점


아무튼 이번에도 백준 문제를 풀고 나서 GPT 에게 내가 짠 코드를 더 짧게 바꿔달라고 했고, 얘가 map 함수를 쓰는 과정을 보고 예상치 못한 궁금증이 생겼다. 바로 map 함수 안에서 외부 변수인 amount를 바꾸는 게 괜찮은가? 하는 점이었다. 이걸 이해하고 나니 map에 대한 시야가 좀 더 넓어진 것 같아 이 경험을 정리해두려 한다.

왜냐하면 나는 여태 map 에 들어가는 고차함수 내부에는 map 의 요소와 관련된 연산만 해야한다고 생각했기 때문이다.......... 나 같은 사람이 어딘가에 한 명쯤은 있지 않을까.


문제 코드


fun main() {
    val coin = listOf(25, 10, 5, 1)
    repeat(readln().toInt()) {
        var amount = readln().toInt()
        val result = coin.map {
            val count = amount / it
            amount %= it
            count
        }
        println(result.joinToString(" "))
    }
}

위 코드를 처음 봤을 때 내가 제일 먼저 걸렸던 건 이 부분이었다. map 안에서 amount를 바꿔도 되는 걸까?


클로저(Closure)란?


처음엔 굉장히 어색하게 느껴졌다. 왜냐면 map은 각 요소에 대해 어떤 연산을 적용해서 새로운 리스트를 만들어내는 함수다. 나는 그동안 map 안에서는 오로지 it에만 집중해서 연산을 하고, 바깥 상태는 건드리지 않는 게 맞다고 알고 있었다.

하지만 코틀린의 람다는 외부 변수를 참조할 수 있고, 그 값을 바꾸는 것도 가능하다. 이를 클로저(closure)라고 부른다. 쉽게 말해서 람다 함수 안에서 바깥쪽 변수의 값을 읽거나 수정할 수 있다는 것이다. 코틀린 공식문서 에 보면 다음과 같이 설명되어 있었다.

다음과 같은 문장이 눈에 띈다.

Lambdas can access variables declared in the outer scope. Such variables are said to be captured by the closure.

이 문장을 이해하려면 스코프(scope)의 개념을 먼저 짚고 넘어가야 한다. viewModel 스코프를 그렇게 써놓고 이제서야 개념을 이제 확실히 알게 된


scope
스코프는 변수나 함수가 유효한 범위를 의미한다. 예를 들어, 함수 안에서 만든 변수는 그 함수 밖에선 쓸 수 없다. 그 변수의 스코프는 “함수 내부”로 제한되기 때문이다.

fun example() {
    val x = 10  // x는 example 함수 안에서만 유효
}

closer
반면 클로저는 조금 다르다. 위에서도 설명했듯이, 클로저란 람다 함수가 바깥에 있는 변수들을 기억하고 사용할 수 있는 함수다. 이때 람다가 바깥 변수를 “캡처”했다고 표현한다. 참 재밌는 표현이다. 클로저의 개념이 이해될 간단한 예시를 살펴보자.

fun main() {
    var count = 0

    val inc = {
        count += 1
    }

    inc()
    inc()
    println(count) // 2
}

람다 inc는 바깥에 선언된 count에 접근해서 값을 바꾸고 있다. 바로 이게 클로저다. Kotlin은 count라는 변수를 람다 내부에서 쓸 수 있 캡처(capture)해서 기억해두고 있는 것이다. 덕분에 map 안에서도 바깥의 amount를 변경할 수 있었던 것!

따라서 amount %= it처럼 바깥 변수를 바꾸는 코드가 map 안에 있어도 전혀 문제 없이 동작한다. 오히려 이렇게 했기 때문에 루프를 따로 안 돌리고도 각 동전 개수를 구할 수 있었다.

유의사항

추가로 알아야 할 것이 있다. Kotlin에서 map은 반드시 각 요소마다 반환할 값을 명시해야 한다. 코드를 다시 살펴보자.


*올바른 예시
이렇게 count가 마지막 줄에 있어야 그 값이 result 리스트에 들어간다.

val count = amount / it
amount %= it
count // ❗ 마지막 이 줄이 없으면 map 결과는 Unit 리스트가 된다

*잘못된 예시
마지막 줄에 아무것도 없거나 println() 같은 걸 쓰고 끝내면 map 함수는 Unit 리스트를 반환하게 된다.

왜냐면 println() 자체는 값을 반환하지 않고, Kotlin에서는 아무것도 반환하지 않으면 Unit이라는 타입을 반환한다고 간주한다. 그래서List<Unit>이 되는 것이다.

val result = listOf(1, 2, 3).map {
    println(it)
}
println(result)

*잘못된 예시의 출력값
kotlin.Unit, kotlin.Unit, kotlin.Unit


따라서 map 안에서는 반드시 반환할 값을 마지막 줄에 써줘야 한다.


정리


  • 필요한 경우 map 안에서 외부 변수(amount)를 수정해도 된다. Kotlin에서는 이를 허용한다.
  • map의 마지막 줄에는 꼭 반환값이 있어야 한다. 그래야 그 값들이 새로운 리스트를 구성한다.
  • 불변성이나 부작용을 신경 써야 할 상황에서는 주의가 필요하겠지만, 이번처럼 단순한 문제에선 오히려 가독성과 간결함을 높여주는 방향이 될 수 있다.

깨달은 점


처음엔 map 안에서는 요소(it)에만 집중해야 한다고 생각했는데, 외부 변수도 충분히 사용할 수 있다는 걸 알게 되었다. Kotlin이 이런 걸 허용해주는 언어라는 점도 새롭게 이해했다.

물론 외부 변수를 바꾸는 방식은 함수형 스타일에서는 부작용(side effect)이라고 해서 지양되기도 한다. 하지만 이번 문제처럼 간단한 연산에서는 오히려 코드가 더 직관적이고 깔끔해지기 때문에 사용해도 좋을 것 같다. 문제 풀이용이니까..

개인적으로 이번 경험은 map이라는 익숙한 함수의 내부 동작을 다시 생각해보는 계기가 됐다. 고차함수를 쓸 때 꼭 ‘원칙’만 지켜야 한다고 생각하기보다, 언어가 허용하는 유연함 안에서 적절한 선택을 하는 것이 더 중요하다는 걸 새삼 느꼈다.

profile
플러터와 안드로이드를 공부합니다

0개의 댓글