티스토리 뷰
[Java] 함수형 프로그래밍(Functional Programming)과 람다식(Lambda Expression) (1/2)
nooblette 2024. 3. 10. 22:47목차
글 목록
- [Java] 함수형 프로그래밍(Functional Programming)과 람다식(Lambda Expression) (1/2)
- [Java] 주요 함수형 인터페이스(Function Interface) (2/2)
배경
최근에 회사에서 내가 개발했던 코드를 리팩토링하며 중복을 개선하려했었는데, 다음과 같은 중복이 여전히 남아있었던 적이 있다. 코드를 직접적으로 작성할 수 없어 예시로 풀어보자면, 전체적인 로직 자체는 거의 유사하지만 결국 호출하는 메서드와 참조하는 객체(repositoryA.save(), repositoryB.save())가 달라 중복 코드가 발생했던 케이스다.
public class ServiceA {
private final ServiceC serviceC;
private final RepositoryA repositoryA;
public void methodA(){
List<ObjectA> objectAList = repositoryA.findAll();
objectAList.forEach(objectA -> {
// objectAList의 원소별 처리 로직
serviceC.call(objectA);
})
}
public class ServiceB {
private final ServiceC serviceC;
private final RepositoryB repositoryB;
public void methodB(){
// ServiceA와 중복 발생
List<ObjectB> objectBList = repositoryB.findAll();
objectBList.forEach(objectB -> {
// objectBList 원소별 처리 로직
serviceC.call(objectB);
})
}
어떻게 중복을 개선할 수 있을지 다른 팀원에게 얘기하고 함께 고민하며 템플릿 메서드 패턴을 비롯해서 해결 방안을 얘기했었는데, 나는 그 중 함수형 인터페이스를 통해 문제를 해결했다. 분명 알고있던 개념이었으나 실무에서 이처럼 문제가 생겼을때 바로 해결방안 중 하나로 고려할정도로 능숙하지는 않다는 생각이 들어 이번 기회에 함수형 인터페이스를 비롯해서 java에서의 함수형 프로그래밍, 람다식의 관한 내용을 정리해보았다.
내용
함수형 인터페이스는 java 8부터 제공하는 기능인데, 위 예시처럼 전체적인 로직은 유사하지만 호출하는 메서드가 다른 경우를 비롯하여 다양한 케이스에서 데이터 처리 방식의 추상화를 제공하고 이를 통해 유연한 데이터 처리를 지원한다.
데이터 처리 방식의 추상화란, 데이터 처리부에서는 데이터 자체만 가지고 있을 뿐 데이터 처리 방식은 정해져 있지 않다는 의미로 볼 수 있다. 데이터 처리 방식(즉, 함수)은 데이터를 처리하는 곳이 아닌 외부에서 정의하고 이 정의한 함수 자체를 데이터 처리부로 보내 데이터를 처리하는데, 외부로부터 데이터 처리 방식을 전달받음으로써 (데이터 처리 내부에 정의된 방식으로만 데이터를 처리하는게 아니라) 유연하게 데이터를 처리할 수 있다.
java 8에서 제공하는 함수형 인터페이스 API로는 대표적으로 Runnable, Function, Supplier, Consumer, Predicate, Operator 등이 있다. 만일 함수형 인터페이스 기능이 필요하다면 개발자가 직접 정의하는게 아니라 이러한 API를 가져다가 사용하면 된다. 대부분의 경우 java에서 기본적으로 제공하는 방식으로 해결할 수 있고 다른 특별한 이유가 없다면 개발자가 새로 직접 정의하는 것은 오히려 코드를 이해하는데 어렵게 만들 수 있어 권고하지 않는다.
함수형 프로그래밍(Functional Programming)
함수형 인터페이스에 대해 작성하기에 앞서 함수형 프로그래밍과 람다식(Lambda Expression)에 대해 먼저 소개해보려고 한다. 함수형 인터페이스(Functional Interface)는 결국 java에서 함수형 프로그래밍 방식으로 개발하기 위한 방법 중 하나이며, 함수형으로 코드를 작성할때 대부분 람다식으로 작성하기 때문이다.
먼저 함수형 프로그래밍은 내용에서 말했듯이 함수를 정의하고 이 함수 자체를 데이터 처리부로 보내 데이터를 처리하는 기법이다. (출처: 이것이 자바다) 즉 데이터 처리부에서는 데이터만 존재하며 데이터를 어떻게 처리할지는 정의되어 있지 않다. 다시 말해 동일한 데이터일지라도 위 이미지에서 데이터 처리부로 전달한 함수 A와 함수 B의 처리 내용이 다르다면 다른 결과를 반환하게 된다. 이러한 방식의 함수형 프로그래밍은 데이터 처리의 다형성을 지원한다.
함수형 인터페이스(Functional Interface)
단 하나의 추상 메서드를 갖는 인터페이스를 함수형 인터페이스(Functional Interface)라고 하며, @FunctionIalnterface 애노테이션을 통해 컴파일 과정에서 인터페이스가 함수형 인터페이스임을 보장한다. (컴파일 과정에서 이 어노테이션이 붙은 인터페이스는 추상 메서드가 하나인지 검사한다.) (출처: 이것이 자바다)
예를 들어 다음은 계산 방식의 추상화를 갖는 Calculable이라는 함수형 인터페이스(Functional Interface)가 된다.
@FunctionalInterface
public interface Calculable {
int calculate(int x, int y);
}
람다식(Labmda Expression)
java는 객체 지향언어이므로 데이터 처리부로 보내는 함수도 결국 객체로써 처리된다.
만약 매개변수로 Calculable 인터페이스를 받고 두 변수 x, y에 대해 추상화된 어떠한 연산을 수행하고 그 결과를 반환하는 action()이라는 메서드가 있다고 가정해보자.
public int action(Calculable calculable) {
int x = 10;
int y = 4;
return calculable.calculate(x, y);
}
이 action() 메서드를 호출하여 더하기 연산을 수행하려면 1. 두 매개변수의 합을 반환하는 Calculable 함수형 인터페이스의 구현 객체를 정의하고 2. action() 메서드의 매개변수로 전달해야한다.
// 두 매개변수의 합을 반환하는 Calculable 함수형 인터페이스의 구현 객체를 정의
public class Adder implements Calculable {
@Override
public calculate(int x, int y){
return x + y;
}
}
// Adder의 인스턴스를 action() 메서드의 매개변수로 전달
public static void main(){
int sum = action(new Adder());
}
만약 action() 메서드에 모든 사칙 연산에 대한 데이터 처리가 필요하다면, 빼기, 곱하기, 나누기, 나머지 연산 등 모든 연산에 대해 Calculable 인터페이스의 구현 클래스를 정의해야하는데 이는 매우 반복적이고 귀찮은 작업일 것이다.
이러한 불편과 무의미한 반복을 줄이기 위해 java에서는 익명 구현 객체를 제공한다. 익명 구현 객체를 통해 구현 클래스를 직접 정의하지 않고 인스턴스를 사용할 수 있다. (구현 클래스를 정의하지 않아 이름이 없어 익명 구현 객체라고 이해했다.)
예를 들어 뺄셈 연산이 필요하다면 직접 Calculable 구현 클래스를 정의하지 않고 action 메서드에 다음과 같이 호출할 수 있다.
// action 메서드의 매개변수로 Calculable 함수형 인터페이스의 익명 객체 전달
int sub = action(new Calculable() {
@Override
public int calculate(int x, int y) {
return x - y;
}
});
이때 매개변수로 전달한 익명 객체의 데이터 처리 방법에 해당하는 중괄호로 감싼 메서드 구현부와 매개변수 x, y만을 action() 메서드에 매개변수로 전달할 수 있다. 이처럼 데이터 처리부에 제공할 처리 방법(= 함수의 구현부)을 매개변수로 받는 중괄호 블록을 람다식(Lambda Expression)이라고 한다.
int sub = action((x, y) -> {
return x - y;
});
// 데이터 처리부가 return문을 포함해서 한 줄이라면 중괄호를 생략할 수 있다.
int sub = action((x, y) -> x-y);
어떤 람다식을 매개변수로 전달하느냐에 따라 실행 결과가 달라질 것이다.
public class Test {
int sub = action((x, y) -> x-y);
System.out.println("sub = " + sub); // 6을 출력
int multiple = action((x, y) -> x * y);
System.out.println("multiple = " + multiple); // 40을 출력
}
함수형 인터페이스의 장점
1. 직관적이며 간결하고 읽기 쉬운 코드 작성
함수형 인터페이스를 사용하면 우선 위와 같은 람다 표현식을 함께 사용할 수 있는데, 람다 표현식을 활용하면 더 직관적이며 간결하고 읽기 쉬운 코드를 작성할 수 있다. (과도하게 사용하지 않는 선에서)
2. Streams API 활용
함수형 인터페이스는 java의 스트림 API와 함께 매우 유용하게 사용할 수 있다. 스트림 API에 대해서는 [Java] Streams API의 장점과 사용시 주의사항에서 소개한 적이 있는데, 경우에 따라 함수형 인터페이스와 함께 사용할 경우 병렬 처리와 스트림 처리 등 더 효율적으로 데이터를 다룰 수 있다.
3. 유연한 데이터 처리
마지막으로 함수형 인터페이스는 데이터 처리의 다형성을 제공하는데, 이를 통해 다양한 함수를 정의하고 유연하게 재사용할 수 있다. 이러한 함수들은 인터페이스를 구현하는 다른 클래스나 메서드에서 사용될 수 있으며, 이는 코드의 재사용성을 높이고 모듈화에도 도움이 된다.
'Java & Kotlin' 카테고리의 다른 글
[Java] 자바의 두가지 정렬 방법(Comparable, Comparator) (0) | 2024.08.01 |
---|---|
[Java] 주요 함수형 인터페이스(Function Interface) (2/2) (0) | 2024.03.19 |
[Java] 변수 선언과 할당을 나눠야하는 경우와 그렇지 않은 경우 (0) | 2023.12.31 |
[Java] 메서드 및 필드 선언 순서 컨벤션(in 자바) (2) | 2023.12.31 |
[Java] Streams API의 장점과 사용시 주의사항 (0) | 2023.12.20 |