1. 람다 표현식의 장점
람다 표현식은 단순한 문법 설탕이 아니라, Java를 함수형 스타일로 한 걸음 더 나아가게 합니다. 아래에서 실제 장점과 현대 코드에서 람다가 왜 그렇게 쓰기 편한지 설명합니다.
간결함과 표현력
람다 이전에는 간단한 “로컬” 코드도 많은 보일러플레이트가 있는 익명 클래스로 변하곤 했습니다. 예를 들어, 문자열을 길이로 정렬하기:
Java 8 이전(익명 클래스):
list.sort(new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.length() - b.length();
}
});
람다 표현식 사용:
list.sort((a, b) -> a.length() - b.length());
코드가 더 짧고 “길이 차이로 정렬”처럼 자연어에 가깝게 읽힙니다.
가독성과 본질에 집중
람다는 클래스 이름, 불필요한 중괄호, 의미를 더하지 않는 return 같은 “서비스성 잡음”을 제거합니다. 그 결과 코드를 읽고 유지보수하기 쉬워집니다:
names.forEach(name -> System.out.println(name));
모든 것이 명확합니다: 각 이름을 출력하면 됩니다. 여기서는 forEach 같은 컬렉션 메서드를 알아두면 편리합니다.
동작을 매개변수로 전달
드디어 메서드의 매개변수로 “동작의 조각”을 편리하게 전달할 수 있게 되었습니다. 특히 컬렉션, Stream API, 이벤트에서 그 장점이 두드러집니다:
예: 숫자 목록 필터링
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
numbers.removeIf(n -> n % 2 == 0); // 짝수를 제거합니다
컬렉션 및 Stream API와의 뛰어난 통합
List<String> words = Arrays.asList("Java", "Python", "C++");
List<String> upper = words.stream()
.map(s -> s.toUpperCase())
.collect(Collectors.toList());
변수 캡처(클로저)
람다는 외부 컨텍스트의 변수를 “캡처”할 수 있습니다(해당 변수가 사실상 final인 경우). 이것은 주변 환경을 기억하는 함수를 즉석에서 만들 수 있게 해줍니다:
int minLength = 3;
list.removeIf(s -> s.length() < minLength);
minLength 변수는 바깥에서 선언되었지만, 람다 내부에서 접근할 수 있습니다.
이벤트와 콜백에 자연스러운 표기
button.addActionListener(e -> System.out.println("버튼을 눌렀습니다!"));
한 줄짜리를 위해 별도의 클래스나 익명 클래스를 만들 필요가 없습니다.
테스트 단순화
클래스를 남발하지 않고도 빠르게 “스텁”을 끼워 넣을 수 있습니다:
doSomething(() -> System.out.println("테스트 핸들러"));
2. 람다 표현식의 단점과 제약
어떤 도구와 마찬가지로 단점과 주의점도 있습니다.
디버깅의 어려움
람다는 익명 함수이므로, 오류가 발생했을 때 호출 스택이 명확하지 않을 수 있습니다. 브레이크포인트는 작동하지만, 길거나 중첩된 람다에서는 문제가 정확히 어디인지 파악하기가 어려울 때가 있습니다.
list.stream()
.filter(s -> s.length() > 3)
.map(s -> s.toUpperCase())
.forEach(System.out::println);
중간 변수로 체인을 “풀어” 쓰면 도움이 될 때가 있습니다.
구현 대상 인터페이스의 모호성
서로 다른 함수형 인터페이스를 받는 오버로드가 있을 때, 컴파일러가 람다가 어떤 인터페이스를 구현하는지 추론하지 못할 수 있습니다(예: Runnable의 void 반환 vs Callable의 String 반환).
void doSomething(Runnable r) { /* ... */ }
void doSomething(Callable<String> c) { /* ... */ }
// doSomething(() -> "Hello"); // 모호함!
복잡한 로직에는 부적합
람다 본문이 3–5줄 이상으로 커지고(조건/반복이 많음) 복잡해지면 가독성이 떨어집니다 — 로직을 이름 있는 메서드로 분리하는 것이 좋습니다.
나쁨:
list.removeIf(s -> s.length() > 3 && s.contains("Java") && s.startsWith("A") && ...);
더 좋음:
list.removeIf(this::isComplexCondition);
private boolean isComplexCondition(String s) {
return s.length() > 3 && s.contains("Java") && s.startsWith("A") && ...;
}
직렬화 관련 제약
람다는 항상 직렬화 가능한 것이 아닙니다. JVM 간(분산 시스템)으로 로직을 전달해야 한다면, 익명/이름 있는 클래스를 사용하거나 Serializable을 명시적으로 지원하는 인터페이스를 사용하는 편이 더 안전합니다.
스코프 제약
람다에서는 외부 메서드의 변수를 final 또는 “사실상” final이 아닌 이상 변경할 수 없습니다.
int count = 0;
list.forEach(s -> count++); // 컴파일러가 허용하지 않습니다!
재사용에는 부적합
람다는 “일회성” 함수에 가깝습니다. 동일한 로직을 여러 곳에서 사용해야 한다면 — 의미 있는 이름을 가진 메서드나 클래스로 분리하세요.
중첩 람다의 어려움
깊은 중첩(특히 스트림/이벤트 처리에서)은 코드를 빠르게 “스파게티”로 만듭니다. 중첩을 피하거나 단계를 나누는 것이 좋습니다.
람다 표현식을 사용할 때
- 짧고 단순한 작업: 필터링, 정렬, 컬렉션 변환, 이벤트 처리.
- 람다가 3–5줄을 넘으면 — 별도의 메서드로 추출하세요.
- 복잡한 비즈니스 로직에는 람다를 사용하지 마세요 — 로직에 이름과 주석을 부여하세요.
- 중첩 람다를 남용하지 마세요.
- 반복적으로 쓰는 람다는 메서드(또는 정적 메서드)로 추출하고 this::method 또는 ClassName::method 형태의 참조를 사용하세요.
- 람다 내부의 매개변수에도 의미 있는 이름을 붙여 가독성을 높이세요.
3. 실용적인 팁
복잡한 체인은 단계로 나누기
하나의 긴 체인 대신 — 중간 변수를 사용합니다:
Stream<String> filtered = list.stream().filter(s -> s.length() > 3);
Stream<String> upper = filtered.map(String::toUpperCase);
upper.forEach(System.out::println);
복잡한 조건에는 이름 있는 메서드를 사용
긴 람다 대신:
list.removeIf(s -> s.length() > 3 && s.contains("Java"));
더 좋음:
list.removeIf(this::isJavaString);
private boolean isJavaString(String s) {
return s.length() > 3 && s.contains("Java");
}
주석을 두려워하지 말기
람다가 직관적이지 않다면 — 앞에 주석을 달세요:
// 공백으로 시작하는 모든 문자열을 제거합니다
list.removeIf(s -> s.startsWith(" "));
4. 람다 사용 시 흔한 실수
오류 1: 지나치게 복잡한 람다. 초보자는 모든 비즈니스 로직을 하나의 람다에 욱여넣으려 합니다. 10줄짜리 “괴물” 람다는 읽고 유지보수하기 어렵습니다. 과감히 메서드로 분리하세요!
오류 2: 모호한 스코프 이해. 람다 내부에서 외부 메서드의 변수를 변경하려다 컴파일러 오류가 납니다. 기억하세요: 변수는 final 또는 “사실상” final이어야 합니다.
오류 3: 메서드 오버로드 모호성. 서로 다른 함수형 인터페이스를 받는 두 오버로드가 있을 때, 컴파일러가 어느 것을 호출하려는지 이해하지 못할 수 있습니다. 이럴 때는 타입을 명시하세요:
doSomething((Runnable) () -> System.out.println("Hello"));
오류 4: 중첩 람다 남용. 중첩된 람다는 코드를 읽기 힘든 “스파게티”로 만듭니다. 멈추고, 일부 코드를 별도의 메서드로 분리하세요.
오류 5: 온전한 객체가 필요한 곳에 람다를 사용. 여러 메서드를 재정의해야 하거나 필드를 추가해야 하거나 비표준 동작이 필요하다면 — 람다가 아니라 익명 클래스나 이름 있는 클래스를 사용하세요.
GO TO FULL VERSION