티스토리 뷰

# 함수형 프로그래밍?

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

 

<함수형 프로그래밍의 특징>

1. 불변 (Immutability)

- 불변인 함수는 부수 효과(Side Effect)가 없다. 

- 불변이기에 같은 값을 넣으면 항상 같은 값을 준다는 것을 보장할 수 있다.

- 불변이기에 상태를 갖지 않는다. 상태를 갖지 않기에 병렬처리에 유리하다. (교착상태에 빠지지 않는다)

- 이렇게 외부 환경에 영향을 받지 않는 함수를 함수형 프로그래밍에서 "순수 함수 (Pure function)" 이라고 불러준다.

 

 

(+) 왜 교착상태에 빠지지 않나?

불변인 함수는 같은 값을 넣으면 같은 값을 준다는 것을 보장한다고 했다. 또한 상태를 갖지 않는다.

그 얘기인 즉슨 여러 스레드가 동시에 같은 함수를 이용해도 같은 값을 넣었을 때 같은 값을 준다는 것을 보장한다는 것이다. (= 상호 배제 조건이 성립하지 않기 때문에 교착상태가 발생하지 않는다)

 

반면 변경 가능한 함수는 상태를 갖는다. (=공유 자원을 갖는다)

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

 

* 교착상태의 발생 조건 * 4가지가 모두 성립해야 교착상태가 발생한다.

> 상호 배제: 한번에 하나의 프로세스만 자원을 사용할 수 있다.

> 점유 대기: 해당 자원을 사용하기 위해 대기중인 프로세스가 있다

> 비선점: 이미 사용중인 자원을 내가 뺏어 쓸 수 없다.

> 순환 대기: 자원을 사용하기 위한 대기줄이 순환 형태(원형)를 이룬다.

 

 

2. 일급 함수 (First-class function)

- 함수를 매개변수로 넘길 수 있어야 한다.

- 함수를 리턴값으로 던질 수 있어야 한다.

- 함수를 변수나 자료구조에 담을 수 있어야 한다. 

 

3. 고차 함수 (Higher-order function)

- 함수의 인자로 함수를 받을 수 있어야 한다

- 함수의 리턴 값으로 함수를 던질 수 있어야 한다

 

4. 함수의 합성 (function composition)

- 함수의 합성을 통해 더 큰 함수를 만들 수 있다.

 

5. 게으른(Lazy) 평가

- 연산을 최대한 뒤로 미뤘다가, 값을 요구하는 시점이 되어야 연산을 수행(평가)한다.

* Lazy의 반댓말은 Eager

 

 

갑자기 뭔 함수 얘기가 나오고 수학시간에나 들었던 고차함수 얘기가 나오니 머리가 어지러워진다.

아무튼

 

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

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

 

# 람다식

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

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

- 익명 클래스가 그랬던 것 처럼 람다식도 특정 클래스에 종속되지 않는다 (메서드와는 다른 점).

- 생성 시점에 딱 한번 구현.

- 자바에서는 결국 람다식도 하나의 객체일 뿐.

-> 람다식도 파라미터로 넘길 수 있고, 리턴타입이 될 수 있고, 자료구조에도 넣을 수 있다.

 

다만 함수형 프로그래밍을 지원하기 위해 만들어졌다 하는 뜻으로 "함수 객체" 라는 용어를 많이 사용하는 것 같다.

- 지금까지는 주로 어떠한 값이나 객체를 파라미터로 넘겨주거나 할당했는데, 이제는 어떠한 함수, 행위를 넘겨줄 수 있게 되었다는 것이다. 별 것 아닌 것 같으면서도 자바에서는 꽤 대단한 일이다.

(근데 C언어 계열에서는 포인터가 있어서 함수 포인터를 이용하면 함수를 그냥 넘겨줄 수 있었던 걸로 기억하는데)

 

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

 

 

또 람다식의 진가는 람다식을 이용한 "스트림"을 사용할 때 나온다.

스트림을 사용함으로써 선언형 프로그래밍이 가능해진다. 스트림 얘기까지 하면 한도끝도 없으니 패스..

return nameList.stream()
               .distinct() // 중복제거
               .filter(name -> name.startsWith("김")) // "김"으로 시작하는 이름만
               .sorted() // 정렬 해서
               .toArray(String[]::new); // 배열로 반환

반복문을 이용한다면 중복 제거 하고 또 특정 문자열로 시작하는 요소 찾기 위해서 Set 하나 만들어서 순회하고 그걸 또 정렬하고 별 난리를 다 쳐야하는데 스트림을 이용하면 저렇게 선언형 방식으로 매우 간결하게 처리가 가능해진다.

정말 놀라운 일이다. 

 

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

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

또한 람다식을 이용한 스트림에서 최종 연산을 찍는 시점 전까지 연산을 뒤로 미루는 지연 평가도 가능하다.

 

 

# 함수형 인터페이스

> 추상 메서드를 단 한개만 갖는 인터페이스

- 추상 메서드가 람다식과 일대일 매칭이 되어야 하기 때문

