클로저가 뭔데? Kotlin으로 알아보자!

Murjune·2024년 4월 13일
2

kotlin/Java

목록 보기
5/5
post-thumbnail

람다를 실행 시점에 표현하는 데이터 구조는 람다에서 시작하는 모든 참조가 포함된 닫힌(closed) 객체 그래프를 람다 코드와 함께 저장해야 한다.
그런 데이터 구조를 클로저(closure)라고 부른다.
(출처: Kotlin in Action)

Kotlin In Action 을 읽으면서 Closure 라는 용어와 이에 대한 설명이 나오는데
도저히 무슨 말인지 이해가 되지 않는다..

클로저에 대한 문헌들을 찾아보면 렉시컬 스코프(Lexical Scope), 렉시컬 환경 와 같은 어려운 용어와 JS 코드만 계속해서 나온다..

상당히 킹받은 필자는 Kotlin 예시 코드를 통해 Closure 와 관련된 용어들을 쉽게 풀어서 정리해보고자 한다! 💪

최대한 복잡한 내용은 걸러내고, 필수적인 내용만 담으려고 노력했다!

Scope(접근 가능한 영역)

Scope변수 혹은 함수에 접근 가능한 영역(Scope) 을 뜻한다.

해당 용어를 처음 본 사람은 무슨 말인지 한 번에 이해하기 힘들 것이다.

코드를 통해 이해해보자 🤓

중첩 함수 innerFunction 와 이를 포함하고 있는 outerFunction 이 있고, 각각 지역변수(outerVariable, innerVariable)를 가지고 있다.

스코프(접근 가능한 영역)을 노란색 영역으로 표시해두겠다!
outer 함수, inner 함수, outer 변수, inner 변수의 스코프를 눈으로 확인해 보자! 👀

1) outerFunction 의 스코프(접근 가능한 영역)

outer 함수는 코드 전체에서 접근 가능하므로, 프로젝트 전체가 접근가능한 스코프 이다!

2) outer 변수 , inner 함수 의 스코프

outer 지역 변수와 inner 함수는 outer 함수의 구현 부분({}) 이 렉시컬 스코프다.

3) innerVariable 의 스코프:

inner 지역 변수는 inner 함수에서의 구현 부분({}) 이 스코프다.

이제 스코프가 어떤 것인지 확실히 아셨쥬? 😸

Closure

Closure (위키백과): 함수와 그 주변 환경을 저장한 레코드를 클로저라 말한다.)

말이 너무 어렵다.. 🥲
좀 더 길게 풀어쓰자면, 아래 조건을 만족하는 함수를 Closure라 한다.

Outer 함수가 실행이 종료된 시점에도
inner 함수가 Outer 함수의 변수(자유 변수)/함수를 사용할 수 있을 때
inner 함수를 Closure라 한다.

역시나 무슨 말인지 모를 것이다..🥲
(만약 바로 이해하셨으면 당신은 천재!)

이번에도 간단한 예제를 통해 이해해보자! 💪

Closure 예시

outer 함수 : inner 함수의 함수 레퍼런스를 반환
inner 함수 : outer 함수의 지역변수 outerVariable 을 return 하는 함수이다.

:: 가 무엇인지 궁금해할 독자들이 있을 수 있는데
일단은 outer 함수가 { outerVariable } 을 반환하고 있다고 생각해도 좋다!
(더 궁금한 사람을 위해 KFunction 라는 키워드를 드리겠다.)

이제 main() 함수에서 outer 함수를 호출한 뒤, inner 함수(closure)를 호출하면 outer 지역 변수 라는 출력문을 확인할 수 있다.

뭔가 이상하지 않은가? 🤨
어떻게 outer 함수의 지역변수 값(outer 지역 변수) 를 출력할 수 있는걸까?

inner 함수를 실행하는 시점에서는 이미 outer 함수는 실행이 종료되었다.
따라서, outer 지역변수 또한 생명을 다했다.

그런데, 어떻게 inner 함수에서는 outer 지역변수를 반환할까?

😎 inner 함수가 outerVariable 를 저장(capture)하고 있기 때문이다.
(capture한 변수를 자유변수라 한다.)

✚ JS, Java, kotlin 모두 자유 변수를 포획하는 방법이 다른데
글이 너무 길어지는 것 같아 다음 글에서 구체적으로 작성하겠다! 💪

inner 함수가 내부적으로 outer 지역 변수를 저장하고 있기 때문에
main()에서 outer 지역 변수 를 print 할 수 있는 것이다.

이제 다시 Closure 의 정의를 살펴보자! 😀

1) 내부 함수가 자신을 가지고 있는 외부함수보다 오래 살아 있을 경우
2) 내부 함수의 스코프 밖에서 호출할 때도 외부 스코프의 변수(자유 변수)/함수를 참조할 수 있을 때

inner 함수는 위 조건을 모두 만족한다.
따라서, inner 함수 는 Closure 다.

