본문 바로가기

코틀린

코틀린(Kotlin) - 고차 함수 안에서 흐름 제어

루프(Loop) 같은 명령형 코드를 람다로 쓰기 시작하게 되면 반드시 return 문제에 부딪히게 되실 겁니다.

일반적으로 흔히 알고 있는 루프 안에서 return을 하게 될 때와 달리, object.forEach() { } 와 같이 람다 안에서의 return을 하게 되면 어떻게 되는지 살펴보도록 하겠습니다.

 

람다 안의 return문 : 람다를 둘러싼 함수로부터 반환

 

/* 일반 루프 안에서 return 사용 예제 */
fun example() {
   val list = listOf(1, 2, 3, 4, 5)
   
   for (element in list) {
      if (3 == element) {
         println("return!")
         return
      }
      
      println(element)
   }
}

>>> example()
1
2
return!

 

위의 for문을 forEach 람다 함수로 써도 루프의 기능은 동일합니다.

 

/* forEach에 전달된 람다에서 return 사용 예제 */
fun example() {
   val list = listOf(1, 2, 3, 4, 5)
   
   list.forEach {
      if (3 == it) {
         println("return!")
         return
      }
      
      println(it)
   }
}

>>> example()
1
2
return!

 

람다 안에서 return을 사용하면 람다뿐만 아니라 그 람다를 호출하는 함수의 실행을 끝내고 반환됩니다.

이렇게 자기 자신을 둘러싸고 있는 블록보다 더 바깥에 있는 블록을 반환하는 return 문을 넌로컬(non-local) return 이라고 부릅니다.

 

이렇게 return이 바깥쪽 함수를 반환시킬 수 있는 경우는 람다를 인자로 받는 함수가 인라인 함수인 경우뿐입니다.

예제에서 forEach는 인라인 함수이므로 람다 본문과 함께 인라이닝 됩니다.

따라서 return식이 바깥 함수를 반환하도록 쉽게 컴파일할 수 있습니다.

하지만 인라이닝 되지 않는 함수에 전달되는 람다 안에서는 return을 사용할 수 없습니다.

이유는, 인라이닝되지 않는 함수는 람다를 변수에 저장할 수 있거나 바깥쪽 함수를 반환한 뒤에 람다가 호출될 수도 있기 때문입니다.

 

람다로부터 반환 : 레이블(label)을 사용한 return

람다 식에서도 로컬(local) return을 사용할 수 있습니다.

람다 안에서 로컬 return은 for 루프의 break, continue와 비슷한 역할을 합니다.

로컬 return은 람다의 실행을 끝내고 람다를 호출했던 코드의 실행을 계속 이어갑니다.

로컬 return과 넌로컬 return을 구분하기 위해서는 레이블(label)을 사용해야 합니다.

return으로 실행을 끝내고 싶은 람다 식 앞에 레이블을 명시하고, return 키워드 뒤에 그 레이블을 추가하면 됩니다.

 

/* 레이블을 통해 로컬 return 사용 예제 */
fun example() {
   val list = listOf(1, 2, 3, 4, 5)
   
   list.forEach lableName@{  //람다 식 앞에 레이블을 붙입니다.
      if (3 == it) {
         println("return!")
         return@lableName  //앞에서 정의한 레이블을 참조합니다.
      }
      
      println(it)
   }
}

>>> example()
1
2
return!
4
5

 

/* break처럼 로컬 return 사용 예제 */
fun example() {
   val list = listOf(1, 2, 3, 4, 5)
   
   run labelName@{  //레이블이 달린 run 함수를 반환
      list.forEach {
         if (3 == it) {
            println("return!")
            return@labelName
         }
         
         println(it)
      }
   }
   
   println("finish")
}

>>> example()
1
2
return!
finish
/* 레이블로 바깥 loop 벗어나기 사용 예제 */
fun example() {

   labelName@ for (j in 1..5) {
      println("j = ${j}")
      
      for (k in 1..5) {
         println("k = ${k}")
         if (3 == k) {
            println("break!")
            break@labelName  //레이블이 달려있는 loop에 대해 break로 벗어난다.
         }
      }
   }
}

>>> example()
j = 1
k = 1
k = 2
k = 3
break!

 

람다 식에 레이블을 붙이려면 레이블명 뒤에 @ 문자를 추가하여 람다 블록 앞에 명시하면 됩니다.

그리고 return 문에서 @ 문자와 레이블명을 차례로 추가하면 됩니다.

 

 

람다에 레이블을 붙여서 사용하는 것 말고도 인라인 함수의 이름을 그대로 return 뒤에 레이블로 사용해도 됩니다.

 

 

람다 식 안에서 this로 전달받는 식 안에서도 레이블 규칙이 동일하게 적용됩니다.

 

/* this로 전달받는 람다식에서 레이블 사용 예제 */
val text = StringBuilder().apply sb@{  //수신객체에 레이블을 달아줌.
   listOf(1, 2, 3).apply {
      this@sb.append(this.toString())  //바깥쪽 수신객체는 this@label로 접근하고,
   }                                   //안쪽 영역은 기존과 동일하게 this로 접근
}

>>> println(text)
[1, 2, 3]

 

하지만 람다 안에서 여러 케이스에 따라 return을 해줘야 하는 식에서는 위와 같이 로컬 return과 넌로컬 return이 혼재되어 있다면 개발자들이 파악하기가 쉽지 않을 수 있습니다.

 

그렇기 때문에 코틀린은 코드 블록을 여기저기 전달하기 위한 다른 해법을 제공해주며, 그 해법을 사용하면 넌로컬 반환문(바깥 함수까지 리턴 시키는 것)을 여러 번 사용해야 하는 코드 블록을 쉽게 작성할 수 있습니다!

 

무명 함수 : 기본적으로 로컬 return

무명 함수는 코드 블록을 함수에 넘길 때 사용할 수 있는 다른 방법입니다.

 

/* 무명 함수 안에서 return 사용 예제 */
fun example() {
   val list = listOf(1, 2, 3, 4, 5)
   
   list.forEach(fun(element) {
      if (3 == element) {
         println("return!")
         return  //return이 적용되는 함수는 가장 가까이 있는 무명 함수 입니다.
      }
      
      println(element)
   })
}

>>> example()
1
2
return!
4
5

 

이렇듯 무명 함수 안에서 return은 무명 함수 자체만 반환시킬 뿐, 무명 함수를 둘러싼 바깥쪽 함수까지 반환시키지는 않습니다.

즉, 이미 우리가 알고 있는 return이 적용되는 규칙대로 가장 가까이 있는 함수에 대해서만 반환된다고 이해하시면 됩니다! ^^