Java Stream이란? 1탄 함수형 프로그래밍.

Java Stream이란? 1탄 함수형 프로그래밍.

2021, Mar 16    

정리를 하게 된 이유.

자바 개발을 해보면서, 좋은 기술들이 많은데, 활용을 못하고 있는 것 같아서 정리를 해본다.

먼저, 함수형 프로그래밍에 대해 간단하게 알아보고 Java Stream에 대해서 이어서 정리를 해보기로 한다.

먼저 망나니개발자님의 블로그에서 잘 정리되어있어서 읽어보고 내것으로 다시 만들어 보기로 한다.

함수형 프로그래밍

프로그래밍 패러다임

  • 프로그래밍 패러다임은 프로그래밍의 패러다임 형태이다. 소프트웨어 공학을 할 때의 패러다임 형태인 방법론과 비교된다. 프로그래밍 패러다임은 프로그래머에게 프로그래밍의 관점을 갖게 해 주고, 결정하는 역할을 한다.

최근 프로그래밍 패러다임은 크게 아래와 같이 구분할 수 있다.

  • 명령형 프로그래밍:무엇(What)을 할 것인지 나타내기보다 어떻게(How)할 건지를 설명하는 방식
    • 절차지향 프로그래밍: 수행되어야 할 순차적인 처리 과정을 포함하는 방식(C,C++)
    • 객체지향 프로그래밍: 객체들의 집합으로 프로그램의 상호작용을 표현(C++,Java,C#)
  • 선언형 프로그래밍: 어떻게 할건지(How)를 나타내기보다 무엇(What)을 할 건지를 설명하는 방식
    • 함수형 프로그래밍: 순수 함수를 조합하고 소프트웨어를 만드는 방식(클로저,하스켈,리스프)

함수형 프로그래밍의 등장

명령형 프로그래밍을 기반으로 개발했던 개발자들은 개발하는 소프트웨어의 크기가 커짐에 따라, 복잡하게 엉켜있는 스파게티 코드를 유지보수 하는 것이 매우 힘들다는 것을 깨닫게 되었다. 그리고 이를 해결하기 위해 함수형 프로그래밍이라는 프로그래밍 패러다임에 관심을 갖게 되었다. 함수형 프로그래밍은 거의 모든 것을 순수 함수로 나누어 문제를 해결하는 기법으로, 작은 문제를 해결하기 위한 함수를 작성하여 가독성을 높이고 유지보수를 용이하게 해준다.

유명한 책인 클린 코드의 저자 Robert.C.Martin은 함수형 프로그래밍을 대입문이 없는 프로그래밍이라고 정의했다.

함수형 프로그래밍의 특징

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

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

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

이러한 부수 효과들을 제거한 함수들을 순수함수라고 부르며, 함수형 프로그래밍에서 사용하는 함수는 이러한 순수 함수들이다.

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

순수함수를 이용하면 얻을 수 있는 효과는

  • 함수 자체가 독립적이며, Side-Effect가 없기 때문에 Thread에 안전성을 보장 받을 수 있다.
  • Thread에 안정성을 보장 받아 병쳘 처리를 동기화 없이 진행할 수 있다.

1급 객체란

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

함수형프로그래밍에서는 함수는 1급 객체로 취급받기 때문에, 함수를 파라미터로 넘기는 등의 작업이 가능 한 것이다. 또한 우리가 일반적으로 알고 개발했던 함수들은 함수형 프로그래밍에서 정의하는 순수 함수들과는 다르다는 것을 알아야한다.

자바를 이용하여 함수형 프로그래밍 예시

함수형을 적용하지 않은 코드


public class WordCount{

 private static List<String> WORDS = Arrays.asList("TONY", "a", "hULK", "B", "america", "X", "nebula", "Korea");

 private static Map<String,Integer> wordPrefixFreq(){
    Map<String,Integer> wordCountMap = new HashMap<>();
    String prefix;
    Integer count;
    for(String workd : WORDS){
        prefix = word.substring(0,1);
        count = wordCountMap.get(prefix);
        if(count == null){
            wordCountMap.put(prefix,1);
        }else{
            wordCountMap.put(prefix,count+1);
        }
    }
    return wordCountMap;
 }

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

}

함수형을 적용한 코드

public class WordCount {

    private static List<String> WORDS = Arrays.asList("TONY", "a", "hULK", "B", "america", "X", "nebula", "Korea");

    private static Map<String,Integer> wordPrefixFreq(){
        Map<String,Integer> wordCountMap = new HashMap<>();
        WORDS.stream().map(w->w.substring(0,1)).forEach(prefix->wordCountMap.merge(prefix,1,(oldValue,newValue)-> (newValue+= oldValue)));
    }
    
    public static void main(String[] args){
        final Map<String,Integer> map = wordPrefixFreq();
        map.keySet().forEach(k -> System.out.println(k+":"+map.get(k)));
    
    }

}

위 코드에서

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

이 세가지 조건이 추가가 되면 함수형 적용 이전의 코드에서는 코드가 상당히 길어질 것이고, 복잡해질 것이다.

하지만 함수형 프로그래밍에서는 아래와 같이 해결 가능하다.

public class Main{
    private static List<String> WORDS = Arrays.asList("TONY", "a", "hULK", "B", "america", "X", "nebula", "Korea");

    public static String wordPrefixFreq(){
        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)throws IOException{
        final String result = wordPrefixFreq();
        System.out.println(result);
    }
}