정리

  • Scope: 변수/함수에 접근 가능한 영역
  • Closure = 함수 + 함수의 외부 환경(e. 자유변수)

함수가 선언될 당시의 외부 환경을 Capture 하는 개념이 Closure 다!

원래 Closure의 장점에 대해서도 따로 작성을 하려했는데
Kotlin 에서 JS 만큼 Closure를 자주 사용할 일이 있을지 모르겠다...

JS에서는 옛날에 private 변수가 없었고, Global 변수를 줄이기 위해 아래와 같은 형태로 Closure를 자주 사용했다고 한다.

(수정) 이제 JS도 private 이 있다고 한다!

data class Closure(val increaseCount: () -> Unit, val printCnt: () -> Unit)

// Closure - kotlin으로 흉내 내봤다.😆
fun createClosure(): Closure {
    var cnt = 0

    fun increaseCount() = cnt++

    fun printCnt() = println(cnt)

    return Closure(::increaseCount, ::printCnt)
}

fun main() {
    val (increaseCount, printCnt) = createClosure()
    printCnt() // output: 0
    increaseCount()
    increaseCount()
    increaseCount()
    printCnt() // output: 3
}

🤔 Kotlin에서는 class로 만들면되는데..?

// 그냥 이렇게 하면 되지 않나?
class Foo() {
    private var cnt = 0

    fun increaseCount() = cnt++

    fun printCnt() = println(cnt)
}

아직 Kotlin 에서는 Closure를 언제 사용해야할지 잘 모르겠다.

외부 변수를 람다나 익명 클래스에 포획하는 것 자체가 클로저이니
자주 사용하고 있다!

가장 최근에는 어뎁터에 람다를 넘길 때 사용했다! (presenter 포획!)

후기 및 Next Article

레벨 1 방학 때 갑자기 클로저에 빠져가지고 심해까지 갔다 온 거 같다.
처음에 작성한 글이 너무 어렵고, 딥하다는 피드백을 들어 최대한 쉽게 쓰려고 수도 없이 뜯어 고쳤는데
이해가 잘 될지 모르겠다.. 🥲

다음 아티클)

  • Kotlin Closure(Lambda)는 어떻게 변수를 포획할까?
  • Java 클로저의 한계

좀 더 궁금한 당신을 위해 🤚

해당 Chapter 는 보지 않아도 된다.

Closure 에 대한 문서들을 보면 JS 용어와 JS 코드가 상당히 많이 나온다.

Variable Environment, Lexical Environment, Lexical Record... 등등

Kotlin/Java 와 같은 JVM 언어 들은 Runtime 에 위 방식을 따르지 않기 때문에 딱히 알 필요는 없다.

1) Lexical Environment

렉시컬 환경: 실행할 스코프 범위 안에 있는 변수와 함수를 프로퍼티로 저장하는 자료구조이다.

쉽게 말해서 스코프 + 스코프 내 변수(local, outer)들을 모아놓은 자료구조다.

val y = 20
fun createClosure(): () -> Unit {
   val x = 10
   val closure = { println(x + y) }
   return closure
}

위 코드의 closure 의 렉시컬 환경은 Scope + 캡쳐한 변수들(x, y) 이다.

여기까지만 알아도 아니 사실 몰라도 괜찮다!

JS 에서는 변수를 포힉하지 않고 외부 렉시컬 환경이라는 개념을 횔용해서
자유 변수들을 가져온다.


사진 출처: jangsebari.log

만약 궁금하다면 MDN 문서 혹은 우테코 프론트 센빠이 꼬재가 올려두신
테코톡 영상을 참고하면 이해하기 쉽다~!!

2) 동적 스코프(Dynamic Scope) vs 정적 스코프(Lexical Scope)

동적 Scope정적(렉시컬) 스코프 를 비교하는 내용이 자주 나온다.

해당 내용이 크게 중요하지 않는다고 생각해 본문에는 따로 설명하지 않았지만
해당 개념을 궁금할 수 있는 독자들을 위해 간단하게나마 정리하고자 한다 🤓

1) 동적 스코프(Dynamic Scope): 함수가 호출되는 시점에 스코프가 결정
2) 정적 스코프(Lexical Scope): 함수가 정의되는 시점에 스코프가 결정

코틀린에서는 정적 스코프를 따른다.

fun createClosure(): {
	val x = 10
	return { println(x) }
}

fun main() {
	val closure = createClosure()
    val x = 20
    closure() // outPut: 10
}

위 코드를 실행하면 10 이 출력 된다.
컴파일 시점에 createClosure() 지역 변수 x가 람다식의 자유변수로 들어간다.(정적 스코프)

fun main() {
	val closure = createClosure()
    val x = 20
    closure() // outPut: 20
}

만약, kotlin 이 동적 스코프 개념을 따른다면
main() 함수의 결과가 10이 아닌 20이 나올 것이다.
즉, closure() 가 호출되는 시점에 근처 있는 x 변수가 자유변수로 들어간다.(동적 스코프)

profile
열심히 하겠슴니다:D

0개의 댓글