Home 함수형 프로그래밍 그리고 람다식과 함수형 인터페이스
Post
Cancel

함수형 프로그래밍 그리고 람다식과 함수형 인터페이스

함수형 프로그래밍

함수형 프로그래밍은 객체지향 프로그래밍과 같은 프로그래밍 패러다임 중 하나이다.

  • 객체 지향 프로그래밍

객체 스스로가 상태를 가지고, 객체간에 메시지를 전달하며 협력하게 된다.

클래스 디자인과 객체들의 관계를 중심으로 코드 작성이 이루어진다. 따라서 상태, 멤버변수, 메서드 등이 긴밀한 관계를 가지고 있다. 특히 멤버변수가 어떤 상태를 가지고 있는가에 따라 결과가 달라진다.

  • 함수형 프로그래밍

함수형 프로그래밍은 작은 단위 함수들이 모여 처리된다. 함수들은 외부와 관계 없고 단지 함수 자신만으로 존재한다.

값의 연산 및 결과 도출을 중심으로 코드 작성이 이루어진다. 함수 내부에서 인자로 받은 값을 별도로 저장하거나 하지 않고, 간결한 과정으로 처리하고 매핑하는데 주 목적을 둔다.

함수형 프로그래밍의 원칙

객체 지향 언어의 특징을 이야기할 때 다형성, 추상화, 상속, 캡슐화 같은 원칙을 이야기 한다. 이와 마찬가지로 함수형 프로그래밍 역시 여러 특징들이 있다.

  • Pure Functions(= Referantial Transparency) - 순수함수
  • FIrst Class Citizen - 일급 객체
  • High Order Function - 고차 함수
  • Immutable Data - 불변성
  • Closure
  • Side Effect가 없는 함수

Pure Function

순수 함수는 동일한 input 값에 대하여 항상 같은 값을 반환해주는 함수를 의미한다.

또한 순수 함수 내부에서 전역변수의 값을 사용하거나, 변경하면서 발생하는 부작용이 없다.

1
2
3
4
5
6
7
private String food = "치킨";

//순수함수 X
public String eat() { return "eat:" + food; }

//순수함수 O
public String eat(String foodName) { return "eat:" + foodName; }

순수 함수는 외부의 영향을 받거나 주지도 않으면서, 동일한 input을 넣었을 때 항상 같은 값을 반환한다. 이를 참조 투명성이라고도 부른다.

함수의 리턴 값은 오로지 입력 값에만 의존해야 한다.

First Class Citizen

자바에서는 class 없이도 함수가 독립적으로 메서드의 인자로 전달되거나, return 값으로 전달받을 수 있다.

1등 시민은 아래와 같은 특징을 가진다.

  • 함수는 다른 함수의 인자(매개변수)로 전달될 수 있다.
  • 함수는 다른 함수의 결과로써 반환될 수 있다.
  • 함수는 변수에 할당될 수 있다.

즉 함수를 데이터 다루듯 다룰 수 있다는 것을 의미한다.

1
2
3
4
5
6
7
8
9
10
@FunctionalInterface
public interface MyFunction {
	void myMethod();
}

public class Lambda {
	static void run(MyFunction f) {
		f.myMethod();
	}
}

함수형 인터페이스와 Lamda 라는 클래스를 위와 같이 정의했다고 하자.

함수는 다른 함수의 인자로 전달될 수 있다.

1
2
3
4
MyFucntion f = () -> System.out.println("myFunction");

Lambda lambda = new Lambda();
lambda.run(f);

함수형 인터페이스를 통해 f에 람다식을 할당했고, f를 lambda.run()의 인자로 넘겨주고 있다.

함수는 다른 함수의 결과로써 반환될 수 있다.

1
2
3
4
5
6
7
int func1(int x) {
	return 2 * x;
}

int func2(int x) {
	return func1(x + 4);
}

High Order Function

고차 함수는 다른 함수를 인수로 받아들이거나, 함수를 리턴하는 함수이다.

함수형 프로그래밍은 위에서 보다시피 1등 시민이 될 수 있기 때문에 고차 함수가 가능해진다. 자바에서 메서드는 인수나 return 하는 값으로 원시 값과 객체만 사용이 가능하다. 하지만 앞서 정의한 Functional Interface를 사용해 고차 함수를 흉내낼 수 있다.

