Language/Java
[Java 8] 객체로서의 함수
by ocwokocw
2021. 2. 10.
- https://flyingbytes.github.io/programming/java8/functional/part1/2017/01/23/Java8-Part1.html
- 개요
Java 8 에서 함수는 1급 객체가 되었다. 이 말은 함수를 다른 함수의 인자가 되거나 반환형이 되거나 객체로 저장될 수 있다는것을 의미한다.
- 왜 함수를 객체로 저장해야 하는가?
- 완전히 private 한 함수
코드의 품질은 중요하다. 기본적으로 어떤 객체에서 근간이 되는 코드를 다른 Client 에게 보여주지는 않는다. 다른 Client 와는 객체의 public 메소드로 상호작용을 한다. 만약 클래스에서 1개의 메소드만 보이고 나머지는 은닉화 되어있는 함수를 만들고 싶다면 어떻게 해야할까? 1급 객체의 함수에서는, 1개의 메소드만 보이는 함수를 객체로 저장할 수 있다.
- 디자인 패턴의 향상
만약 대규모 소프트웨어 프로젝트를 해봤다면 코드가 얼마나 더러운지 알 것이다. 디자인 패턴은 이런 이유때문에 발명되었다. 이런 패턴들 중에서 전략패턴이라는것이 존재한다. 전략패턴은 파라미터에 따라 알고리즘을 변경하는 패턴이다. 각 알고리즘은 특정 인터페이스를 구현한 각자의 클래스에 작성되어야 한다. 하지만 객체에 함수를 저장할 때, 단지 각 알고리즘에 대한 "함수 객체" 만 있으면 된다. 이런 함수 객체는 더 코드를 더 명료하고 간단하게 만든다.
- 고차 함수(higher-order function)
모든 객체는 메소드의 파라미터가 될 수 있다. 함수를 파라미터로 받거나 반환하는 함수를 고차 함수라고 한다. 해당 예제를 배우기전에 함수를 객체로 저장하는 법부터 살펴보자.
- 함수의 객체화
Java 8 에서 java.util.Function<T, R> Interface 가 처음으로 소개되었다. 이 인터페이스 덕분에 함수를 인자로 취하거나 객체로 반환할 수 있게 되었다. 제네릭 T 는 인자의 형이고, R은 반환 형이다.
예제: Compute 함수
고차함수에 대한 아주 간단한 예제를 살펴보자. 이 함수는 함수와 정수를 인자로 받아서 해당 정수와 주어진 함수를 계산한다.
1
2
3
|
public static Integer compute(Function<Integer, Integer> function, Integer value) {
return function.apply(value);
}
|
이제 이 함수를 정수를 invert 하는(-1을 곱하는) 함수로 사용해보자.
1
2
3
4
5
6
7
8
9
10
11
12
13
|
public class AwesomeClass {
private static Integer invert(Integer value) {
return -value;
}
public static Integer invertTheNumber(){
Integer toInvert = 5;
Function<Integer, Integer> invertFunction = AwesomeClass::invert;
return compute(invertFunction, toInvert);
}
}
|
여기서 2 가지 사항을 주의깊게 살펴보자.
1
|
return function.apply(value);
|
우선 함수 객체의 apply 메소드는 인자를 취해서 메소드의 결과를 반환한다. 위의 예제에서는 아래와 같이 작성할 수도 있다.
그리고 또
1
|
Function<Integer, Integer> invertFunction = AwesomeClass::invert;
|
위의 예제에서는 메소드 레퍼런스(::) 문법을 사용하였다. :: 연산자를 사용하여 invert() 함수를 Function 객체로 만들었다. 이것은 함수를 객체로 저장하는 2 가지 방법 중 하나이다. 하지만 이런 코드는 보기에 좀 복잡하다. 사실 아래처럼 리팩토링 할수도 있다.
1
2
3
4
5
6
7
8
9
10
11
12
|
public class AwesomeClass {
private static Integer invert(Integer value) {
return -value;
}
public static Integer invertTheNumber(){
Integer toInvert = 5;
return invert(toInvert);
}
}
|
compute 함수나 fp 방식의 프로그래밍이 전혀 사용되지 않았지만 깔끔한 코드이다. fp 코드를 유용하게 사용하기 위해서, 함수를 객체로 저장하기 위한 2 번째 방법을 사용해야 한다. 이 2 번째 방법이 바로 람다(Lambdas, 또는 익명 함수 - anonymous function)이다.
- 람다(Lambdas)
Java 8 에서 람다를 다루기 위해서 새로운 문법을 사용해야 한다.
예제: 두 정수 더하기
Java 7 에서는 2 개의 정수를 더하는 메소드를 아래와 같이 작성하였다.
1
2
3
|
public Integer add(Integer a, Integer b) {
return a + b;
}
|
그리고 Java 8 에서는 아래와 같이 사용하면 된다.
1
|
BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b;
|
es 6 의 arrow function을 사용해보았다면 위의 코드가 익숙할 것이다. BiFunction 은 java.util 패키지 안에 있는 또 다른 인터페이스인데 2개의 인자와 1개의 반환형을 나타낸다. Lambdas 의 소괄호에서는 인자를 정의한다. 소괄호에서 Type 을 명시하지 않아도 된다. Java 7 에서 아래처럼 작성한것과 동치이다.
그 다음 "->" 라는 화살표를 살펴보자. 이것은 중괄호와 같고 함수의 헤더를 본체로 부터 분리시킨다. 만약 1번의 계산만 한다면 return 구문은 작성할 필요가 없다. 함수의 본체가 커진다면 중괄호를 사용한다.
1
2
3
4
|
BiFunction<Integer, Integer, Integer> add = (a,b) -> {
Integer result = a + b;
return result;
};
|
하지만 대부분 익명함수를 사용할때는 한줄로 작성해서 중괄호와 return 구문을 사용하지 않는다. 익명함수를 이용해서 앞의 compute 함수를 사용하여 다시 리팩토링을 해보자.
1
2
3
4
5
6
7
8
|
public class AwesomeClass {
public static Integer invertTheNumber(){
Integer toInvert = 5;
return compute((a) -> -a, toInvert);
}
}
|
코드가 훨씬 깔끔해졌다. 앞에서 invert 함수를 Lambda 로 만들었다. 이전 fp 버전의 함수나 Java 7 의 예제보다 훨씬 코드가 깔끔해졌다. 정수를 invert 하기 위해 추가적인 메소드를 생성하지 않았고 단지 lambda 한줄만을 이용하였다.
물론 return -value 를 하면되는데 왜 이런짓을 할까라고 생각할 수 있다. 이번 경우에는 그게 훨씬 나을것이다. 하지만 만약 실행시점에 함수가 변경되어야 한다면, 아래처럼 우리가 작성한 버전이 사용되어야 할 것이다.
1
2
3
4
5
6
7
8
|
public class AwesomeClass {
public static Integer changeTheNumber(Function<Integer, Integer> func){
Integer toChange = 5;
return compute(func, toChange);
}
}
|
댓글