
왼쪽과 오른쪽은 완전히 같은 일을 합니다. 람다는 군더더기를 지운 표기법입니다.
JAVA3에서 예외부터 트랜잭션까지, 자바로 프로그램을 굴리는 데 필요한 도구를 훑었습니다. 이번 JAVA4 시즌은 결이 조금 다릅니다. 새 기능을 배우는 것이 아니라, 이미 할 줄 아는 일을 훨씬 짧게 쓰는 법을 배웁니다. 자바 8에서 들어온 람다와 스트림이 그 주인공이고, 오늘은 그 출발점인 람다식입니다.
다섯 줄이 한 줄이 되는 순간
먼저 결론부터 보겠습니다. 두 숫자를 더하는 기능을 코드에 넘겨야 한다고 해봅시다. 예전 방식은 이렇습니다.
Calculator add = new Calculator() {
@Override
public int calculate(int a, int b) {
return a + b;
}
};
람다식으로 쓰면 이렇게 됩니다.
Calculator add = (a, b) -> a + b;
다섯 줄이 한 줄이 되었습니다. 중요한 건 짧아졌다는 사실 자체가 아닙니다. 실제로 하려던 일은 a + b 하나뿐이었는데, 나머지 네 줄은 그 한 줄을 전달하기 위한 포장지였다는 점입니다. 람다는 그 포장지를 걷어냅니다.
그 포장지는 왜 필요했나
JAVA2에서 인터페이스를 배웠습니다. 인터페이스는 "이런 메서드를 만들어라"는 약속이고, 그 약속을 지키는 클래스를 따로 만들어 썼죠.
interface Calculator {
int calculate(int a, int b);
}
그런데 더하기 한 번 하자고 클래스 파일을 새로 만드는 건 과합니다. 그래서 자바는 이름 없이 그 자리에서 만들어 쓰고 버리는 익명 클래스를 제공했습니다. 위의 new Calculator() { ... }가 바로 그것입니다.
문제는 익명 클래스도 여전히 장황하다는 데 있었습니다. 인터페이스 이름을 다시 적고, 메서드 이름과 반환형과 매개변수 타입을 전부 다시 적어야 합니다. 그런데 잘 보면 그 정보는 전부 인터페이스에 이미 적혀 있습니다. 컴파일러가 알고 있는 것을 사람이 한 번 더 받아 적는 셈이죠.
쉽게 — 택배 송장과 같습니다. 받는 사람 주소가 이미 시스템에 등록돼 있는데, 송장에 그 주소를 손으로 다시 옮겨 적으라고 하는 상황입니다. 람다는 "이미 아는 건 적지 말고 내용물만 넣으세요"로 바꾼 것입니다.
화살표가 하는 일
람다식의 문법은 화살표 하나가 전부입니다. 왼쪽에 받을 값, 오른쪽에 할 일을 씁니다.
(int a, int b) -> { return a + b; }
왼쪽 괄호는 매개변수, 오른쪽 중괄호는 메서드 본문입니다. 익명 클래스에서 public int calculate라는 이름표만 떼어낸 모양이라고 보면 정확합니다. 이름을 뗄 수 있는 이유는 뒤에서 설명하겠습니다.
여기서 더 줄일 수 있습니다. 그리고 이 생략 규칙이 초보자를 가장 많이 헷갈리게 만드는 지점입니다.
어디까지 줄일 수 있나
생략은 세 번 일어납니다. 각각 조건이 다르니 하나씩 보겠습니다.

