코딩하는 털보

21.09.19 TIL 본문

Diary/Today I Learned

21.09.19 TIL

이정인 2021. 9. 19. 22:20

어제는 람다를 공부해봤으니 오늘은 함수형 프로그래밍과 스트림(java.util.stream.Stream)에 대해서 공부한다.

참고 : https://www.slideshare.net/arawnkr/api-42494051

함수형 프로그래밍(FP: Functional Programming)

함수형 프로그래밍은 순수 함수를 조합하고 공유 상태(shared state), 변경 가능한 데이터(mutable data) 및 부작용(side-effects) 을 피하는 기본 원칙에 따라 소프트웨어를 구성하는 프로그래밍 패러다임 입니다.

내가 보통 사용하던 프로그래밍 방식(for, if문 같은 로직에 따라 구현된 기능의 메서드를 호출하는 것. 어떻게 할 것인가(How)를 표현)은 명령형 프로그램이다. 그와 대비적으로 선언형 프로그래밍은 무엇을 할 것인가(What)를 표현한다.

함수형 프로그래밍은 명령형이 아닌 선언적 방식으로 구현된다. 즉, 흐름 제어를 명시적으로 기술하지 않고도 프로그램 로직이 표현된다는 것을 의미 한다.

닌텐도 게임의 캐릭터들 이름 목록을 대문자로 바꾸고 정렬하는 프로그램을 작성해보자. 게임매렵다.

먼저 내가 원래 하던식으로 해봤다.

public static List<String> solution(List<String> list) {
    Set<String> set = new TreeSet<>();
    for (String s : list) {
        set.add(s.toUpperCase());
    }
    return List.copyOf(set);
}

public static void main(String[] args) {
    List<String> list = Arrays.asList("rockman","kirby","link","jelda","mario","sonic","koopa");
    List<String> result = solution(list);
    System.out.println(result);
}

이 시점에서 인지했다. 난 거의 선언형 프로그래밍을 하고있었다.
메서드 구현부는 리스트를 어떻게 정렬하고 대문자로 바꿀 것인지를 표현하고 있다.

그럼 이번엔 람다로 한번 바꿔보자.

    public static void main(String[] args) {
        List<String> list = Arrays.asList("rockman","kirby","link","jelda","mario","sonic","koopa");
        List<String> result = list.stream().sorted().map(
                s -> s.toUpperCase()).collect(Collectors.toList());
        System.out.println(result);
    }

다시 한번 느끼지만 메서드 구현부가 없는 건 코드 크기가 굉장히 줄어든다. (물론 단 한번만 사용될 때의 얘기이다.)
코드는 스트림을 통해서 무엇을(sort / map) 할것인가를 표현한다.

느낌은 대충 알았으니 함수형 프로그래밍에 대해서 좀 더 자세히 정리해 놓자.

함수형 프로그래밍의 전제 조건

1급 객체(First object, First class citizen)
일반적으로 다음과 같은 조건을 만족하는 객체를 말한다.
자바에서는 함수형 인터페이스를 통해 구현이 가능하다.

  • 변수나 데이터 구조안에 넣을 수 있다.
  • 파라미터로 전달 할 수 있다.
  • 동적으로 프로퍼티 할당이 가능하다.
  • 리턴값으로 사용할 수 있다.
  • 할당에 사용된 이름과 관계없이 고유한 구별이 가능하다.

순수 함수(pure function)
순수 함수는 멀티쓰레드에서도 안전하고 병렬처리 및 계산도 가능하다.

  • 동일한 입력에 대해 항상 같은 값을 반환한다.
  • 함수의 실행은 프로그램의 실행에 영향을 미치지 않아야 한다. (ex 함수에서 인자를 변경하거나 프로그램의 상태를 변경하지 않음.)

고차함수(High Order Function)

  • 함수의 인자로 함수를 전달할 수 있다.
  • 함수의 리턴값으로 함수를 사용할 수 있다.

익명 함수(Anonymous function)
이름이 없는 함수를 말하며 람다식으로 표현되는 함수 구현을 말한다.

합성 함수(function composition)
합성 함수는 새로운 함수를 만들거나 계산하기 위해서 둘 이상의 함수를 조합하는 과정을 말한다.
실제 구현에서는 메서드 체이닝 방식으로 나타난다.

