[Java] Java8 Lambda와 함수형 인터페이스

코딩하는 개구리·2023년 11월 12일
2

[Java]

목록 보기
2/2

며칠 전 학교수업에서 람다를 배우게 되었는데, 평소 프로젝트를 할 때도 자주 사용했었지만 정작 무엇인
지 정확히 알고 쓰지 못하는 것 같아 오늘은 람다가 무엇인지, Lambda가 도입하게 된 함수형 인터페이스란 무엇인지 알아보겠다.

Lambda(람다)란?

익명 함수의 한 형태로, 메소드를 하나의 식으로 표현하는 방법

람다 표현식은 객체 지향 프로그래밍 언어인 Java에 함수형 프로그래밍 패러다임의 특징을 가져오게 한 중요한 부분이다.
코드를 더욱 간결하게 만들어주고, 메소드를 데이터처럼 전달할 수 있게 해준다.

Java에서 람다 표현식의 문법과 기본 구조

원래 함수를 작성하기 위해서는

반환타입 메서드이름 (매개변수) {
	문장들;
}

이러한 형태로 작성해야했다. 하지만 람다는 이를 더 간단하게 만들었다.

기본적인 Lambda 표현식의 문법은 다음과 같다.

(매개변수) -> { 본문; }

람다식은 '익명 함수'답게 함수에서 반환타입을 제거하고, 매개변수 선언부와 몸통 {} 사이에 -> 를 추가한다.

각 부분이 무엇을 의미하는지 알아보자.

  • 매개변수 : 왼쪽 괄호안에 정의된다. 매개변수의 타입을 명시적으로 선언할 수도 있고, 컴파일러가 타입을 추론하도록 할 수도 있다,
  • 화살표 : -> 기호는 람다 표현식의 시작을 나타낸다.
  • 본문 : 중괄호 안에 표현식이나 문장들이 위치한다. 본문이 단일 표현식만 포함하는 경우, 중괄호와 return 문을 생략할 수 있다.

람다식을 활용한 간단한 예시

기본 구조를 알아봤으니 간단한 람다식을 사용한 예제를 살펴보자.
일단은 람다식을 사용하지 않은 방법이다.

List<String> items = Arrays.asList("Apple", "Banana", "Cherry", "Orange");

for (String item : itmes) {
	System.out.println(item);
}

위 코드는 for-each문을 통해 리스트를 순회하여 출력하는 간단한 예제이다. 이제 이 코드에 람다식을 적용해보자.

List<String> items = Arrays.asList("Apple", "Banana", "Cherry", "Orange");

items.forEach(item -> System.out.println(item));

이 두 코드를 비교해보면 확실히 람다식을 사용한 경우 코드 길이가 더 짧아지고, 의도가 더 명확하게 보인다는 것을 알 수 있다.

특히 Stream과 Lambda가 결합될 때, 코드의 가독성을 크게 증가시킬 수 있다.
Stream과 결합한 간단한 예제도 살펴보자.

Lambda와 Stream을 결합한 간단한 예시

List<String> names = Arrays.List("Frog", "Lion", "Tiger", "Rabbit", "Riger");
List<String> filteredNames = new ArrayList<>();

for (String name : name) {
	if (name.startsWith("R")) {
    	String uppercaseName = name.toUppercase();
        filteredNames.add(uppercaseName);
    }
}

System.out.println(filteredNames);

이 예제는 'R'로 시작하는 동물이름만 필터링하고, 그 결과를 리스트에 저장하고 출력하는 코드이다. 이 코드 역시 for-each문을 사용하고 있다.
이제 Lambda와 Stream을 이 코드에 적용시켜보자.

List<String> names = Arrays.List("Frog", "Lion", "Tiger", "Rabbit", "Riger");

List<String> filteredNames = names.stream()
								  .filter(name -> name.startsWith("S"))
                                  .map(String::toUpperCase)
                                  .collect(Collectors.toList());
                                 
System.out.println(filteredNames);

두 예제를 살펴보면 사실 코드의 길이는 차이가 없다.
하지만 Lambda와 Stream을 사용한 경우, 각 단계의 목적이 명확하게 드러나기 때문에 코드를 더 읽기 쉽고, 유지보수가 용이해진다.


함수형 인터페이스란?

함수형 인터페이스란 과연 무엇일까?

