CodeGym /행동 /JAVA 25 SELF /람다식: 문법과 스코프

람다식: 문법과 스코프

JAVA 25 SELF
레벨 48 , 레슨 0
사용 가능

1. 소개

솔직히 말해서: 한두 줄짜리 코드를 위해 익명 클래스를 쓰는 것은 빵집에서 가게까지 빵 하나 옮기려고 거대한 트럭을 부르는 것과 같습니다.

예를 들어 문자열 목록을 길이로 정렬하고 싶다면, Java 8 이전에는 이렇게 작성해야 했습니다:

List<String> list = Arrays.asList("사과", "바나나", "키위");
Collections.sort(list, new Comparator<String>() {
    @Override
    public int compare(String a, String b) {
        return a.length() - b.length();
    }
});

문제는 단순한데 코드가 화면의 반을 차지합니다. 특히 이런 작업이 많아지면 코드가 “시끄러워져”서 핵심이 흐려집니다. 개발자들은 울고불고하다가 람다식을 만들었습니다. 우리는 이미 조금 배웠으니, 이제 복습하고 더 깊이 들어가 보겠습니다.

람다식이란 무엇인가

람다식은 기능적 인터페이스(추상 메서드가 하나뿐인 인터페이스, 예: Comparator, Runnable, Consumer 등)의 구현을 간결하게 표현하는 방법입니다.

쉽게 말해, 람다식은 필요한 자리에서 곧바로 ‘함수’를 작성할 수 있게 해 주며, 별도의 클래스나 메서드를 선언할 필요가 없습니다.

일반 문법:

(parametry) -> { telo }

예시:

  • (a, b) -> a + b — 두 수를 더하는 함수
  • x -> x * x — 제곱 함수
  • () -> System.out.println("Hello!") — 매개변수가 없는 함수

기능적 인터페이스와의 관계:
람다식은 항상 기능적 인터페이스 타입의 변수에 대입할 수 있고, 그러한 인터페이스를 인자로 받는 메서드에 전달할 수도 있습니다.

2. 람다식 문법

매개변수 없음

Runnable r = () -> System.out.println("안녕, 세상!");
r.run(); // 출력: 안녕, 세상!

매개변수 하나
매개변수가 하나면 괄호를 생략할 수 있습니다:

Consumer<String> print = s -> System.out.println(s);
print.accept("Java는 멋지다!");

여러 매개변수
괄호는 필수입니다:

Comparator<String> cmp = (a, b) -> a.length() - b.length();

본문이 한 개의 표현식일 때
본문이 하나의 표현식이면 중괄호와 return이 필요 없습니다:

Function<Integer, Integer> square = x -> x * x;
System.out.println(square.apply(5)); // 25

본문이 블록일 때
여러 문장이 필요하면 중괄호와 return(반환값이 있는 경우)을 사용합니다:

Function<Integer, Integer> abs = x -> {
    if (x < 0) {
        return -x;
    }
    return x;
};
System.out.println(abs.apply(-3)); // 3

매개변수 타입
대부분의 경우 매개변수 타입은 생략해도 컨텍스트로부터 컴파일러가 추론합니다. 원한다면 명시할 수도 있습니다:

Comparator<String> cmp = (String a, String b) -> a.length() - b.length();

반환값이 없는 람다
인터페이스가 void를 반환하면, 명령만 작성하면 됩니다:

list.forEach(s -> System.out.println("요소: " + s));

표: 람다식 문법 변형

무엇을 하는가 설명
매개변수 없음
() -> System.out.println("Hi")
예: Runnable에 사용
매개변수 하나
x -> x * x
괄호 생략 가능
여러 매개변수
(a, b) -> a + b
괄호 필수
단일 표현식
x -> x + 1
return과 중괄호 없음
코드 블록
x -> { int y = x + 1; return y * 2; }
결과가 있으면 return 필요

3. 활용: 람다식을 어디서 어떻게 사용할까

람다식은 보통 ‘동작’—즉 함수—을(를) 인자로 전달해야 할 때 가장 자주 사용됩니다. 컬렉션, 스트림(Stream API), 이벤트 등에서 혁신을 가져왔습니다.

