[Java] 람다 표현식
코드를 간결하게 작성하고 중복을 최대한 피하기 위해 사용한다.
어떤 메서드를 구현한다고 해 보자.
확장성을 열어놓기 위해 특정 비즈니스 요구사항이 추가됐을 때 쉽게 수정하고 추가할 수 있어야 한다.
이를 위해 값을 메서드의 파라미터로 넘기는 것 보다 동작을 메서드의 파라미터로 넘기는 편이 합리적이다.
package Parameter;
public class Apple {
private String color;
priavet String weight;
public Apple(String color, String weight) {
this.color = color;
this.weight = weight;
}
public String getColor() {
return color;
}
public String getWeight() {
return weight;
}
}
package Parameter;
public interface AppleFormatter {
String accept(Apple apple); // 함수형 인터페이스의 추상 메서드 시그니처를 함수 디스크립터라고 부른다
} // 시그니처 : 메서드명, 매개변수 타입, 순서
package Parameter;
public class AppleFancyFormatter implements AppleFormatter {
@Override
public String accept(Apple apple) {
if (Integer.valueOf(apple.getWeight()) > 150) {
return "heavy apple";
} else {
return "normal apple";
}
}
}
사과 객체가 있고, 사과 객체를 출력하는 함수를 구현한다고 해 보자.
지금은 객체에 색상과 무게만 있지만 추후 필드가 더 추가될 수 있으니 이 부분도 고려해야 한다.
이 때 전략 패턴을 사용해 인터페이스를 두고 인터페이스를 구현하는 객체들을 동작 파라미터로 설정할 수 있다.
위의 예시에서는 AppleFancyFormatter가 인터페이스의 전략이고, 해당 구현체를 사용해 비즈니스 로직을 구현한다.
필드가 더 추가되면 비즈니스 로직에 맞는 구현체를 새로 만들어서 구현할 수 있다.
Apple apple = new Apple("Green", "166");
printApple(apple, new AppleFancyFormatter());
public static void printApple(Apple apple, AppleFormatter formatter) {
String output = formatter.accept(apple);
System.out.println(output);
}
객체지향의 다형성 덕분에 인터페이스를 메서드의 매개변수로 선언하고, 구현체를 넘길 수 있다.
문제를 깔끔하게 해결할 수는 있지만, 매번 전략 구현체를 만들어야 하는 점이 불편하다.
printApple(apple, new AppleFormatter() {
@Override
public String accept(Apple apple) {
if (Integer.valueOf(apple.getWeight()) > 150) {
return "heavy apple";
} else {
return "normal apple";
}
}
});
printApple(apple, apple2 -> {
if(Integer.valueOf(apple2.getWeight()) > 150) {
return "heavy apple";
} else {
return "normal apple";
}
});
익명 클래스와 람다 표현식을 사용하면 좀 더 간결하게 작성할 수 있다.
람다 표현식은 메서드로 전달할 수 있는 익명 함수를 단순화 한 표현식으로 생각하면 된다.
Comparator<Apple> sortByWeight = new Comparator<Apple>() {
@Override
public int compare(Apple o1, Apple o2) {
return o1.getWeight().compareTo(o2.getWeight());
}
};
Comparator<Apple> sortByWeight2 = (Apple o1, Apple o2) -> o1.getWeight().compareTo(o2.getWeight());
정렬 조건을 명시하는 Comparator를 람다 표현식으로 작성한 예시이다.
파라미터 리스트 : Comparator의 메서드 파라미터를 가진다.
-> : 파라미터 리스트와 바디 부분을 구분한다.
바디 : 람다의 반환값에 해당하는 표현식이다. (final 키워드가 붙은 외부 변수를 바디에서 사용할 수 있다)
람다 표현식은 함수형 인터페이스라는 맥락에서 자주 사용된다.
여기서 말하는 함수형 인터페이스는 하나의 추상 메서드를 가지는 인터페이스이다. (두 개가 넘어가면 사용할 수 없다)
디폴트 메서드를 고려해서 추상 메서드가 하나만 있다면 함수형 인터페이스라고 할 수 있다. (@FunctionalInterface 애너테이션으로 명시할 수 있다)
즉, 위의 예시에서 o1.getWeight().compareTo(o2.getWeight()); 부분은 함수형 인터페이스를 구현한 클래스의 인스턴스로 해석된다.
람다를 제대로 활용하기 위해서 어떤 부분을 파라미터화 할 지 생각해보자.
자바와 스프링이 항상 그렇듯.. 람다 표현식도 중복되는 부분을 줄이고 간결하게 코드를 작성하는걸 목적으로 한다.
람다를 언제 사용해야 하는지 의문이 든다면 함수형 프로그래밍의 이점과 도입 목적을 생각해보자.
자바8은 Predicate, Consumer, Function, Operator 등 여러 함수형 인터페이스를 제공하고 있다. (Predicate는 래퍼 클래스와 Primitive 클래스를 변환할 때 사용된다)
따라서 (int a, int b) -> a * b 이런식으로 작성해도 내부적으로는 자바8이 제공하는 함수형 인터페이스를 사용한다.
IntBinaryOperator intBinaryOperator = (int a, int b) -> a * b;
System.out.println(intBinaryOperator.applyAsInt(3,4));
그런데 람다에는 어떤 함수형 인터페이스를 구현하는지 명시되어있지 않은데 어떻게 구현하는걸까?
람다는 사용되는 문맥을 파악해서 람다의 형식을 추론한다.
func(a, (Object obj) -> obj.getSomething() > 1);
이런 식으로 람다 표현식이 있다고 하자.
1. func 메서드의 선언을 확인하고, 두 번째 파라미터로 Predicate<Object> 형식을 기대함을 확인한다.
2. Precidate<Object> 함수형 인터페이스를 분석해 함수 디스크립터를 묘사한다. (람다의 시그니처도 함께 추론할 수 있어 파라미터 타입을 생략해도 된다)
이 두 가지 과정을 거쳐 함수형 인터페이스를 추론한다.
메서드 참조를 사용하면 람다식을 좀 더 간단하고 가독성 좋게 작성할 수 있다.
(Apple ap) -> (ap.getWeight())
Apple::getWeight
List<String> list = new ArrayList<>();
list.add("a"); list.add("b"); list.add("c");
list.sort((s1, s2) -> s1.compareTo(s2));
list.sort(String::compareTo);
Function<String, Integer> stringToInteger = Integer::parseInt; // Integer 클래스의 parseInt 메서드를 참조한다.
Function<String, Integer> stringLength = String::length;
Supplier<ArrayList<String>> arrayListConstructor = ArrayList::new;
메서드명 앞에 :: 를 붙여 메서드 참조를 활용한다. 위의 람다식과 메서드 참조 식은 같은 역할을 수행한다.
특정 메서드만을 호출하는 람다 표현식을 더욱 간결하게 표현하는 방법으로, 메서드 참조는 기본적으로 람다 표현식의 축약형이다.
컴파일러는 람다 표현식의 형식을 검사하는 방식과 같은 과정으로 메서드 참조가 주어진 함수형 인터페이스와 호환되는지 확인한다.
요점은 컴파일러가 형식을 검사하니 사용법을 이해하고 적당한 수준으로 축약해서 작성하는 부분이다. (이게 되네? 싶은데 된다)
인수가 0,1,2 개인 시그니처를 가지는 함수형 인터페이스는 웬만하면 자바가 제공해 주지만, 그렇지 않은 경우 직접 함수형 인터페이스를 구현해야 한다.
전략 패턴 구현 -> 익명 클래스 -> 람다 표현식 -> 메서드 참조
코드 자체로 의미를 전달할 수 있도록 하기 위해 위와 같은 과정을 거쳤다.
여기서 좀 더 응용하면 람다식과 람다식을 조합해서 하나의 복잡한 람다 표현식을 만들 수도 있다.
안드로이드 개발에서는 자바의 람다 표현식을 자주 사용하게 되지만.. 스프링을 사용한 웹 개발에서는 그렇게 자주 사용되지는 않는다.
그래도 알아 두면 코드를 직관적으로 작성할 수 있고 엔티티를 DTO로 바꾸는 등 여러 곳에서 활용할 수 있으니 알아두자.
함수를 값처럼 다룰 수 있게 되면서 함수를 함수 인자로 넘기거나 함수가 함수를 반환하는 로직을 작성할 수 있게 돼 복잡한 로직을 더 간결하게 표현할 수 있다.
'Programming Language > Java' 카테고리의 다른 글
[Java] 스트림과 병렬 실행 (0) | 2023.08.02 |
---|---|
[Java] 컬렉션과 스트림 (0) | 2023.08.01 |
자바 예외 이해하기 (0) | 2022.09.03 |
[Java] 네트워킹 (Networking) 2 (0) | 2021.12.13 |
[Java] 네트워킹 (Networking) 1 (0) | 2021.12.05 |
댓글
이 글 공유하기
다른 글
-
[Java] 스트림과 병렬 실행
[Java] 스트림과 병렬 실행
2023.08.02 -
[Java] 컬렉션과 스트림
[Java] 컬렉션과 스트림
2023.08.01 -
자바 예외 이해하기
자바 예외 이해하기
2022.09.03 -
[Java] 네트워킹 (Networking) 2
[Java] 네트워킹 (Networking) 2
2021.12.13