- 추상 메서드가 두개 이상이라면 이는 함수형 인터페이스가 아니다. 람다식이 어떤 추상 메서드를 구현한것인지 알 수 없기 때문이다. 이는 람다식이 아니라 익명 클래스로 쓰인다.

 

> 자바에서 람다식을 다룰 수 있는 유일한 도구.

 

> @FunctionalInterface 애노테이션을 통해 함수형 인터페이스임을 표시한다

- 붙이지 않아도 동작은 하나, 붙여야만 컴파일러의 도움을 받을 수 있다.

@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>

닉값에 걸맞게 T 타입 파라미터를 받아서 소비하고 void를 리턴한다. 

 

(public abstract) void accept() 메서드를 람다식이 구현해서 사용할 수 있다.

* 인터페이스 속 메서드는 모두 기본적으로 public abstract이다.

** 인터페이스 속 default 메서드, static 메서드는 추상 메서드에 포함되지 않으므로 추상 인터페이스라 볼 수 있다.

 

 

이는 다음과 같이 사용된다.

Consumer<String> consumer = (name) -> {
    System.out.println("안녕하세요 " + name + "님!");
};

consumer.accept("자바");

기본 제공되는 함수형 인터페이스들은 java.util.function 패키지 안에 정의되어있다.

(https://docs.oracle.com/javase/8/docs/api/java/util/function/package-summary.html)

 

 

 

(+) 익명 클래스와 람다식의 차이?

- 람다식은 추상 메서드가 딱 하나인 함수형 인터페이스로만 접근 가능하지만 익명 클래스는 추상 메서드가 몇개이든 무관하게 만들 수 있다.

- 람다식은 상태를 갖지 않지만, 익명 클래스는 가질 수 있다. (내부에 필드 변수를 만들 수 없다는 것이다)

Consumer<String> lambdaConsumer = name -> System.out.println(name + "님을 호출했습니다");

Consumer<String> anonymousConsumer = new Consumer<>() {
    private int eventCount = 0; // 익명 클래스는 상태를 가질 수 있다!! (다만 외부에서는 접근할 수 없다)
    @Override
    public void accept(String s) {
        System.out.println(s + "님을 " + eventCount++ + "번 호출했습니다");
    }
};

lambdaConsumer.accept("자바");

anonymousConsumer.accept("스프링");
anonymousConsumer.accept("스프링");
anonymousConsumer.accept("스프링");

둘 간에는 차이가 분명히 있다.

 

- 익명 클래스는 실제로 클래스를 생성하는 반면 람다식은 해당 클래스에 새로운 메서드만 추가한다.

(익명 클래스에서의 this는 새로 생성된 클래스를 뜻하지만, 람다식에서의 this는 람다식을 포함하고 있던 클래스를 가리킨다)

 

 

(++) 람다식에서의 Variable Capture?

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

 

그렇기에 람다식은 내부적으로 Variable Capture를 통해 지역변수를 미리 복사해둔 다음, 나중에 람다식이 호출되었을 때 복사된 지역변수를 가져다 사용한다.

 

(인스턴스 변수는 힙 영역, 클래스 변수는 클래스 영역에 저장되기 때문에 별 문제가 없다)

 

 

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

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

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

 

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

 

 

 

진짜 여러번 봐도 잘 와닿지도 않고 쉽지 않은 개념인 것 같다. 나만 그런가.

머리 뿌서지는줄

 

 

(참고)

https://alkhwa-113.tistory.com/entry/%EB%9E%8C%EB%8B%A4%EC%8B%9Dfeat-%EC%9D%B5%EB%AA%85-%EA%B5%AC%ED%98%84-%ED%81%B4%EB%9E%98%EC%8A%A4-vs-%EB%9E%8C%EB%8B%A4%EC%8B%9D

 

람다식(feat. 익명 구현 클래스 vs 람다식)

선장님과 함께하는 마지막 자바 스터디입니다. (ㅜ) 자바 스터디 Github github.com/whiteship/live-study whiteship/live-study 온라인 스터디. Contribute to whiteship/live-study development by creating an..

alkhwa-113.tistory.com

https://tecoble.techcourse.co.kr/post/2021-09-30-java8-functional-programming/

 

Java 8 에서 왜 함수형 프로그래밍이 도입되었을까?

우선 당신에게 질문을 던져본다. 객체지향 프로그래밍과 함수형 프로그래밍은 상호 배제 관계에 있다고 생각하는가? 객체지향과 함수형 프로그래밍 Java…

tecoble.techcourse.co.kr

https://codechacha.com/ko/java-effectively-final-vs-final/

 

Java의 Effectively final이란 무엇인가?

Java8에서 final이 붙지 않은 변수의 값이 변경되지 않는다면, 그 변수를 Effectively final이라고 합니다. Effectively final은 익명 클래스 내부에서 접근할 수 있습니다. 개발자의 의도를 알 수 있게 명시

codechacha.com

https://www.scaler.com/topics/java/lambda-expression-in-java/

 

Lambda Expression in Java | Scaler Topics

This article by Scaler Topics, covers the syntax of Lambda expression in java with several examples. It will also take you through the advantages and disadvantages of Lambda and access variables.

www.scaler.com

 

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함