정리해보면 자바에서는 함수형 프로그래밍을 하고싶은 사람들을 위해서 람다식을 내놓은 것 같다. 람다식은 함수형 프로그래밍의 조건을 만족하고 있다.
함수형 프로그래밍의 장점으로 안정성이 대표적인 것 같다. 함수 밖에 있는 데이터는 변경할 수 없기 때문에 사이드 이펙트가 적다.

자바 스트림

자바 함수형 프로그램에서 람다를 사용하여 단계적으로 정의된 계산을 처리하기 위한 인터페이스.
자바 I/O 스트림도 있는데 그거 아님.

나는 아까의 예시에서도 Stream API를 사용했었다.

        List<String> result = list.stream().sorted().map(s -> s.toUpperCase()).collect(Collectors.toList());

스트림은 그 명칭 그대로 흐름이다. 파이프-필터 기반 API 이며 데이터 연산의 흐름을 표현한다.

컬렉션 vs 스트림
솔직히 아직은 왜 컬렉션과 스트림을 비교하는지는 모르겠지만 많은 기술 자료에 이 둘을 비교해서 한번 비교해본다.

  • 컬렉션은 데이터 소스에 해당(저장)하고 스트림은 자료구조를 다루는 방법(연산)을 제공하는 것이다.
  • 컬렉션은 외부 반복, 스트림은 내부 반복을 사용한다.
    외부 반복 : 반복을 명시적으로 수행한다. 가독성이 떨어질 수 있다.
    내부 반복 : 반복이 외부로 노출되지 않고 내부적으로 수행된다. 즉, 반복 구조가 캡슐화된다.
  • 컬렉션은 재사용이 가능하고 스트림은 재사용할 수 없다.
  • 스트림은 원본 데이터를 바꾸지 않고 새로운 스트림을 반환한다.
  • 스트림은 가능한 지연(Lazy)처리를 기본으로 한다.
    지연 처리란 결과가 필요하기 전에는 실행되지 않음을 의미한다. 즉, 최종연산에서 모든 중간 연산들이 처리된다. (성능 최적화)
  • 병렬 처리를 쉽게 할 수 있고 컬렉션 내부에서 처리되므로 많은 데이터 처리시 성능 향상의 효과가 있다.

어제 람다를 공부했을때 나왔던 것 처럼 스트림 연산자의 대부분은 인수로 함수형 인터페이스를 받는다. (Function, Consumer, Supplier, ...)
그렇기 때문에 스트림 연산자에 람다를 사용할 수 있다.

스트림의 생성
스트림은 주로 리스트로부터 생성하지만 그 외에도 여러 부분에서 생성되고 사용된다.

//리스트
List<Integer> list = Arrays.asList(324,26,241,534,4,35,64,85,537,42,746,14,376,5,547,34,88,14);
List<Integer> result1 = list.stream().filter(i -> i<100).collect(Collectors.toList());
System.out.println(result1);

//배열
int[] arr = new int[]{324, 26, 241, 534, 4, 35, 64, 85, 537, 42, 746, 14, 376, 5, 547, 34, 88, 14};
int result2 = Arrays.stream(arr).filter(i -> i<100).sum();
System.out.println(result2);

//Directly from values
Stream<Integer> stream = Stream.of(324, 26, 241, 534, 4, 35, 64, 85, 537, 42, 746, 14, 376, 5, 547, 34, 88, 14);
List<Integer> result3 = stream.filter(i -> i<100).collect(Collectors.toList());
System.out.println(result3);

//Stream ranges
IntStream intStream = IntStream.rangeClosed(0,10);
System.out.println(intStream.sum());

//그 외 Generators, Resources

중개 연산자
map(), filter(), 등은 중개 연산자로 스트림을 받아서 새로운 스트림을 반환한다.
지연 연산 처리로 최종 연산자 실행 전에는 연산을 처리하지 않는다.

최종 연산자
collect(), sum(), average() 등은 최종 연산자로 스트림을 사용하여 결과값을 반환한다.
여기서 모든 연산이 처리되고 더이상 스트림을 사용할 수 없다.

자주 사용되는 스트림 연산자들

filter(Predicate) : 각 요소마다 조건을 확인하여 통과한 요소만으로 새로운 스트림을 생성하는 중개 연산자.

