loop문에서 continue와 break를 사용해야 했다. Java에서 작성하던 방식과 비슷하게 작성햇는데 내가 생각한 대로 동작이 일어나지 않아 레퍼런스 문서를 찾게 되었고, 잘못 사용했다는 것을 깨달아 블로그에 정리한다.
1. break / continue
우리가 흔히 사용하는 break, continue는 for문 안에서 사용할 수 있다.
fun main() {
for (i in 1..5) {
if (i==3) break
print("$i ")
}
}
// 결과
1 2
문제는 Iterable<T>.forEach() 에서는 break, continue를 사용할 수 없다는 점이다. 아래와 같이 작성하면 break 때문에 compile이 불가능하다.
fun main() {
(1..5).forEach { i ->
if (i==3) break
print("$it ")
}
}
'break' and 'continue' are only allowed inside a loop
collection.forEach()는 for를 Iterable로 한번 감싼 형태이기 때문에 loop라고 인식을 하지 않는 듯하다.
나는 collection을 chaining하던 중이었기 때문에 위의 for()문은 사용할 수 없었고, 다른 방법을 찾아 보았다.
2. Label 사용
fun main() {
listOf(1, 2, 3, 4, 5).forEach {
if (it == 3) return
print("$it ")
}
println("다른 로직 처리")
}
// 결과
1 2
break 자리에 return을 사용하면 될줄 알았는데 아예 함수가 끝나버린다..ㅎㅎ
2-1. label을 사용한 continue
fun main() {
listOf(1, 2, 3, 4, 5).forEach {
if (it == 3) return@forEach
print("$it ")
}
println("다른 로직 처리")
}
// 결과
1 2 4 5 다른 로직 처리
return이지만 현재의 loop를 태깅하면 continue 동작이 실행된다...
2-2. label을 사용한 break
fun main() {
run {
listOf(1, 2, 3, 4, 5).forEach {
if (it == 3) return@run
print("$it ")
}
}
println("다른 로직 처리")
}
// 결과
1 2 다른 로직 처리
그리고 만약 코드에 run이나 forEach가 많다면 태그를 붙여 확실하게 return 하는 expression을 지정할 수도 있다.
fun main() {
run startForeach@ { // 이름 태깅
listOf(1, 2, 3, 4, 5).forEach loop@ {
if (it == 3) return@startForeach // 태깅한 이름을 지정
print("$it ")
}
}
println("다른 로직 처리")
}
이제서야 내가 원하는 break 방식을 찾았다. 그렇지만 불필요한 run을 한번 더 감싸는 방식이 마음에 들지 않았다.
다른 방식을 찾아보기로 했다.
3. Collection 함수 사용
3-1. filter()를 사용한 continue
만약 중간에 continue가 들어가야 한다면 사용할 수 없는 방법이지만, 처음부터 조건문을 사용하여 continue 문을 거는 경우라면 filter()를 사용해볼 수 있다. 조건에 따라 filterNot()을 사용할 수도 있다.
fun main() {
run startForeach@ {
listOf(1, 2, 3, 4, 5)
.filter { it != 3 }
// .filterNot { it == 3 }
.forEach {
print("$it ")
}
}
println("다른 로직 처리")
}
// 결과
1 2 4 5 다른 로직 처리
3-2. takeWhile()을 사용한 break
위와 마찬가지로 처음부터 조건문을 사용하여 break를 거는 경우라면 takeWhile()을 사용할 수 있다. takeWhile()은 자주 쓰지 않는 함수라 찾아보았다.
public inline fun <T> Iterable<T>.takeWhile(predicate: (T) -> Boolean): List<T> {
val list = ArrayList<T>()
for (item in this) {
if (!predicate(item))
break
list.add(item)
}
return list
}
조건문이 실행되기 전까지의 list를 반환하는 함수이다. filter와 다른 점은 filter는 전체 중 조건문에 해당하는 전체 리스트 반환이지만, takeWhile()은 조건문에 해당하지 않는 순간 loop가 종료되고, 그 순간까지의 리스트만 반환한다.
fun main() {
run startForeach@ {
listOf(1, 2, 3, 4, 5)
.takeWhile { it != 3 }
.forEach {
print("$it ")
}
}
println("다른 로직 처리")
}
// 결과
1 2 다른 로직 처리
결론
나는 개인적으로 2-2의 방법은 불필요한 run()을 작성하는게 별로라고 생각되어 3-2의 takeWhile()문을 사용하여 해결했다. 그러나 로직을 실행하다 break를 해야하는 순간에는 2-2를 선택할 수 밖에 없고, 어떻게 보면 그게 더 직관적인 코드일 수도 있다. 이것은 개인이 취향이기 때문에 여러가지 방법을 알아두고 사용하는 것이 좋겠다.
참고1: https://kotlinlang.org/docs/returns.html#break-and-continue-labels
참고2: https://www.baeldung.com/kotlin/break-continue-functional-loop