Immutable Data

자바에서는 final과 같은 키워드를 사용하여 값을 변경하는 할당을 방지할 수 있다.

그런데 값이 변경되는 것을 피해야 하는 이유는 무엇일까?

  • 멀티 스레드가 공유하고 있는 하나의 값을 동시에 읽어도 아무런 문제를 일으키지 않는다.
  • 프로그램의 정확성을 높여준다.
    • 값의 변경이 한 곳에서 이루어지지 않고 여러 곳에 흩어져 있다면 코드의 흐름을 이해하거나 테스트하는데 어려움이 생긴다.
    • 큰 시스템에서 흔히 발견되는 가장 수정하기 어려운 버그는 어떤 상태의 변경이 예측 불가능한 임의의 장소에서, 즉 프로그램의 바깥에 존재하는 클라이언트 코드에 있는 경우이다.

불변이기에 상태를 갖지 않고, 상태를 갖지 않기에 병렬 처리에 유리하다.

상태를 공유하는 함수는 스레드가 동시에 접근해서 사용할 수 없다. 때문에 공유 자원에 락을 걸고 하나의 스레드에서만 사용할 수 있도록 제한하는데 이때 교착상태가 발생하게 된다.

불변 컬렉션에 관한 문제 참고

Closure

클로저는 함수 본문이 인수로 전달될 때, 혹은 함수가 자신의 내부에서 정의된 것이 아닌 바깥에서 정의된 변수(자유 변수)를 사용할 때 만들어진다.

코드를 실행하는 런타임에 자유변수를 잠궈두고 나중에 함수가 실제로 실행될 때 꺼내서 사용할 수 있도록 만든다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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("lee"));
System.out.println(hello.apply("lee"));

//결과
Hi lee
Hello lee

여기서 text 라는 변수는 outer 함수의 값이다.

이 text는 inner 함수에서 text + “ “ + name 을 동작하는데 사용된다.

즉 text는 자유 변수이다. text라는 변수는 inner 함수에서 선언한 적이 없다. outer 함수에서 선언한 변수이다.

문제는 greeting 변수로 반환받을 때 이다.

1
greeting = (text) -> { return ?? }

?? 부분은 inner 함수에 해당된다. 따라서 greeting 변수에는 inner 함수가 담기게 된다.

1
greeting = (name) -> { return text + " " + name; }

outer 함수가 실행되고 이렇게 되고나면 outer 함수는 종료되어 버린 상황이다.

그렇다면 outer 함수의 scope 내부에서 선언되었던 text라는 자유 변수는 어떻게 되는 것일까?

클로저가 이 때 등장한다. text 변수가 outer 함수가 끝났다고 메모리에서 사라지지 않고, inner 함수에서 여전히 사용이 가능하다.

outer 함수의 변수이지만 이를 기억해두고 있다가 나중에 필요한 시점에 사용할 수 있도록 해준다.

Side Effect가 없는 함수

함수가 불변한 특성을 가지기 때문에 함수를 사용하는 입장에서 특별한 부수 효과가 발생하지 않는다. 따라서 멀티 스레드 환경에서 안정적인 사용이 가능하다.

람다식과 함수형 인터페이스

시대가 변하고 기술이 발전할수록 CPU의 성능은 발전하고 2코어, 4코어, 8코어.. 와 같이 늘어남에 따라 효율적인 병렬 처리에 대한 요구가 증가했다.

그래서 함수형 프로그래밍에 대한 관심도가 높아지게 되었고 자바에서도 이러한 요구에 걸맞춰 함수형 프로그래밍을 지원하기 위해 만들어진 것이 람다식, 람다식을 보조하기 위해 만들어진 것이 함수형 인터페이스 이다.

람다식

메서드를 하나의 식(Expression)으로 표현할 수 있다고 해서 람다식이라는 이름이 붙었다.

람다식은 익명 클래스를 기반으로 해서 만들어졌다. (람다식은 기존에 사용하던 익명 클래스 스타일로 모두 대체 표현이 가능하다)

  • 익명 클래스가 그랬던 것 처럼 람다식도 특정 클래스에 종속되지 않는다 (메서드와는 다른 점).
  • 생성 시점에 딱 한번 구현.
  • 자바에서는 람다식도 하나의 객체이다.
    • 람다식을 파라미터로 넘기거나, 리턴 타입이 될 수 있고, 자료구조에도 넣을 수 있다.
    • 함수형 프로그래밍을 지원하기 위해 만들어졌다 하는 뜻으로 “함수 객체” 라는 용어를 많이 사용하는 것 같다.
    • 주로 어떠한 값이나 객체를 파라미터로 넘겨주거나 할당했는데, 람다식을 통해 어떠한 함수, 행위를 넘겨줄 수 있게 되었다.

