1 람다표현식이란?
람다 표현식(Lambda Expression)은 익명 함수를 간결하게 표현하는 문법으로, 함수를 값처럼 전달할 수 있게 해줍니다.
자바에서는 함수형 인터페이스(추상 메서드가 1개인 인터페이스)를 구현할 때 주로 사용합니다.
1) 왜 필요한가
- 불필요한 클래스/익명 클래스 제거
- 코드 가독성 향상
- 컬렉션 처리(Stream), 이벤트 처리에 적합
2 기본 형태
(매개변수) -> { 실행문 }
예시 (람다 전)
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println("Hello");
}
};
예시 (람다 후)
Runnable r = () -> System.out.println("Hello");
3) 다양한 문법 예시
// 매개변수 1개 (타입 생략 가능)
x -> x * 2
// 매개변수 2개
(a, b) -> a + b
// 실행문 여러 줄
(x, y) -> {
int sum = x + y;
return sum;
}
4) 함수형 인터페이스
람다는 반드시 함수형 인터페이스와 함께 사용됩니다.
@FunctionalInterface
interface Calculator {
int calc(int a, int b);
}
Calculator add = (a, b) -> a + b;
System.out.println(add.calc(3, 5)); // 8
대표적인 표준 함수형 인터페이스:
- Runnable
- Comparator
- Consumer<T>
- Supplier<T>
- Function<T, R>
- Predicate<T>
5) 언제 사용하면 좋은가
- 한 번만 쓰는 간단한 로직
- 컬렉션 필터링, 정렬, 매핑
- 이벤트 핸들러
- Stream API와 함께 사용 시
6) 주의할 점
- 상태를 많이 가지는 로직엔 부적합
- 디버깅이 어려울 수 있음
- 너무 복잡하면 가독성 저하
2. 람다를 실무에서 사용하지 못한다면.
1) 코드가 불필요하게 길어진다
람다를 못 쓰면 익명 클래스 남발이 됩니다.
// 람다 사용
list.forEach(v -> System.out.println(v));
// 람다 미사용
list.forEach(new Consumer<String>() {
@Override
public void accept(String v) {
System.out.println(v);
}
});
결과
- 코드량 증가
- 핵심 로직 파악 어려움
- 리뷰 시 “왜 이렇게 썼지?”라는 피드백 발생
2) Stream API를 제대로 못 쓴다
실무에서 람다는 Stream과 세트입니다.
람다를 못 쓰면:
- filter / map / reduce 이해 불가
- for문 + if문 + 임시 변수 범람
// 실무에서 선호
List<String> names =
users.stream()
.filter(u -> u.isActive())
.map(User::getName)
.toList();
📌 람다를 못 쓰면
-> 로직은 장황해지고, 버그 포인트는 늘어남
3) 의도를 드러내는 코드 작성이 어렵다
람다는 "어떻게"가 아니라 "무엇을" 하려는지를 표현합니다.
// 의도가 바로 보임
users.removeIf(u -> !u.isActive());
람다 없이 작성하면:
- 조건 분기 + 반복문 분산
- 비즈니스 의도가 코드에 묻힘
-> 실무에선 가독성 = 생산성
4) 최신 라이브러리·프레임워크 활용 제한
Spring, JPA, Java 표준 API 다수가 람다 기반입니다.
예:
- @EventListener
- Optional.ifPresent
- CompletableFuture
- Map.computeIfAbsent
람다 미숙 ->
- API는 아는데 활용을 못함
- “아는 척만 하는 개발자”로 보일 위험
5) 팀 협업에서 신뢰도가 떨어진다
실무 팀 기준은 이렇습니다.
- 람다/Stream 못 쓰는 주니어 -> 학습 중
- 람다 회피하는 경력자 -> 기술 부채
코드 리뷰에서 자주 나오는 말
“이거 람다로 정리하면 되지 않나요?”
6) 단, 무조건 써야 하는 건 아니다
람다 남용도 문제입니다.
X 이런 경우는 오히려 안 좋음
- 람다 안에 복잡한 분기/상태 변경
- 디버깅이 중요한 핵심 비즈니스 로직
// 가독성 나쁨
list.stream()
.filter(...)
.map(...)
.flatMap(...)
.reduce(...)
명확하면 람다, 복잡하면 메서드
즉 람다를 못 쓰면 코드는 돌아가도, 실무에서는 뒤처진다.
람다는 “선택 기술”이 아니라 자바 실무 기본기다.
3. 람다표현식이 제공하는 최적화와 효율성
1) 가장 큰 효율성: 개발 생산성과 유지보수
람다의 1순위 가치는 사람 기준 효율입니다.
orders.stream()
.filter(o -> o.isPaid())
.map(Order::getAmount)
.sum();
- 로직 흐름이 위→아래로 읽힘
- 중간 상태 변수 제거
- 변경 시 영향 범위 최소화
결과
-> 버그 감소 + 리뷰 속도 증가 + 수정 비용 감소
2) 객체 생성 비용 감소 (익명 클래스 대비)
람다는 익명 클래스보다 가볍게 처리됩니다.
익명 클래스
- 컴파일 시 별도 클래스 생성
- this 캡처 방식 복잡
- 메모리 오버헤드 큼
람다
- 대부분 메서드 참조 + invokedynamic
- JVM이 싱글턴 캐싱 가능
- 불필요한 객체 생성 억제
반복 호출 환경에서는 GC 부담 감소
3) JVM 최적화에 유리한 구조
람다는 JVM이 최적화하기 쉬운 형태입니다.
- invokedynamic 기반 바인딩
- JIT 컴파일 시 인라이닝 가능
- 불변(Stateless) 람다는 재사용 가능
x -> x * 2 // 상태 없음 → 재사용 최적화
결과
-> 핫스팟 구간에서 성능 손실 거의 없음
4) Stream과 결합 시의 실행 최적화
람다 + Stream은 실행 계획을 JVM이 조정할 수 있게 합니다.
예시
list.stream()
.filter(x -> x > 10)
.map(x -> x * 2)
.findFirst();
- findFirst() 때문에 단락 평가
- 전체 순회 X
- 필요 최소 연산만 수행
전통적인 for문보다 불필요 연산 감소
5) 병렬 처리에 유리한 구조
람다는 **무상태(stateless)**를 강제하는 방향의 설계입니다.
list.parallelStream()
.filter(x -> x > 0)
.forEach(System.out::println);
- 공유 상태 제거 → 락 감소
- 병렬 처리 안전성 증가
단, 무조건 빠르진 않음
-> 데이터 크기·작업 비용·코어 수 의존
6) 컬렉션 API 내부 최적화 활용
람다 기반 메서드는 내부 구현이 최적화되어 있습니다.
map.computeIfAbsent(key, k -> new ArrayList<>());
- 기존 containsKey + put 제거
- 원자적 처리
- 중복 연산 감소
실무에서 자주 놓치는 고급 최적화 포인트
7) 성능 착각 포인트 (중요)
X 람다 = 무조건 빠름 → 틀림
오히려 느려지는 경우:
- 아주 단순한 루프
- 박싱/언박싱 과다
- 람다 안에서 객체 생성
- 디버깅용 복잡 로직
성능 극한 구간에서는
-> 전통 for문이 더 빠른 경우도 많음
=> 람다의 진짜 최적화는 CPU가 아니라 사람과 JVM을 동시에 최적화하는 것이다.
성능은 “비슷”, 효율은 “확실히 우위”.
4. 면접관의 질문 의도.
1) “이 사람, 성능을 맹신하지는 않나?”
가장 먼저 보는 포인트입니다.
면접관이 경계하는 답변
“람다는 성능이 좋아서요.”
이 답변이 나오면:
- 내부 동작(invokedynamic, JIT)을 모름
- 추상적인 기술 마케팅만 알고 있음
- 성능 이슈에서 위험한 판단 가능
의도
-> “이 사람은 최적화를 맹목적으로 믿는가, 맥락을 따지는가?”
2) “사람 기준 효율과 기계 기준 효율을 구분하는가?”
좋은 개발자는 두 가지를 분리해서 말합니다.
- 사람 기준: 가독성, 유지보수, 변경 비용
- 기계 기준: 객체 생성, JIT, 단락 평가, 병렬성
의도
-> “효율을 코드 차원에서 입체적으로 설명할 수 있는가?”
3) “언제 쓰고, 언제 피해야 하는지 아는가?”
이 질문의 진짜 핵심입니다.
면접관은 이런 답을 듣고 싶어 합니다.
“대부분은 람다로 가독성을 얻지만,
성능 크리티컬하거나 복잡한 로직은 메서드로 분리합니다.”
📌 의도
-> “도구를 맹목적으로 쓰는 사람이 아닌가?”
4) “JVM 레벨까지 생각해본 경험이 있는가?”
고급 신호입니다.
면접관이 반응하는 키워드:
- 익명 클래스 vs 람다 객체 생성
- invokedynamic
- Stateless 람다 캐싱
- JIT 인라이닝
의도
-> “이 사람이 한 단계 깊게 공부한 적이 있는가?”
(모르면 감점은 아니지만, 알면 확실한 가점)
5) “실무에서 실제로 써봤는가?”
암기형 답변과 실무형 답변은 바로 구분됩니다.
암기형
“람다는 코드가 짧아지고 성능이 좋아집니다.”
실무형
“Stream에서 단락 연산이나 computeIfAbsent처럼
내부 최적화를 활용할 수 있다는 점이 큽니다.”
의도
-> “이 사람, 실제 코드에서 고민해본 적 있나?”
5. 꼬리 질문
Q1. 람다는 왜 익명 클래스보다 가볍다고 하나요?
의도: JVM 내부 동작 이해 여부
답변
익명 클래스는 컴파일 시 별도 클래스가 생성되고 인스턴스가 매번 만들어질 수 있습니다.
반면 람다는 invokedynamic 기반으로 바인딩되며, 무상태 람다는 JVM이 재사용하거나 인라이닝할 수 있어
객체 생성과 메모리 오버헤드가 줄어듭니다.
Q2. 람다를 쓰면 항상 성능이 더 좋아지나요?
의도: 성능 맹신 여부 판단
답변
아닙니다. 람다는 가독성과 유지보수 측면의 효율이 핵심입니다.
아주 단순한 반복이나 박싱이 많은 경우엔 전통적인 for문이 더 빠를 수 있고,
성능 크리티컬 구간에서는 항상 측정 후 선택합니다.
Q3. Stream + 람다가 for문보다 느리다는 말도 있는데요?
의도: 상황 판단 능력
답변
경우에 따라 맞습니다. Stream은 추상화 비용이 있기 때문에 단순 루프에서는 느릴 수 있습니다.
다만 findFirst 같은 단락 연산이나 내부 최적화를 활용할 수 있는 경우엔
오히려 불필요한 연산을 줄일 수 있습니다.
Q4. 병렬 스트림은 언제 효과적인가요?
의도: 병렬 처리 이해도
답변
데이터가 충분히 크고, 작업이 CPU 바운드이며,
람다가 무상태이고 공유 자원이 없을 때 효과적입니다.
작은 컬렉션이나 I/O 작업에서는 오히려 오버헤드가 커질 수 있습니다.
Q5. 람다 안에서 상태를 변경하면 어떤 문제가 있나요?
의도: 함수형 사고 여부
답변
상태 변경은 가독성을 해치고, 병렬 처리 시 동시성 문제를 유발할 수 있습니다.
특히 Stream이나 parallelStream에서는 예측 불가능한 버그가 생길 수 있어
람다는 가급적 순수 함수 형태로 사용하는 게 좋습니다.
Q6. 람다 대신 메서드 참조를 쓰는 이유는 뭔가요?
의도: 표현력·가독성 판단
답변
메서드 참조는 이미 존재하는 로직을 재사용한다는 의도가 명확하고,
람다보다 간결해 가독성이 좋습니다.
기능은 같지만 “새 로직”인지 “기존 로직 전달”인지를 명확히 구분할 수 있습니다.
Q7. 실무에서 람다를 피하는 경우도 있나요?
의도: 도구 남용 여부
답변
있습니다. 복잡한 분기, 예외 처리, 상태 변경이 많은 핵심 비즈니스 로직은
람다보다는 이름 있는 메서드로 분리하는 게 디버깅과 유지보수에 유리합니다.
Q8. 람다는 왜 함수형 인터페이스에서만 사용되나요?
의도: 개념 이해도
답변
람다는 하나의 동작을 하나의 메서드로 매핑해야 하기 때문에
추상 메서드가 1개인 함수형 인터페이스에만 바인딩될 수 있습니다.
그렇지 않으면 어떤 메서드에 매핑해야 할지 모호해집니다.
Q9. 람다 캡처 변수 제약이 있는 이유는 뭔가요?
의도: 안정성 이해
답변
캡처되는 지역 변수는 effectively final이어야 합니다.
이는 동시성 안정성과 예측 가능한 실행을 보장하기 위한 제약으로,
값 변경으로 인한 사이드 이펙트를 방지하기 위함입니다.
Q10. 람다를 한 문장으로 정의해보세요
의도: 개념 정리 능력
답변
람다는 동작을 객체처럼 전달하기 위한 표현 방식이며,
가독성과 설계를 개선하는 것이 주된 목적입니다.
'Daily Dev Q&A' 카테고리의 다른 글
| 스프링 프레임 워크 (0) | 2025.12.22 |
|---|---|
| 트랜잭션 (0) | 2025.12.21 |
| 자바의 접근 제어자(Access Modifier) (0) | 2025.12.09 |
| Collections Framework (0) | 2025.12.09 |
| SOLID원칙 (0) | 2025.12.07 |