람다란
프로그래밍 언어에서 람다는 익명 함수(Anonymous Function)를 의미합니다. 수학자 알론조 처치(Alonzo Church)가 1930년대에 고안한 람다 계산법(λ-calculus)을 기반으로 합니다.
람다 계산법(λ-calculus) - 계산을 표현하기 위해 함수에 명시적인 이름을 부여하지 않고 취급하는 개념
람다의 탄생배경
1. 익명 클래스를 대체한다.
2. 코드 블록을 데이터로 취급한다. (함수를 일급 객체로서 취급)
람다는 익명 내부 클래스를 만들지 않아도 되게 만들어준다.
예시 1: Comparator 전달
// Java 7 스타일: 익명 내부 클래스 사용
Collections.sort(list, new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.length() - b.length();
}
});
// Java 8 스타일: 람다 표현식 사용
Collections.sort(list, (a, b) -> a.length() - b.length());
익명 내부 클래스 방식은 보일러플레이트 코드가 남발되어 가독성이 떨어집니다. 그리고 글 뒤에 다룰 변수 캡처 문제가 발생합니다.
람다내에서의 직관적인 외부 변수 사용 (Lexical Scoping)
public class OuterClass {
public int outerField = 10;
public void executeLambda() {
// 람다는 렉시컬 스코프를 따릅니다.
Runnable lambda = () -> {
// 이 'this'는 OuterClass의 인스턴스를 가리킵니다.
System.out.println("람다 'this' 참조: " + this.getClass().getName()); // -> OuterClass
System.out.println("필드 접근: " + outerField);
};
new Thread(lambda).start();
}
public void executeAnonClass() {
// 익명 클래스는 새로운 스코프를 만듭니다.
Runnable anon = new Runnable() {
@Override
public void run() {
// 이 'this'는 익명 클래스 자체의 인스턴스를 가리킵니다.
System.out.println("익명 'this' 참조: " + this.getClass().getName()); // -> OuterClass$1
// outerField 접근을 위해서는 OuterClass.this.outerField를 사용해야 합니다.
}
};
new Thread(anon).start();
}
}
람다는 새로운 레벨의 스코프를 만들지 않습니다. 즉, 정의된 주변환경(OuterClass)을 가장 바깥쪽의 스코프(next outermost scope)로 인식합니다.
반면에 익명 클래스는 명시적 스코프(lexical scope)를 따르지 않고, 가장 바깥쪽 스코프는 익명 클래스이기에 이는 비직관적입니다.
람다(클로저)의 변수 캡처(Variable Capture)
람다는 변수 캡처를 지원하여, 함수 리터럴이 둘러싸는 스코프 내의 변수 참조를 "클로즈 오버(close over)"할 수 있도록 합니다.
public class ClosureExample {
// 2. 외부 클래스의 인스턴스 필드 (인스턴스 스코프)
private int instanceValue = 5;
public void startClosure() {
// 1. 메소드의 지역 변수 (로컬 스코프)
String prefix = "결과: ";
// 람다가 정의된 영역. 이 바깥쪽이 '둘러싸는 스코프'입니다.
// 람다가 둘러싸는 스코프의 변수 'prefix'와 'instanceValue'를 '클로즈 오버'합니다.
IntConsumer lambda = (number) -> {
// 람다 본문 내부
int result = number + instanceValue;
System.out.println(prefix + result);
// 'prefix'와 'instanceValue'는 람다 본문 외부에 있지만, 람다가 캡처하여 사용합니다.
};
lambda.accept(10); // 출력: 결과: 15
}
}
자바의 변수 캡쳐 (final 변수만 가능)
익명 내부 클래스는 final 변수만 참조할 수 있습니다. 람다는 effectively final까지 참조할 수 있지만, 이는 단순한 syntax sugar에 불과하기에, 람다도 사실상 final 변수만 참조 가능하다고 생각해도 무방합니다.
public class AnonClassFinalConstraint {
public void startTask() {
// 1. 익명 클래스가 참조할 변수
final int finalCounter = 10; // ✅ 반드시 final (또는 effectively final) 이어야 함
// 2. Runnable 익명 내부 클래스 생성
Runnable task = new Runnable() {
@Override
public void run() {
// 익명 클래스 내부에서 final 변수만 참조 가능
System.out.println("익명 클래스가 캡처한 변수: " + finalCounter);
// finalCounter++;
// ❌ 익명 클래스 내에서 캡처된 변수는 변경할 수 없습니다 (final이기 때문).
}
};
// 스레드 시작 (동작을 위한 부분)
new Thread(task).start();
// 만약 nonFinalCounter를 참조하려고 했다면, 컴파일러는 다음 에러를 발생시켰을 것입니다:
// "Local variable nonFinalCounter is accessed from within inner class; needs to be declared final"
}
public static void main(String[] args) {
new AnonClassFinalConstraint().startTask();
}
}
제약의 이유
익명 내부 클래스(Runnable)가 생성되어 실행될 때, 외부 메소드(startTask)는 이미 실행이 끝나고 스택(함수콜스택)에서 사라집니다. 익명 클래스(Runnable)가 나중에 실행될 때 지역 변수(finalCounter)를 참조하려면, 그 변수의 사본이 익명 클래스 내부에 저장(캡처)되어야 합니다.
만약 이 변수가 final이 아니라서 외부 스코프에서 값이 변경될 수 있다면, 익명 클래스 내부의 사본 값과 외부 스코프의 원본 값 사이에 불일치(inconsistency)가 발생합니다. 이 문제를 방지하기 위해 Java는 final로 선언된 변수만 캡처를 허용했습니다.
코틀린의 변수 캡쳐 (Reference Wrapper)
코틀린은 참조를 캡처하는 메커니즘을 사용합니다.
fun runKotlinClosureExample() {
// 1. 캡처할 외부 변수 (var로 선언되어 변경 가능함)
var counter = 0 // Java에서는 이 변수를 람다에서 사용하려면 final이어야 함
println("초기 counter 값: $counter") // 출력: 초기 counter 값: 0
// 2. 람다 정의: 'counter' 변수를 캡처함
val incrementAndPrint = {
// 람다 내부에서 캡처된 외부 변수 'counter'의 값을 수정
counter += 1
println("람다 내부에서 수정 후: $counter")
}
// 3. 람다 실행
incrementAndPrint() // 람다 실행 1회
incrementAndPrint() // 람다 실행 2회
// 4. 외부 스코프에서 최종 상태 확인
println("외부 스코프에서 확인된 최종 counter 값: $counter") // 출력: 외부 스코프에서 확인된 최종 counter 값: 2
// 5. 외부 스코프에서도 변수 수정 가능
counter += 5
println("외부에서 추가 수정 후: $counter") // 출력: 외부에서 추가 수정 후: 7
}
runKotlinClosureExample()
/**
* 초기 counter 값: 0
* 람다 내부에서 수정 후: 1
* 람다 내부에서 수정 후: 2
* 외부 스코프에서 확인된 최종 counter 값: 2
* 외부에서 추가 수정 후: 7
*/
코틀린 람다가 외부 스코프의 지역 변수(counter)를 캡처할 때, 해당 변수는 더 이상 단순한 스택 변수가 아닙니다. 코틀린 컴파일러는 내부적으로 해당 변수(counter)를 객체 내부에 래핑(Wrap)하거나 힙(Heap) 메모리 영역으로 이동시킵니다.
람다와 람다를 둘러싼 외부 코드가 모두 동일한 Ref 객체(힙에 위치)를 참조하므로, 어느 쪽에서 값을 변경하든 항상 최신 값으로 일치합니다. 따라서 final 제약이 필요 없어지고 변수 수정이 허용됩니다.
람다의 타입
람다는 일급객체처럼 취급되기 때문에 인자로 전달 및 반환할 수 있어야 합니다. 고로 람다를 표현하는 타입이 필요합니다.
☕ 자바 (Java): SAM 인터페이스를 활용한 타입
자바는 함수 타입이 존재하지 않기 때문에, 기존의 객체 지향 메커니즘인 인터페이스를 활용하여 람다의 타입을 정의합니다.
🎯 원칙: SAM(Single Abstract Method) 인터페이스
자바 8에서 람다를 도입할 때, 람다는 추상 메소드가 딱 하나인 인터페이스(SAM 인터페이스, 즉 함수형 인터페이스)의 인스턴스로 취급됩니다. 람다는 이 SAM 인터페이스를 구현하는 객체(실제는 익명 내부 클래스로 컴파일되어 객체는 맞다)인 척합니다.
💻 샘플 코드: Runnable (SAM 인터페이스)
가장 흔한 SAM인터페이스인 Runnable을 람다의 타입으로 사용하는 예시입니다. 람다는 Runnable 타입을 "대상 타입(Target Type)"으로 갖게 됩니다.
// Java
// 자바는 이 람다를 'Runnable' 인터페이스의 인스턴스로 취급합니다.
// Runnable은 단 하나의 추상 메소드(void run())를 가집니다.
Runnable myTask = () -> {
System.out.println("자바: 람다를 Runnable 인터페이스 타입으로 취급");
};
// 람다를 인자로 전달 (Runnable 타입을 요구하는 메소드에 사용)
new Thread(myTask).start();
// 출력: 자바: 람다를 Runnable 인터페이스 타입으로 취급
🤖 코틀린 (Kotlin): 언어 차원의 함수 타입
코틀린은 JVM에서 실행되지만, 언어 설계 단계부터 **함수 타입(Function Types)**을 기본 문법 요소로 가지고 있습니다. 람다는 객체로 래핑될 필요 없이, 그 자체로 함수 타입의 값을 가집니다.
🎯 원칙: 함수 타입 문법
코틀린의 함수 타입은 (매개변수 타입) -> 반환 타입의 형태를 가집니다.
💻 샘플 코드: ()->Unit (함수 타입)
// Kotlin
// 코틀린은 이 람다를 '() -> Unit' 함수 타입의 값으로 취급합니다.
val myLambda: () -> Unit = {
println("코틀린: 람다를 () -> Unit 함수 타입으로 취급")
}
// 람다를 인자로 전달 (함수 타입 자체를 요구하는 고차 함수에 사용)
fun execute(f: () -> Unit) {
f() // 함수 타입의 값을 직접 호출
}
execute(myLambda)
// 출력: 코틀린: 람다를 () -> Unit 함수 타입으로 취급
요약: 자바는 "이 람다는 Runnable (함수형 인터페이스)이야"라고 말하고, 코틀린은 "이 람다는(Nothing)->Unit함수야"라고 말합니다.
'개발' 카테고리의 다른 글
| [Kotlin] Null을 대하는 자세: 꼬리표(Nullable)와 세계관의 분리 (1) | 2025.12.10 |
|---|---|
| [Kotlin] 컬렉션(Collection) vs 시퀀스(Sequence): 함수형 API와 동작 원리 심층 분석 (0) | 2025.12.08 |
| 싱글톤 패턴 (0) | 2025.03.30 |
| Index는 왜 B-Tree를 사용하나 (0) | 2025.03.16 |
| Ajax에 대해서 알아보자 (0) | 2025.03.02 |