그림 1. 왼쪽에서 오른쪽으로 갈수록 짧아지지만, 2단계와 3단계는 조건이 붙습니다.
첫째, 매개변수 타입을 생략합니다. 인터페이스에 int calculate(int a, int b)라고 적혀 있으니 컴파일러는 두 값이 int라는 걸 이미 압니다.
(a, b) -> { return a + b; }
둘째, 매개변수가 하나뿐이면 괄호도 생략합니다.
name -> { System.out.println(name); }
단, 이건 개수가 정확히 하나일 때만 됩니다. 둘 이상이면 괄호를 반드시 씁니다. 하나도 없을 때는 빈 괄호 ()를 그대로 둡니다.
셋째, 할 일이 식 하나뿐이면 중괄호와 return을 함께 생략합니다.
(a, b) -> a + b
이 세 번째가 앞의 두 개와 성격이 다릅니다. 앞의 둘은 "적어도 되고 안 적어도 되는" 선택이지만, 세 번째는 중괄호와 return이 한 몸이라는 규칙입니다. 이 점이 바로 오늘의 함정으로 이어집니다.
단골 함정 — 중괄호를 열었으면 return을 써야 합니다
가장 자주 나오는 컴파일 오류입니다. 중괄호를 쓰면서 return을 빠뜨리는 경우죠.
Calculator add = (a, b) -> { a + b; }; // 컴파일 오류
Calculator add = (a, b) -> { return a + b; }; // 정상
Calculator add = (a, b) -> a + b; // 정상
첫 줄이 왜 오류일까요. 중괄호를 여는 순간 그 안은 일반적인 메서드 본문이 됩니다. 메서드 본문에서 값을 돌려주려면 return을 써야 하죠. a + b;는 계산만 하고 버리는 문장이라 돌려주는 값이 없고, calculate는 int를 돌려주기로 약속했으니 약속 위반이 됩니다.
반대로 중괄호가 없으면 자바는 그 식의 결과를 자동으로 돌려주는 것으로 해석합니다. 그래서 return을 쓰면 안 됩니다.
한 가지 더 있습니다. 타입을 적으면서 괄호를 생략하는 것은 안 됩니다.
String s -> s.length() // 컴파일 오류
(String s) -> s.length() // 정상
s -> s.length() // 정상
정리하면 이렇습니다. 타입을 적을 거면 괄호도 적고, 중괄호를 열 거면 return도 쓴다. 둘 다 "반만 생략"이 안 되는 짝입니다.
직접 실행해 보기
사칙연산을 람다로 만들어 봅니다. 네 가지가 각각 다른 축약 단계로 쓰여 있으니 비교하며 읽어 보시기 바랍니다. 실행한 뒤 mul 줄에서 return을 지우고 다시 돌려 보면 위의 함정을 직접 확인할 수 있습니다.
interface Calculator {
int calculate(int a, int b); // 추상 메서드 하나
}
public class Main {
public static void main(String[] args) {
Calculator add = (a, b) -> a + b; // 가장 짧은 형태
Calculator sub = (int a, int b) -> a - b; // 타입을 다 적은 형태
Calculator mul = (a, b) -> { return a * b; }; // 중괄호 + return
Calculator div = (a, b) -> { // 여러 줄도 가능
if (b == 0) {
System.out.println("0으로는 나눌 수 없습니다");
return 0;
}
return a / b;
};
System.out.println("더하기: " + add.calculate(10, 20));
System.out.println("빼기 : " + sub.calculate(30, 10));
System.out.println("곱하기: " + mul.calculate(10, 20));
System.out.println("나누기: " + div.calculate(100, 5));
System.out.println("나누기: " + div.calculate(100, 0));
// Calculator bad = (a, b) -> { a + b; }; // 주석을 풀면 컴파일 오류
}
}📱 코드 실행(실습)은 태블릿·PC에서 이용할 수 있어요. 모바일에서는 위 코드를 눈으로 따라가며 확인하세요.
아무 인터페이스에나 되는 것은 아닙니다
여기서 당연한 의문이 생깁니다. 람다식에는 메서드 이름이 없는데, 자바는 어느 메서드를 구현한 것인지 어떻게 알까요.
답은 단순합니다. 고를 필요가 없기 때문입니다. 람다를 쓸 수 있는 인터페이스는 추상 메서드가 정확히 하나뿐이어야 합니다. 하나뿐이니 이름을 적지 않아도 헷갈릴 일이 없죠.
interface Calculator {
int calculate(int a, int b); // 추상 메서드 1개 → 람다 가능
}
interface Broken {
int calculate(int a, int b);
int reset(); // 2개 → 람다 대입 불가
}
이렇게 추상 메서드가 하나뿐인 인터페이스를 함수형 인터페이스라고 부릅니다. 이름은 거창하지만 조건은 이 하나가 전부입니다. 다음 편에서 이 인터페이스를 직접 만들고, @FunctionalInterface로 실수를 막는 법을 다룹니다.
컬렉션에서 진가가 나옵니다
지금까지는 인터페이스를 직접 만들어 썼지만, 사실 자바가 이미 만들어 둔 곳이 훨씬 많습니다. JAVA3 4편에서 배운 컬렉션이 대표적입니다.
List의 forEach와 sort는 할 일을 람다로 받습니다. 특히 sort를 보면 람다의 쓸모가 분명해집니다. 정렬 기준은 상황마다 다른데, 기준마다 클래스를 만들 수는 없으니 기준 자체를 값처럼 넘기는 것이죠.
import java.util.ArrayList;
import java.util.List;
class Person {
private String name;
private int age;
Person(String name, int age) {
this.name = name;
this.age = age;
}
public int getAge() { return age; }
public String getName() { return name; }
@Override
public String toString() { return name + " (" + age + "세)"; }
}
public class Main {
public static void main(String[] args) {
List<Person> list = new ArrayList<>();
list.add(new Person("홍길동", 55));
list.add(new Person("남길동", 45));
list.add(new Person("서길동", 35));
System.out.println("[입력한 순서]");
list.forEach(p -> System.out.println(p));
list.sort((a, b) -> b.getAge() - a.getAge()); // 나이 내림차순
System.out.println("[나이 많은 순]");
list.forEach(p -> System.out.println(p));
list.sort((a, b) -> a.getName().compareTo(b.getName())); // 이름 오름차순
System.out.println("[이름 가나다순]");
list.forEach(p -> System.out.println(p));
}
}📱 코드 실행(실습)은 태블릿·PC에서 이용할 수 있어요. 모바일에서는 위 코드를 눈으로 따라가며 확인하세요.
sort에 넘긴 람다를 눈여겨보시기 바랍니다. b.getAge() - a.getAge()는 뒤 사람이 더 나이가 많으면 양수가 되고, 자바는 그 부호를 보고 순서를 정합니다. 기준을 바꾸고 싶으면 람다만 갈아 끼우면 됩니다. 클래스를 새로 만들 필요도, 기존 코드를 고칠 필요도 없습니다.
여기까지가 람다의 본질입니다. 지금까지 자바에서 넘길 수 있는 것은 값과 객체뿐이었는데, 람다부터는 "할 일"을 넘길 수 있게 되었습니다.
한 장 정리
| 항목 | 내용 |
|---|---|
| 람다식 | 추상 메서드 하나짜리 인터페이스의 구현을 화살표로 줄여 쓴 것 |
| 기본 문법 | (매개변수) -> { 할 일 } |
| 타입 생략 | 항상 가능 (컴파일러가 추론) |
| 괄호 생략 | 매개변수가 정확히 1개일 때만 |
| 중괄호 생략 | 할 일이 식 하나일 때, return도 함께 생략 |
| 쓸 수 있는 곳 | 추상 메서드가 1개인 인터페이스(함수형 인터페이스) |
| 대표 활용 | list.forEach(...), list.sort(...) |
이것만 기억하면 됩니다. 람다는 새로운 기능이 아니라 짧게 쓰는 표기법입니다. 익명 클래스에서 컴파일러가 이미 아는 정보를 지운 결과이고, 지울 수 있는 이유는 추상 메서드가 하나뿐이기 때문입니다. 그리고 생략에는 짝이 있습니다. 타입을 적으면 괄호도 적고, 중괄호를 열면 return도 씁니다.
미니 퀴즈
문법 자체보다 생략 조건에서 많이 틀립니다. 정답은 눌러야 나옵니다.
미니 퀴즈
포켓코딩(JAVA4) 1편 · 예제 출처: 강사 개인 GitHub 저장소. 다음 편: 함수형 인터페이스 — 람다가 들어갈 자리 직접 만들기.