Functional; JDK 8,9,10
깨끗한 코드를 넘어서 아름다운 코드를 작성하기 위한 여정;
명령형과 선언형;
Ticket 서비스 개발 업무를 HM에게 지시 할 때
명령형; How 어떻게?
첫 번째, Front에서 티켓 예매를 진행하기 위해서 Reservation 정보를 Request Message를 보내야 하니, 필요한 메시지 구조를 문서화 해서 전달 해주세요.
두 번째, 전달 받은 메시지를 이용해서 유효성 검사와 예약 가능 여부를 확인하고 예약을 진행해주세요.
선언형; What 무엇을?
티켓 예매 시스템 1주일 이내로 개발해주세요!
위 예제를 보면 명령형의 경우, 목적을 달성하기 위해서 어떻게(How) 동작 해야하는지에 대해서 구체적으로 이야기한다. 그런데 선언형의 경우에는 그렇지 않다. 원하는 목적만 이야기한다. 그 목적을 달성하기 위해서 누구와 협의를 하고 어떻게 구조를 설계를 해야하는지는 모른다. 개발실장은 원하는 목적(What)만 이야기하면 된다.
개발 환경에서도 마찬가지다. 예를 들어, int형 배열에 있는 요소에 곱하기 2를 하고 싶다면 명령형의 경우, 반복문이 실행될 때 마다 하나의 요소를 꺼내와서 그 요소에 2를 곱하는 부분을 상세하게 명령 한다.
1
2
3
4
5
6
int[] ary = new int[]{1,2,3,4,5};
for(int i = 0; i < ary.length; i++){
ary[i] = ary[i] * 2;
ary[i] *= 2;
}
System.out.println(Arrays.toString(ary));
하지만, 선언형의 경우는 단순히 “요소에 2를 곱해줘”라고 이야기 하지 상세하게 이야기 하지 않는다. 그 이유는 상세한 동작(여기서는 for구문을 말한다.)은 추상화 되어 있기 때문이다.
1
2
3
4
int[] ary = new int[]{1, 2, 3, 4, 5};
int[] new_ary = Arrays.stream(ary).map(element -> element * 2).toArray();
System.out.println("Array => " + Arrays.toString(ary));
System.out.println("New Array => " + Arrays.toString(new_ary));
람다 표현식
함수형 프로그래밍
순수 함수를 기반으로 데이터 처리와 상태 변화를 최소화하는 방식의 프로그래밍 기법을 말한다. 여기서 순수 함수란 동일한 입력에 대해 항상 같은 결과(멱등성)를 반환하며, 외부 상태를 변경하지 않는 함수를 말한다. 여기서 “외부 상태를 변경하지 않는다.”의 의미는 “String[] 타입을 인자(Input 값)로 받는 upperCase 함수가 동작할 때 인자로 전달 받은 외부 상태(=원본(Source), Input 값)를 변경하지 않고 동작하는 것을 말하며, 그렇기 때문에 return 값(Output 값)은 항상 동일하며 부수적인 영향을 주지 않는다.” 라는 의미를 가진다.
1
2
3
4
5
public void immutablePureFunctionTests(){
List<String> ary = Arrays.asList(new String[]{"a", "b", "c"});
System.out.println(Arrays.toString(ary.toArray()));
System.out.println(Arrays.toString(ary.stream().map(String::toUpperCase).toList().toArray()));
}
순수 함수를 이용하면 코드의 복잡성에 따른 부작용(Side-Effects)를 최소화하여, 프로그램의 유지 보수와 테스트를 용이하게 할 수 있다.
1급 시민 객체
사용할 때 다른 요소들과 아무런 차별이 없다는 것을 뜻한다. (여기서 다른 요소란?; Primitive Type, Reference Type을 말한다.)
1급 시민 객체가 되기 위한 조건; 값으로 취급 할 수 있어야 한다.
- 모든 일급 객체는 변수가 데이터 구조(ex. List, Map)에 담을 수 있어야 한다.
- 모든 일급 객체는 메서드의 파라미터로 전달 할 수 있어야 한다.
- 모든 일급 객체는 메서드의 리턴 값으로 사용 할 수 있어야 한다.
그런데 Java8 이전 세상에서 메서드(Method)는 1급 시민 객체가 아니였다! Java8부터 람다 or 람다 표현식 문법을 제공함으로써 Java도 함수(=Method)라는 개념(?)을 말할 수 있고 함수를 메서드의 파라미터나 리턴 값으로 사용할 수 있게 되었다.
람다식?
정의
함수형 프로그래밍에서 중요한 개념으로, 익명 함수 표현을 간결한 문법을 말한다.
람다 표현식은 익명 함수(Anonymous Function) 정도로 생각하면 된다. 자바에서는 “함수(Function)”이라는 개념이 없다. Java8 이전 버전에서는 Only Function만을 정의하는 방식이 없었다. 무조건 하나의 클래스를 선언하고 선언한 클래스 내부에 Method를 정의하는 방식을 사용했다. Java8 버전 부터 람다가 적용되면서 부터 함수 or 함수형 인터페이스라는 개념이 생겼다.
Functional Interface; 함수형 인터페이스
함수형 인터페이스(@FunctionalInterface) 타입을 기대하는 곳 어디에서나 람다 표현식을 사용할 수 있다. 함수형 인터페이스란, 오직 하나의 추상 메서드를 정의하는 인터페이스이다.
람다 표현식으로 함수형 인터페이스의 추상 메서드 구현을 직접 전달할 수 있으므로 전체 표현식을 함수형 인터페이스의 인스턴스로 취급할 수 있다. 아래의 예제 코드를 보면 Runnable 인터페이스를 구현하는 전통적인 방식(익명 클래스)과 람다식을 이용한 코드 2가지 타입이 있다. 위에서 이야기한 것처럼 추상 메서드 구현을 직접 변수 할당 또는 메서드의 파라미터로 전달하는 방식은 Runnable의 구현체(Concrete)를 할당하는 것과 다를 바가 없다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Runnable r1 = () -> System.out.println("Lambda Function Style-A");
Runnable r2 = new Runnable(){
public void run(){
System.out.println("Lambda Function Style-C");
}
};
public static void process(Runnable r){
r.run();
}
process(r1);
process(r2);
process(()-> System.out.println("Lambda Function Style-B"));
익명 클래스를 이용해서 함수형 인터페이스를 구현한 인스턴스를 전달하는 방식(불필요한 코드가 너무 많음.)보다 람다 표현식으로 직접 추상 메서드의 Body 구현체를 전달하는 더 아름다운 코드를 만든다.
1
2
3
4
5
6
7
8
9
10
11
12
// 람다 표현식을 이용해 추상 메서드를 구현 후, 인수로 직접 전달
execute(()->{})
// 익명 클래스를 이용한 인터페이스 구현[
execute(new Runnable(){
public void run(){
System.out.println("Runnable Interface Instance");
}
}
public void execute(Runnable r){
r.run();
}
JDK에서 기본적으로 제공하는 함수형 인터페이스
**Predicate
추상 메서드 → boolean test(T t);
T 타입의 객체를 인수로 받아서 불리언 표현식이 필요한 상황에서 해당 인터페이스를 사용할 수 있다.
1
2
3
4
5
6
@FunctionalInterface
public interface Predicate<T>(){
boolean test(T t);
}
public <T> List<T> emptyStringFilter(){
**Comparator
추상 메서드 → int compare(T o1, T o2);
- void accept(T t)
T 형식의 객체를 인수로 받아서 어떤 동작을 수행하고 싶을 때 해당 인터페이스를 사용할 수 있다. 예를 들어서 Integer 리스트를 인수로 받아서 각 항목에 어떤 동작을 수행하는 forEach 메서드를 정의할 때 Consumer를 활용할 수 있다.
1
2
3
4
5
6
public static void main(String[] args) {
List<Integer> elements = Arrays.asList(new Integer[]{1, 2, 3, 4, 5, 6});
List<Integer> result = new ArrayList<>();
elements.forEach((element) -> result.add(element * 2));
System.out.println(result);
}
Runnable; () -> void
추상 메서드 → void run()
**Callable
추상 메서드 → V call();
Function Descriptor; 함수의 시그니처
함수형 인터페이스의 추상 메서드 시그니처는 람다 표현식의 시그니처를 묘사한다. 람다 표현식의 시그니처를 서술하는 메서드를 함수 디스크립터라고 부른다. Runnable 인터페이스의 경우, run 추상 메서드를 유일하게 하나 가지고 있고 인자와 리턴 값을 가지는 형태이다. 그래서 이 추상 메서드의 시그니처는 ()->void 이다.
쉽게 이야기해서 함수형 인터페이스를 인자로 받는 메서드에 람다 표현식의 구조(인수 타입, 인수의 갯수, 리턴 타입)를 간략하게 표현한 것을 우리는 “함수 디스크립터”라고 부른다고 보면 된다.
람다 표현식은 변수에 할당(A)하거나 함수형 인터페이스를 인수로 받는 메서드(B)로 전달할 수 있으며, 함수형 인터페이스의 추상 메서드와 동일한 시그니처를 갖는다.
특징(장점과 단점)
익명
보통의 메서드와 다르게 이름(Name)이 없다. 개발자의 고민거리를 덜어주었다. 하지만, 코드의 재사용은 불가능하다는 단점이 있다.
함수
특정 클래스에 종속되지 않는다. 그래서 익명 메서드가 아니라, 익명 함수라고 지칭하는 것이다.
전달
메서드 인수(Args)로 전달하거나 변수로 저장할 수 있다.
간결성
비즈니스 로직과 상관 없는 코드 이면서 여러 곳에서 반복적으로 사용되는 코드(보일러플레이트, Boilerplate code)인 익명 클래스의 선언부를 제거할 수 있다.
1 2 3 4 5 6 7 8 9
// Anonymous Class Implement new Comparator<Apple>(){ public int compare(Apple a1, Apple a2){ return a1.getWeight().compareTo(a2.getWeight()); } } // Lambda Expression (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()
파라미터 리스트
람다 파라미터
화살표
람다의 파라미터와 선언부를 구분
람다 바디
람다의 반환값에 해당하는 표현식, 실제 구현부
람다식(Lambda Expression) 문법
메서드로 전달할 수 있는 익명 함수를 단순화한 것.
구조
expression이라고 표현한 것처럼, 람다는 명령형 문법이 아닌 선언형 문법을 지향한다.
(parameters) -> expression(parameters) -> { statements; }
syntax
익명 함수
- Default Functional API
1 2 3 4 5 6 7 8 9 10
// 람다 표현식에는 return이 함축되어 있으므로 return문을 명시적으로 사용하지 않아도 된다. (String s) -> s.length() // int형 문자열 길이를 반환 (Apple a) -> a.getWeight() > 150 // 사과의 무게가 150 초과되는 것들만 리턴 (int x, int y) -> { // void형 람다 함수 System.out.println("x + y"); System.out.println(x+y); } () -> 42 // 파라미터가 없으면 42를 리턴 // Apple 형식의 파라미터 2개를 가지면 int를 반환한다. (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
::메서드 참조; Method reference람다 표현식에서 업그레이드된 형식이
“메서드 참조(Method Reference)”문법이다.기존의 메서드 정의를 재활용해서 람다처럼 전달할 수 있다.
메서드 참조는 특정 메서드만을 호출하는 람다의 축약형
메서드 참조를 새로운 기능이 아니라 하나의 메서드를 참조하는 람다를 편리하게 표현할 수 있는 문법
기존 코드
1 2
inventory.sort((Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));
메서드 참조 적용 코드
1
inventory.sort(comparing(Apple::getWeight);
메서드 참조를 만드는 방법
정적 메서드 참조
람다 표현식
(char c) -> Integer.parseInt(c);메서드 참조 표현식
Integer::parseInt1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
public static int convertToInt(ToIntFunction<String> f, String s) { return f.applyAsInt(s); } public static void main(String[] args) { Function<String, Integer> string2Int4Lambda = (String s) -> Integer.parseInt(s); ToIntFunction<String> string2Int4Method = Integer::parseInt; try { List<Integer> result = new ArrayList<>(); Arrays.asList(new String[]{"10", "20", "30"}).forEach(element -> { result.add(ReferenceMethod.convertToInt(string2Int4Method, element)); }); System.out.println(Arrays.toString(result.toArray())); } catch (Exception e) { System.out.println("Error => " + e.getMessage()); } }
다양한 형식의 인스턴스 메서드 참조
람다 표현식
(String s) -> s.length();(String s) -> s.toUpperCase();메서드 참조 표현식
String::length,String::toUpperCase기존 객체의 인스턴스 메서드 참조
람다 표현식
(name) -> expensiveTransaction.getByValue(name);메서드 참조 표현식
expensiveTransaction::getByValue
Stream API
데이터 처리 연산을 지원하도록 Source에서 추출된
연속된 요소(Sequence of elements, 순서가 있는 요소들)- 연속된 요소: 특정 요소 형식으로 이루어진 연속된 값 집합, 데이터 위주가 아닌 계산을 위주로 하는 자료주고
- Source: 데이터 원천, 스트림에게 데이터를 제공하는 대상들(컬렉션, 배열, I/O 등)을 말한다. 그래서 스트림은 소스에서 데이터를 소비한다
- 데이터 처리 연산: 스트림은 함수형 프로그래밍 언어에서 일반적으로 지원하는 연산과 DB와 비슷한 연산을 제공한다. 예를 들면, map, filter, reduce, find, match, sort 등으로 데이터를 조작할 수 있다. 데이터의 순서를 지키면서 순차적으로 실행할 수도 있고 단순한 방식으로 병렬로 데이터를 처리 할 수도 있다.
Java8 API에 새롭게 추가된 기능
선언형(Declarative)으로 컬렉션 데이터를 처리할 수 있다.멀티스레드 코드를 구현하지 않아도 데이터를 투명하게 병렬로 처리할 수 있다.
연산을 바로 수행하지 않고 Lazy 연산을 수행한다.
외부 반복이 아닌 내부 반복을 지원한다.