CodeGym /행동 /JAVA 25 SELF /람다 표현식의 장점과 단점

람다 표현식의 장점과 단점

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

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

중간 변수로 체인을 “풀어” 쓰면 도움이 될 때가 있습니다.

구현 대상 인터페이스의 모호성

서로 다른 함수형 인터페이스를 받는 오버로드가 있을 때, 컴파일러가 람다가 어떤 인터페이스를 구현하는지 추론하지 못할 수 있습니다(예: Runnablevoid 반환 vs CallableString 반환).

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: 온전한 객체가 필요한 곳에 람다를 사용. 여러 메서드를 재정의해야 하거나 필드를 추가해야 하거나 비표준 동작이 필요하다면 — 람다가 아니라 익명 클래스나 이름 있는 클래스를 사용하세요.

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