람다식의 진가는 람다식을 이용한 “스트림“을 사용할 때 나온다. 스트림을 사용함으로써 선언형 프로그래밍이 가능해진다.

가장 중요한 것은 람다식은 불변이고, 일급 함수이면서 순수 함수라는 것이다.

람다식도 결국 객체 이므로, 함수형 인터페이스를 이용해서 람다식을 매개변수로 넘길 수 있고, 리턴 타입으로 만들 수도 있고, 자료구조에도 담을 수 있다.

그리고 람다식이 이러한 장점을 갖도록 하기 위해 람다식을 컨트롤 할 수 있는 인터페이스를 만들어두는 것이 필요한데, 이를 위해 나온 것이 함수형 인터페이스이다.

함수형 인터페이스

BiPredicate, Function 포스트에서 다루었던 것들이 함수형 인터페이스에 속한다.

  • 추상 메서드를 단 한개만 갖는 인터페이스
    • 추상 메서드가 람다식과 일대일 매칭이 되어야 하기 때문
    • 추상 메서드가 두개 이상이라면 이는 함수형 인터페이스가 아니다. 람다식이 어떤 추상 메서드를 구현한것인지 알 수 없기 때문이다. 이는 람다식이 아니라 익명 클래스로 쓰인다.
  • 자바에서 람다식을 다룰 수 있는 유일한 도구.
  • @FunctionalInterface 애노테이션을 통해 함수형 인터페이스임을 표시한다
    • 붙이지 않아도 동작은 하지만, 붙여야만 컴파일러의 도움을 받을 수 있다.
1
2
3
4
5
6
7
8
9
10
@FunctionalInterface  
public interface Consumer<T> { 

	void accept(T t); 

	default Consumer<T> andThen(Consumer<? super T> after) { 
		Objects.requireNonNull(after); 
		return (T t) -> { accept(t); after.accept(t); }; 
	} 
}

위는 자바가 기본적으로 제공하는 함수형 인터페이스 중 하나인 Consumer 이다. T 타입 파라미털르 받아 소비하고 void를 리턴한다.

1
2
3
4
5
6
7
8
Consumer<String> consumer = (name) -> { 
	System.out.println("안녕하세요 " + name + "님!"); 
}; 

consumer.accept("자바");

//결과
안녕하세요 자바님!

(참고) 람다식에서의 Variable Capture

람다식은 객체이므로 힙 영역에 저장되는 반면 지역변수는 스택 영역에 저장된다. 서로 다른 영역에 저장되어 있기에 람다식이 호출된 시점에 해당 스택 영역에 저장된 지역변수는 이미 사라져있을 수도 있다.

그렇기에 람다식은 내부적으로 Variable Capture를 통해 지역변수를 미리 복사해둔 다음, 나중에 람다식이 호출되었을 때 복사된 지역변수를 가져다 사용한다. (클로저와 비슷한 개념으로 봐도 될 듯 하다.)

(참고2) 람다식에서 사용하는 지역변수는 왜 effectively final이어야 하는가?

람다식이 지역변수를 캡쳐를 통해 미리 복사해둔다고 했다. 그런데 이 복사된 값들이 변경된다면? 이는 람다식이 상태를 가지게 된다는 것을 뜻하게 되고, 상태를 가지는 함수는 함수형 프로그래밍이라고 할 수 없다.

그렇기 때문에 불변하는 람다식(객체)을 만들어주기 위해서 복사된 지역변수는 변경될 수 없어야 하고, 이는 (effectively) final 이어야만 한다는 것을 뜻한다.

(“effectively” 라는 말이 붙은 이유는 Java8부터는 final이 붙어있지 않은 지역변수라도 소멸되기 전까지 값이 변경되지 않는다면 그 변수를 사실상 final으로 간주해주기 때문이다.)

This post is licensed under CC BY 4.0 by the author.