함수형 프로그래밍이란 무엇일까? 스스로의 궁금증에 답하기 위해 공부하며 기록해 본다.
함수형 프록래밍이 엄청 특별하고 그런것은 아니다.
우리가 일반적으로 익숙한 절차지향적 프로그래밍과 같은 프로그래밍 패러다임중 하나이다.
보통 객체지향 패러다임 에서는 객체 스스로가 상태를 가지고 있고, 객체간에 메시지를 전달하면서 협력하게 된다.
하지만 함수형 패러다임 에서는 작은 단위의 함수들이 모여 처리된다.
함수들은 외부와의 관계는 없고 단지 함수 자신만으로 존재한다.
- 객체지향 프로그래밍의 경우, 클래스 디자인과 객체들의 관계를 중심으로 코드 작성이 이루어진다. 따라서 상태, 멤버변수, 메서드 등이 긴밀한 관계를 가지고 있다. 특히 멤버변수가 어떤 상태를 가지고있는가에 따라 결과가 달라진다.
- 함수형 프로그래밍의 경우, 값의 연산 및 결과 도출 중심으로 코드작성이 이루어진다. 함수 내부에서 인자로 받은 값을 별도로 저장하거나 하지 않고, 간결한 과정으로 처리하고 매핑하는데에 주 목적을 둔다.
함수형 프로그래밍의 원칙
객체지향 언어의 특징을 이야기 할 때 다형성, 추상화, 상속, 캡슐화 같은 원칙을 이야기 한다.
마찬가지로 함수형 프로그래밍 역시 여러 특징들이 있다.
1) Pure Funtions(= Referantial Transparency) (순수함수)
2) First Class Citizen (일급 객체)
3) High Order Function (고차함수)
4) Immutable Data (불변성)
5) Closure
6) Side Effect 가 없는 함수
1. Pure Function
순수 함수는 동일한 input 값에 대하여 항상 같은 값을 반환해주는 함수를 의미한다.
또한 순수함수 내부에서 전역변수의 값을 사용하거나, 변경하면서 발생하는 부작용이 없다.
private String food = "치킨";
//순수함수가 아닌
public String eating() { return "Blog Shine eat " + food; }
//순수함수
public static String eating(String name) { return "Blog Shine eat " + name; }
순수함수는 외부의 영향을 받거나 주지도 않으면서, 항상 동일한 input를 넣었을 때 항상 같은 값을 반환한다.
이를 참조 투명성 이라고도 부른다.
즉, 함수의 리턴 값은 오로지 입력 값에만 의존한다는 것이다.
2. First Class Citizen
1등 시민으로서의 함수, 즉 함수형 프로그래밍 에서는 함수가 1등 시민이다.
자바에서의 경우 class 없이도 함수가 독립적으로 메서드의 인자로 전달되거나, return 값으로 전달받을수가 있다.
1등 시민은 다음과 같은 특징을 가진다.
- 함수는 다른 함수의 인자(매개변수)로 전달될 수 있다. (callback 함수)
- 함수는 다른 함수의 결과로서 반환될 수 있다. (수학에서의 합성함수 표현)
- 함수는 변수에 할당될 수 있다. (Binding)
즉, 함수를 데이터 다루듯이 다룰수 있다는 것을 의미한다. 코드로 살펴보자.
- 함수는 다른 함수의 인자로 전달될수 있다.
우선 함수형 인터페이스를 하나 정의하였다. 또한 Lambda 라는 class 또한 만들었다.
@FunctionalInterface
public interface MyFunction {
void myMethod();
}
public class Lambda {
static void run(MyFunction f){
f.myMethod();
}
}
이를 활용하는 main을 살펴보면 다음과 같다.
public static void main(String[] args) {
MyFunction f = () -> System.out.println("전달될 함수 입니다.");
Lambda lambda = new Lambda();
lambda.run(f);
}
함수형 인터페이스로 f로 람다식을 받았다. 이 f를 lambda.run()의 인자로 넘겨주고 있다.
- 함수는 다른 함수의 결과로서 반환될 수 있다.
int func1(int x){
return 2*x;
}
int func2(int x){
return func1(x+4)
}
3. High Order Function
고차 함수(고계 함수) 는 다른 함수를 인수로 받아들이거나, 함수를 리턴하는 함수이다.
함수형 프로그래밍은 함수가 1등 시민이 될 수 있기 때문에 고계함수가 가능해진다.
자바에서 메서드는 인수나 return 하는 값으로 원시 값과 객체만 사용이 가능하다. 하지만 앞서 정의한 Funtional Interface를 사용해 고계 함수를 흉내 낼 수 있다.
4. Immutable Data
자바에서는 final과 같은 키워드를 사용하여 값을 변경하는 할당을 방지할수 있다.
그럼 근본적으로 값이 변경되는 것을 피해야 하는 이유는 무엇일까?
1) 멀티 스레드가 공유하고 있는 하나의 값을 동시에 읽어도 아무런 문제를 일으키지 않는다.
2) 프로그램의 정확성을 높혀준다.
값의 변화가 한 곳에서 이루어지지 않고 여러 곳에 흩어져 있으면 코드의 흐름을 이해하거나 테스트하기 어렵다.
큰 시스템에서 흔히 발견되 는 가장 수정하기 어려운 버그는 어떤 상태의 변경이 예측 불가능한 임의의 장소에 서, 즉 프로그램의 바깥에 존재하는 클라이언트 코드에 있는 경우다.
다음 예를 살펴보자.
public class Customer {
// setter 메서드가 없다
private final List<String> orders;
public List<String> getOrders() { return orders; }
public Customer(...) {...;}
}
Customer 객체를 사용하는 Client(예를 들면 종업원) 입장에서는 getOrders() 를 통해 orders list를 받아가는 것은 당연하다.
하지만 getOrders 메서드 처럼 Customer 객체 외부로 list를 반환하는 함수는 리스트에 대한 통제권을 완전히 상실한다는 의미이다.
이렇게 되면 종업원이 Customer가 모르는 사이에 매뉴를 변경시킬수도 있다.
물론, orders 라는 변수를 final로 선언했고 그에 대한 setter 또한 없지만, 어떤 새로운 리스트를 orders라는 변수에 재할당 하는것만 막을수가 있다.
리스트 내의 내용 자체는 충분히 변경될수가 있다.
이를 코드로 살펴보자.
public class Customer {
private final List<String> orders;
public Customer(List<String> orders) {
this.orders = orders;
}
public List<String> getOrders() { return orders; }
}
이를 사용하는 메인 코드는 다음과 같다.
public class Main {
public static void main(String[] args) {
List<String> foodList = new ArrayList<>();
foodList.add("피자");
foodList.add("국밥");
Customer customer = new Customer(foodList);
List<String> orders = customer.getOrders();
orders.add("치킨");
}
}
처음 foodList 에는 {피자, 국밥} 만이 담겨있었다,
하지만 customer 로부터 orders를 받은 Client(여기서는 Main함수)가 "치킨"을 추가해버리고 있다.
getOrders가 리스트의 복사본을 리턴하도록 만들거나 orders에 대해서 일정하게 통제된 접근만 허용하는 별도의 메서드를 Customer에 더함으로써 이러한 문제를 피할 수 있다. DeepCopy를 이용하는 것 이다.
하지만 이 경우에 복사해오는 overhead가 클수도 있다.
만약 주문을 담고 있는 리스트 자체가 변경 불가능한 것은 물론, 그 안에 담긴 요소 들도 변경이 불가능하다면 이런 걱정은 할 필요가 없다.
이런 변경 가능한 컬렉션을 사용하지 말던, 또는 변경 가능 지점을 최소화 시켜야 한다.
5. 클로져
클로저는 함수 본문이 인수로 전달될 때, 혹은 함수가 자기 내부에서 정의된 것이 아니라 바깥에서 정의 된 변수(자유변수, free variables)를 사용할 때 만들어진다.
코드를 실행하는 런타임때, 자유변수를 “어떤 박스에 넣고 잠가두고” 나중에 함수가 실제로 실행할 때 꺼내서 사용할 수 있도록 만든다.
함수 는 그러한 변수를 선언한 바깥 부분의 코드가 이미 실행되고 사라진 한참 후에 실행될지도 모른다.
자바 는 내부 클래스를 통해서 클로저의 기능을 제한적으로 지원한다.
이러한 내부 클래스는 바깥 영역에 있는 변수가 final로 선언되었을 때에 한해서 사용할 수 있다
코드로 살펴보자.
public static void main(String[] args) {
Function<String, UnaryOperator<String>> greeting = (text) -> {
return (name) -> {
return text + " " + name;
};
};
Function<String, String> hi = greeting.apply("Hi");
Function<String, String> hello = greeting.apply("Hello");
System.out.println(hi.apply("blogShine"));
System.out.println(hello.apply("blogShine"));
}
==== 실행 결과 ====
Hi blogShine
Hello blogShine
main함수 안에 선언된 내용을 보면, text 라는 변수는 outer 함수의 값이다.
이를 inner 함수 에서 text + " " + name 에서 사용하고 있다. 즉, text는 자유변수 이다.
다음은 inner 함수를 살펴보자.
(name) -> {
return text + " " + name;
};
text라는 변수는 inner 함수에서 선언한적이 없다. outer 함수에서 선언했었기 때문이다.
문제는 greeting 변수로 반환받을 때 이다. 간단하게 코드로 작성해보면 다음과 같다.
Function<String, UnaryOperator<String>> greeting = (text) -> {return "뭔가 반환될것"};
"뭔가 반환될것" 의 부분은 inner 함수가 해당된다. 따라서 greeting 변수에는 inner 함수가 담기게 된다.
다음 처럼 말이다.
greeting = (name) -> {return text + " " + name;};
이렇게 되고 나면 outer 함수는 종료되어 버린 상황이다. 그럼 outer 함수의 scope 안에서 선언됬던 text라는 자유 변수는 어떻게 될까?
outer 함수는 끝나버렸으니, text 변수는 메모리 에서 사라져야 할까?
클로저는 이때 등장한다!
이미 외부 함수는 끝났음에도 inner 함수에서 자유변수인 text를 아직도 사용이 가능하다. 이를 클로저 라 부른다.
outer 함수의 변수이지만 이를 기억해두고 있다 나중에 필요한 시점에 사용이 가능한것 이다.
6. Side Effect 가 없는 함수
함수가 불변(immutable)한 특성을 가지기 때문에 함수를 사용하는 입장에서 특별한 부수효과가 발생하지 않는다.
따라서 멀티 스레드 환경에서 안정적인 사용이 가능하다.
예를 들어 sin(x)가 어떤 일을 수행한다고 해도 그 결과는 전적으로 함수를 호출한 곳으로 리턴된다.
이 함수의 호출 때문에 외부의 상태가 변하는 일은 없다.
함수를 호출하는 사람의 입장에서 보았을 때는 (스레드 안정성을 포 함한) 외적인 부수효과가 없도록 만드는 것이 전적으로 함수를 구현하는 사람의 의도에 달려있다.
참고 자료
- 자바 개발자를 위한 함수형 프로그래밍, 딘 왐플러 지음, 임백준 옮김, 한빛미디어
'BackEnd > Java' 카테고리의 다른 글
[Java] equals, hashCode 를 같이 구현하는 이유 (0) | 2022.05.08 |
---|---|
[Java] Java 에서의 Thread, Light Weight Process (0) | 2022.03.30 |
[Java] Exception 기초 (0) | 2022.02.23 |
[Java] StringBuilder와 StringBuffer의 차이 (0) | 2022.02.14 |
[Java] 람다와 익명클래스의 scope (0) | 2022.01.23 |
댓글