리스트 정렬

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());

스레드(Thread) 생성

Thread t = new Thread(() -> System.out.println("스레드가 시작되었습니다!"));
t.start();

컬렉션 처리

list.forEach(s -> System.out.println(s.toUpperCase()));

리스트 필터링

List<String> longWords = list.stream()
    .filter(s -> s.length() > 5)
    .collect(Collectors.toList());

예제: 학습용 애플리케이션 확장

사용자 목록이 있다고 가정해 봅시다:

List<String> users = Arrays.asList("Alice", "Bob", "Charlie");

이름이 4자보다 긴 모든 사용자를 출력:

users.stream()
    .filter(name -> name.length() > 4)
    .forEach(name -> System.out.println("사용자: " + name));

4. 람다식에서의 변수 스코프

람다식은 외부 메서드의 변수를 사용할 수 있지만, 몇 가지 주의점이 있습니다!

변수와 람다

Java의 람다는 초기화된 뒤 더 이상 변경되지 않는 변수만 ‘캡처’할 수 있습니다. 변수에 final이 붙어 있으면 당연히 가능하고, final이 없어도 컴파일러가 값이 바뀌는지 검사합니다. 바뀌지 않으면 ‘사실상 final(effectively final)’로 간주되어 람다에서 사용할 수 있습니다.

예:

int minLength = 4; // 값이 어디에서도 변경되지 않음
users.forEach(name -> {
    if (name.length() > minLength) {
        System.out.println(name);
    }
});

여기서는 문제가 없습니다. minLength가 그대로이기 때문입니다.

하지만 람다에서 사용한 뒤 minLength에 다시 값을 대입하려 하면 컴파일 오류가 발생합니다:

int minLength = 4;
users.forEach(name -> {
    if (name.length() > minLength) {
        System.out.println(name);
    }
});
minLength = 10; // 오류! 람다가 이미 값을 '고정'했습니다

핵심 규칙은 아주 간단합니다: 람다에 포획된 변수는 불변이어야 합니다.

익명 클래스와의 차이

익명 클래스와 람다식에서 외부 메서드의 변수 사용 규칙은 동일합니다: final/사실상 final만 허용됩니다.

하지만!
람다식에서의 this는 외부 객체(현재 클래스의 인스턴스)를 가리키지만, 익명 클래스에서는 익명 클래스 자신의 인스턴스를 가리킵니다. 람다 내부에서 현재 클래스의 필드나 메서드에 접근할 때 중요한 차이입니다.

예:

public class Example {
    String name = "외부 클래스";

    void demo() {
        Runnable r1 = new Runnable() {
            String name = "익명 클래스";
            @Override
            public void run() {
                System.out.println(this.name); // "익명 클래스"
            }
        };

        Runnable r2 = () -> System.out.println(this.name); // "외부 클래스"

        r1.run();
        r2.run();
    }
}

5. 람다식 사용 시 흔한 실수

오류 №1: final/사실상 final이 아닌 변수를 사용. 람다에서 사용한 뒤 변수를 바꾸려 하면 컴파일러가 즉시 오류를 보고합니다. 이는 안전을 위한 것으로, 그렇지 않으면 어떤 값이 사용되어야 하는지 애매해집니다.

오류 №2: this와의 혼동. 람다식에서 this는 외부 클래스이고, 익명 클래스에서는 익명 클래스 자신입니다. 람다에서 외부 클래스의 메서드를 호출하는 것은 동작하지만, 익명 클래스에서 외부 클래스 컨텍스트를 기대하면 그렇지 않을 수 있습니다.

오류 №3: 컨텍스트가 없는 람다. 람다식은 그 자체로 사용할 수 없습니다. 반드시 기능적 인터페이스 타입의 변수에 대입하거나, 그 인터페이스를 기대하는 곳에 전달해야 합니다. 아무 컨텍스트 없이 x -> x + 1을 적어 넣으면 오류가 발생합니다.

오류 №4: 지나치게 복잡한 람다. 람다식이 3–5줄을 넘어가면 읽기 어렵습니다. 이런 경우에는 로직을 별도의 메서드로 분리하는 것이 좋습니다.

코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION