프로그래밍 언어/[ Java ]

[ Java ] 23. 자바에서의 함수형 프로그래밍

kim.svadoz 2021. 11. 16. 18:00
반응형

함수형 프로그래밍


프로그래밍의 패러다임은 프로그래머에게 프로그래밍의 관점을 갖게 하고 어떻게 코드를 작성할지 결정하게 하기 때문에 큰 역할을 한다고 볼 수 있다.

  • 명령형 프로그래밍 : 무엇(What)을 할 것인지 보다는, 어떻게(How)할 건지 설명
    • 절차적 프로그래밍 (Tow-Down)
      • 단순히 순차적으로 프로그래밍 하는 것이 아니라, 프로시저 콜이라는 함수 호출을 통해 명령을 수행하는 것. (C, C++)
    • 객체지향 프로그래밍
      • 실제 세계를 모델링하여 객체 간의 상호작용을 나타내는 개발 방법(C++, Java, C#)
  • 선언형 프로그래밍 : 어떻게(How)를 나타내기 보다 무엇(What)을 할 건지 설명
    • 함수형 프로그래밍
      • 순수 함수를 조합하고 소프트웨어를 만드는 방식(클로저, 하스켈, 리스프)

 

> 함수형 프로그래밍의 등장

명령형 프로그래밍을 기반으로 개발했던 개발자들은, 소프트웨어의 크기가 커짐에 따라서 복잡하게 엉켜있는 스파게티 코드를 유지보수하는 것이 매우 힘들다는 것을 깨닫게 되었다.

이를 해결하기 위해서 함수형 프로그래밍이라는 새로운 패러다임에 관심을 갖게 된다.

 

함수형 프로그래밍은 거의 모든 것을 순수 함수로 나누어서 문제를 해결하는 기법으로, 작은 문제를 해결하기 위한 함수를 작성해 가독성을 높이고 유지보수를 용이하게 해주는 것이 장점이다.

클린 코드(Clean Code)의 저자 Robert C.Martin은 함수형 프로그래밍을 대입문이 없는 프로그래밍으로 정의하였다.

Functionl Programming is programming without assignment statements.

위와 같이 함수형 프로그래밍은 대입문을 사용하지 않는 프로그래밍이며, 작은 문제를 해결하기 위한 함수를 작성한다고 하였다.

 

명령형 프로그래밍에서는 메소드를 호출하면 상황에 따라 내부의 값이 바뀔 수 있다. 즉, 우리가 개발한 함수 내에서 선언된 변수의 메모리에 할당된 값이 바뀌는 등의 변화가 생길 수 있다.

 

하지만, 함수형 프로그래밍에서는 대입문이 없기 때문에 한 번 할당된 값은 새로운 값으로 변할 수 없다는 특징이 있다.

함수형 프로그래밍의 기본 원리는 함수를 1급 시민(First-Class Citizen) 또는 1급 객체(First-Class Object)로 관리한다.

 

함수형 프로그래밍의 특징을 한줄로 요약하면 이와 같다.

부수 효과가 없는 순수 함수 1급 객체로 간주하여 파라미터로 넘기거나 반환값으로 사용할 수 있으며, 참조 투명성을 지킬 수 있다.

 

여기서 부수 효과(Side Effect)란 다음과 같은 변화 또는 변화가 발생하는 작업을 의미한다.

  • 변수의 값이 변경됨
  • 자리에서 자료 구조를 수정함
  • 객체의 필드값을 섲렁함
  • 예외나 오류가 발생하여 실행이 중단됨
  • 콘솔 또는 파일 I/O가 발생함

 

이러한 부수 효과(Side Effect)를 제거한 함수들을 순수 함수(Pure Function)이라고 하며, 함수형 프로그래밍에서 사용하는 함수는 이러한 순수 함수들이다.

  • Memory나 I/O 관점에서 Side Effect가 없는 함수.
  • 함수의 실행이 외부에 영향을 끼치지 않는 함수.

 

이러한 순수 함수(Pure Function)을 이용해서 얻을 수 있는 효과는 다음과 같다.

  • 함수 자체가 독립적이며 Side Effect가 없기 때문에 안정적인 쓰레드를 보장할 수 있다.
  • Thread가 안정하기 때문에 병렬 처리를 동기화 없이 진행할 수 있다.

 

또한, 1급 객체란 다음과 같은 것들이 가능한 객체를 의미한다.

  • 변수나 데이터 구조 안에 담을 수 있고
  • 파라미터로 전달할 수 있어야 하고
  • 반환값으로 사용할 수 있어야 하며
  • 할당에 사용된 이름과 문관하게 고유한 구별이 가능해야 한다.

 

함수형 프로그래밍에서 기본적으로 함수는 1급 객체로 취급받기 때문에 함수를 파라미터로 넘기는 작업이 가능하다.

우리가 일반적으로 알고 개발했던 함수들은 함수형 프로그래밍에서 정의하는 순수 함수들과는 다르다는 것을 인지해야 한다.

 

참조 투명성(Referential Transparency)

  • 동일한 인자에 대해 항상 동일한 결과를 반환해야 한다.
  • 참조 투명성을 통히 기존의 값은 변경되지 않고 유지된다.(Immutable Data)

 

명령형 프로그래밍과 함수형 프로그래밍에서 사용하는 함수는 사이드이펙트의 유/무에 따라 차이가 있다. 그에 따라서 함수가 참조에 투명한지 안한지 나뉘어 지게 되는데, 참조에 투명하다는 것은 말 그대로 함수를 실행해도 어떠한 상태변화 없이 항상 동일한 결과를 반환하여 동일하게(투명하게) 실행 결과를 참조(예측)할 수 있다는 것을 의미한다.

 

즉, 어떤 함수 f에 어떠한 인자 x를 넣고 f를 실행하게 되면, f는 입력된 인자에만 의존하므로 항상 f(x)라는 동일한 결과를 얻는다.

 

이처럼 부작용을 제거해서 프로그램의 동작을 이해하고 예측을 용이하게 하는것이 함수형 프로그래밍으로 개발하려는 핵심 동기 중 하나이다. 또한, 값의 대입 없이 항상 동일한 실행에 대해 동일한 결과를 반환하기 때문에, 병렬 처리 환경에서 개발할 때 Race Condition에 대한 비용을 줄여준다.

 

> Java에서의 함수형 프로그래밍

자바에서 어떤 List에 저장된 단어들의 접두사가 각각 몇개씩 있는지 Map으로 저장하는 코드를 작성해보자.

- 함수형 적용 X

import java.util.*;

public class Main {
    private static List<String> Words = Arrays.asList("APPLE", "BANANA", "orange", "PINEAPPLE", "korea");

    private static Map<String, Integer> setPrefixFreq() {
        Map<String, Integer> map = new HashMap<>();
        String prefex;
        Integer cnt;
        for (String word : Words) {
            prefix = word.substring(0, 1);
            cnt = map.get(prefix);
            if (cnt == null) {
                map.put(prefix, 1);
            } else {
                map.put(prefix, cnt + 1);
            }
        }

        return map;
    }

    public static void main(String[] args) {
        final Map<String, Integer> map = setPrefixFreq();
        map.keySet.forEach(k -> System.out.println(k + ":" + map.get(k)));
    }
}

함수형 프로그래밍을 적용하지 않은 코드는 List에서 루프를 돌면서 접두사 하나 잘라내고 그 갯수를 Map에 저장한다.

이 코드에 함수형 프로그래밍을 적용하면 더욱 간결하고 가독성있는 코드로 변경할 수 있다.

- 함수형 적용 O

Java는 대표적으로 객체지향 프로그래밍 언어이기 때문에 함수형으로 개발하기 위해서 별도의 도구가 필요하다.

JDK8에서부터 Stream API와 함수형 인터페이스(Functional Inteface)의 람다 등을 제공하기 때문에 Java 8 이상의 버전을 사용해야 함수형 프로그래밍을 사용할 수 있다.

import java.util.*;

public class Main {
    private static List<String> Words = Arrays.asList("APPLE", "BANANA", "orange", "PINEAPPLE", "korea");

    private static Map<String, Integer> setPrefixFreq() {
        Map<String, Integer> map = new HashMap<>();
        Words.stream().map(w -> w.substring(0, 1)).forEach(prefix -> map.merge(prefix, 1, (v1, v2) -> (v2 += v1)));
        return map;
    }

    public static void main(String[] args) {
        final Map<String, Integer> map = setPrefixFreq();
        map.keySet.forEach(k -> System.out.println(k + ":" + map.get(k)));
    }
}

stream()을 통해 함수형 프로그래밍을 위한 Stream 객체를 생헝하고, map()을 통해 Stream 객체의 단어들을 prefix로 변형시키고 있다.

그리고 forEach로 prefix를 map에 추가하고 있다.

 

다음과 같은 추가 요구 사항이 생겼다고 가정해 보자.

  • 단어의 크기가 2 이상인 경우에만 처리할 것.
  • 모든 단어를 대문자로 변환하여 처리할 것.
  • 스페이스로 구분한 하나의 문자열로 변환 할 것

이러한 요구사항을 기존의 코드에 반영한다면 상당이 코드가 길어지고 복잡해질테지만, 함수형 프로그래밍을 적용하면 비교적 간단하게 처리할 수 있다.

import java.util.*;

public class Main {
    private static List<String> Words = Arrays.asList("APPLE", "BANANA", "orange", "PINEAPPLE", "korea");

    private static Map<String, Integer> setPrefixFreq() {
        return Words.stream().filter(w -> w.length() > 1).map(String::toUpperCase).map(w -> w.substring(0, 1)).collect(Collectors.joining(" "));
    }

    public static void main(String[] args) {
        final Map<String, Integer> map = setPrefixFreq();
        map.keySet.forEach(k -> System.out.println(k + ":" + map.get(k)));
    }
}

참조

https://mangkyu.tistory.com/111

반응형