new Object() {
	int mIn(int a, int b) {
    	return a < b ? a : b;
    }
}
(int a, int b) -> a < b ? a : b

우리는 위에서 봤다시피 이 두 코드는 같은 역할을 한다.
그렇다면 아래에 있는 람다식으로 정의된 익명 객체의 메서드를 어떻게 호출할 수 있을까?

(타입) f = (int a, int b) -> a < b ? a : b;

이렇게 참조변수를 선언하여 호출할 수 있을 것이다.
그러면, 참조변수 f는 어떤 것이여야 할까?
바로 람다식과 동등한 메서드가 정의되어 있는 클래스나 인터페이스여야 한다.

예를 들어 위의 min() 메서드가 정의된 인터페이스가 있다고 해보자.

interface FrogInterface {
	public abstact int max(int a, int b);
}

그러면 이 인터페이스를 구현한 익명 클래스의 객체를 생성할 수 있다.

FrogInterface f = new FrogInterface() {
						public int min(int a, int b) {
                        	return a < b ? a : b;
                       	}
                  }

int minNumber = f.min(5, 3);

우리는 이제 min()을 람다식으로 대체할 수 있다는 것을 알고 있다. 적용해본다면

FrogInterface f = (int a, int b) -> a < b ? a : b;

int minNumber = f.min(5, 3);

이처럼 인터페이스를 구현한 익명 객체를 람다식으로 대체가 가능한 이유는, 람다식도 실제로는 익명 객체리고, 인터페이스를 구현한 익명 객체의 메서드 min()과 람다식의 매개변수의 타입과 개수 그리고 반환값이 모두 일치했기 때문이다.

하나의 메서드가 선언된 인터페이스를 정의해서 람다식을 다루는 것은 매우 자연스럽다.

그래서 인터페이스를 통해 람다식을 다루기로 결정이 되었으며, 람다식을 다루기 위한 인터페이스를 '함수형 인터페이스'라고 명명하였다.


람다 표현식의 이점과 한계

지금까지 람다 표현식의 이점을 살펴보았지만, 반대로 한계점도 존재한다. 두 가지를 한 번에 살펴보자.

람다 표현식의 이점

  1. 간결성
  • 람다 표현식은 간단한 기능을 가진 함수를 한 줄의 코드로 작성할 수 있게 해준다.
  1. 유연성과 편의정
  • 람다는 일반적으로 작은 코드 블록을 인자로 다른 함수에 전달할 때 용이하다.
  • 고차원 함수의 사용을 용이하게 하며, 맵, 필터와 같은 함수에 대한 강력한 표현력을 제공한다.
  1. 익명성
  • 이름이 필요 없기 때문에, 람다 표현식은 코드 상에서 한 번만 사용되는 함수를 만들 때 유용하다.
  • 코드의 재사용성을 증가시키지 않으면서도 특정 기능을 수행할 수 있게 해준다.
  1. 클로저 지원
  • 람다 표현식은 외부 변수를 캡처하여 사용할 수 있다.
  • 함수의 동작을 해당 함수의 범위 밖에서도 정의할 수 있게 해주어, 더 동적인 프로그래밍이 가능하게 한다.

람다 표현식의 한계점

  1. 제한된 표현력
  • 람다 표현식은 보통 한 줄의 코드로 제한된다.
  • 복잡한 로직이나 여러 단계의 연산을 수행해야 하는 경우, 람다 표현식은 적합하지 않을 수 있다.
  1. 가독성 문제
  • 람다 표현식이 너무 많이 사용되거나 복잡할 경우, 코드의 가독성이 떨어질 수도 있다.
  • 특히 람다 표현식 내에서 복잡한 조건문이나 루프를 사용하는 경우 이해하기 어려워질 수 있다.
  1. 디버깅 문제
  • 람다 표현식은 디버깅하기가 더 어렵다.
  • 스택 추적에서 람다 표현식이 익명으로 표현되기 때문에, 오류의 원인을 찾기가 더 복잡할 수 있다.

람다 표현식은 확실한 이점이 있지만, 사용할 때 상황에 맞게 사용을 하여야 그 이점을 극대화할 수 있다.
즉, 무분별한 람다 표현식 사용은 코드의 가독성을 더욱 떨어뜨릴 수 있다.

0개의 댓글