타입 자리에 무엇이 들어가는가
가장 큰 구조적 차이는 람다를 담는 타입입니다. Kotlin에는 (Int) -> String 같은 함수 타입이 문법에 있어서, 함수를 인자로 받는 API를 자연스럽게 설계할 수 있습니다. Java에는 함수 타입이 없고 대신 추상 메서드가 하나뿐인 인터페이스, 즉 함수형 인터페이스가 그 자리를 대신합니다. Function, Predicate, Consumer, Supplier 같은 이름을 외우게 되는 이유이며, 인자가 세 개인 함수를 받고 싶으면 인터페이스를 직접 만들어야 합니다.
여기서 실무적으로 번거로운 지점이 원시 타입입니다. Java의 제네릭은 원시 타입을 담지 못하므로 Function<Integer, Integer>는 박싱을 유발합니다. 그래서 표준 라이브러리에 IntFunction, IntPredicate 같은 전용 인터페이스가 따로 존재합니다. Kotlin은 인라인 함수로 람다 호출 자체를 호출 지점에 펼쳐 넣어 이 비용을 피하는 별도의 길을 갖고 있습니다. PHP의 클로저는 객체이므로 생성 비용이 있고, 반복문 안에서 매번 새로 만들면 그만큼 할당이 늘어납니다.
람다를 쓰지 않는 편이 나은 경우
람다는 코드를 짧게 만들지만 짧은 것이 목표는 아닙니다. 세 언어 모두에서 다음 경우에는 이름 있는 함수로 빼는 편이 낫습니다. 본문이 서너 줄을 넘어갈 때, 같은 로직이 두 곳 이상에서 필요할 때, 그리고 단위 테스트를 따로 붙이고 싶을 때입니다. 특히 스택 트레이스가 문제입니다. 중첩된 람다에서 예외가 나면 트레이스에 의미 없는 합성 이름이 늘어서고, 어느 단계에서 터졌는지 읽기 어려워집니다. 이름 있는 메서드로 한 단계만 빼도 디버깅 난이도가 확 내려갑니다.
Kotlin의 it, Java의 메서드 참조(String::length), PHP의 fn은 모두 짧은 변환 하나를 표현할 때 가장 잘 맞습니다. 반대로 조건 분기가 들어가고 예외를 다뤄야 하는 로직을 람다 안에 밀어 넣으면, 체인은 여전히 한 줄처럼 보이지만 읽는 사람은 각 단계에서 무슨 일이 일어나는지 추적하지 못합니다. 함수형 스타일의 이점은 간결함이 아니라 각 단계가 하는 일이 하나로 좁혀지는 데 있습니다.
람다와 붙어 다니는 고차 함수, 컬렉션 파이프라인 사용법은 Kotlin 함수형, Java 람다와 메서드 참조, PHP 함수형 문서에서 이어집니다. 일반 함수 선언과의 차이는 Function 비교에서 확인하실 수 있습니다.
세 언어를 오갈 때의 실무 메모
Java에서 Kotlin으로 넘어가면 함수 타입이 문법에 있다는 점 때문에 API 설계가 훨씬 자유로워지지만, Java 코드에서 Kotlin의 함수 타입을 호출할 때는 여전히 Function 계열로 보인다는 사실을 알고 있어야 합니다. 혼재된 프로젝트에서 시그니처가 예상과 다르게 보이는 이유입니다. 반대로 Kotlin에서 Java의 함수형 인터페이스를 받는 API를 호출할 때는 SAM 변환이 적용되어 람다를 그대로 넘길 수 있습니다.
PHP에서는 클로저가 객체라는 점이 오히려 유용하게 쓰입니다. bindTo로 클로저가 바라볼 객체를 바꿀 수 있어, 테스트에서 내부 상태에 접근하거나 DSL 형태의 설정 코드를 만드는 데 활용됩니다. 다만 이런 기법은 읽는 사람이 문맥을 추적하기 어렵게 만들기 때문에, 프레임워크 내부나 테스트 유틸리티 같은 좁은 범위로 한정하는 편이 낫습니다.
정리하면 세 언어의 람다는 문법의 길이가 아니라 캡처 규칙과 타입 표현에서 갈립니다. Java는 안전을 위해 캡처를 제한하고 인터페이스로 타입을 대신하며, Kotlin은 함수 타입과 가변 캡처를 언어에 넣어 표현력을 얻는 대신 부수 효과가 섞이기 쉬워졌고, PHP는 무엇을 붙잡을지 코드에 직접 적게 합니다. 어느 쪽이 낫다기보다 각자 무엇을 감수했는지를 알고 쓰는 것이 실수를 줄입니다.