// 100 미만의 요소로만 새로운 스트림 만들기
stream.filter(i -> i<100)

forEach() : 각 요소마다 특정 처리를 하는 최종 연산자.

// 짝수만 출력하기.
stream.filter(i -> i%2==0).forEach(i -> System.out.print(i));

map() : 스트림의 각 요소를 변환하고 새로운 스트림을 생성하는 중개 연산자.

// 10 미만의 요소에 100을 추가한 새로운 스트림 만들기
stream.filter(i -> i<10).map(i -> i+100)

sum() : 모든 요소를 합친 값을 반환하는 최종 연산자. (유사하게 사용할 수 있는 average(), min(), max(), summaryStatistics())

// 100 미만의 요소만 합치기.
stream.filter(i -> i<100).sum();

reduce() : 두 개의 요소를 결합 또는 계산하여 새로운 요소로 만들고 다시 이 연산을 수행하며 이를 마지막 요소까지 반복하는 최종 연산자.

// 1부터 100까지의 수 합치기.
stream.rangeClosed(1,100).reduce((x, y) -> x+y);

parallel() : 병렬 처리하기.

// 1부터 100까지의 수 합치기, 병렬로 처리.
stream.rangeClosed(0,100).parallel().reduce((x, y) -> x+y);

toArray() : 배열로 집계하는 최종 연산자.

stream.filter(i -> i % 2 == 0).toArray(Integer[]::new);

collect() : Collection/Map 으로 집계하는 최종 연산자.

stream.filter(i -> i % 2 == 0).collect(Collectors.toList());
stream.filter(i -> i % 2 == 0).collect(Collectors.toSet());
stream.filter(i -> i % 2 == 0).collect(Collectors.toCollection(TreeSet::new));
stream.filter(i -> i % 2 == 0).collect(Collectors.toList());

sort(), distinct(), 등 여러 중개연산자와
findFirst(), findAny(), count(), 등 여러 최종 연산자가 더 있다.

이번주 마무리

배운것
인증 방식(JWT, 세션/쿠키), mongodb, jinja2, SSR/CSR, 자바 람다식, 자바 스트림, 함수형 프로그래밍

느낀것
한 주가 저------엉말 길게 느껴졌다. 매일 6시반에 일어나서 사무실로 이동하고 10시 ~11시에 퇴근하는걸 반복하니까 하루가 매우 길어졌다. 항해99를 시작하면서 생활 규칙이나 공부 방법을 바꿔보고 싶었기 때문에 이런 점은 매우 만족중이다.
TIL을 작성하는것이 점점 익숙해진다. 항해 시작 전에는 책읽고 내용만 정리했었는데, 공부하면서 내 생각같은걸 적으니까 훨씬 재미있고 결과도 만족스럽다.

내게 아쉬웠던 것
피로감이 있다. 아직 잇몸 수술 받고 통증이 좀 있어서 더한거 같기도 하다. 예전부터 체력적으로 약함이 느껴지긴했는데, 운동을 하거나 영양제를먹거나 해야할 듯 하다.
주말엔 친구들이 모여서 술마시는데 솔직히 너무 부러웠다. 부러워서 공부에 집중도 잘 못했다. 카톡을 삭제해야할까.
스트레스로 금연에 실패했다. 그냥 피면서 하련다.
자기주도적인 학습이 아직 익숙치 못한 것 같다. 금요일, 토요일은 개인 공부할 시간이 많았는데 그렇게 잘 집중하지 못했다.

좋은 개발자
일주일마다 좋은 개발자란 어떤 사람인지 하나씩 생각해보기

  • 노력하는 사람
    잘 모르더라도 먼저 해보려고 하고 아무것도 안하고있는걸 싫어하는 사람은 동료로서 매력이 있다는 것을 많이 느꼈다.
  • 소통하는 사람
    질문도 좋고 사적인 얘기도 좋고 그냥 농담도 좋고 뭐든지 대화하려고 하는 사람은 협업할 때 좋은 것 같다.
    서로에 대해서 더 이해할 수 있는 것은 많은 도움이 된다.
  • 책임감 있는 사람
    맡은 일에 책임을 다하려고 노력하는 사람은 멋지다. 자신이 일을 못하는 상황이 되었을 때마저 인수인계를 대충하지 않는 사람은 멋질 것 같다.
Comments