자바와 함수형 프로그래밍

햄햄·2024년 4월 24일
0
post-custom-banner

왜 함수형 프로그래밍인가?

함수형 프로그래밍에서는 작은 단위의 순수 함수를 작성하고 이를 조합하여 프로그램을 만든다. 각 함수는 하나의 목적을 가지고 있어 이해하기 쉽고 전체 코드의 가독성을 높인다. 또한 순수 함수는 같은 입력에 대해 항상 같은 출력을 반환하며 side effect를 일으키지 않는다. 즉 객체의 불변을 보장한다. 이런 특성 때문에 순수 함수는 예측 가능하고, 테스트하기 쉽다. 이는 버그를 줄이고, 안정적인 코드를 작성하는 데 도움을 준다.

Stream API와 파이프라인

Java 8에서 도입된 Stream API는 함수형 프로그래밍을 지원하는 중요한 기능 중 하나이다. Stream API는 데이터를 파이프라인 구조로 연산할 수 있도록 도와준다. 파이프라인은 여러 단계의 연산을 순차적으로 처리하는 것을 말하는데, 각 단계는 이전 단계의 출력을 입력으로 받아 처리하고, 다음 단계로 결과를 전달한다. 각 단계를 독립적으로 작성할 수 있으므로 문제를 분할정복 하는 데에 도움을 주며 코드의 가독성을 크게 향상시킨다.

Stream API에서 제공하는 map, filter, reduce와 같은 메소드는 고차함수이자 순수함수이다. map, filter, reduce를 직접 구현해보면 다음과 같다

public <T, R> List<R> map(final List<T> list, final Function<T, R> mapper) {
    final List<R> result = new ArrayList<>();
    for (final T item : list) {
        result.add(mapper.apply(item));
    }
    return result;
}

public <T> List<T> filter(final List<T> list, final Predicate<T> predicate) {
    final List<T> result = new ArrayList<>();
    for (final T item : list) {
        if (predicate.test(item)) result.add(item);
    }
    return result;
}

public <T> T reduce(final List<T> list, final T identity, final BinaryOperator<T> accumulator) {
    T result = identity;
    for (final T item : list) {
        result = accumulator.apply(result, item);
    }
    return result;
}}

주목해야할 점은 인자로 받은 데이터를 변경하지 않고, 새로운 변수를 선언하여 반환한다는 것이다. 이 덕분에 신뢰성 있는 어플리케이션을 만들 수 있다.

그런데 Stream API를 이렇게 쓰면 어떨까?

final List<Name> names = users.stream()
	.map(user -> {
    	user.changeFirstName("햄");
        return new Name(user.getFirstName(), user.getLastName());
    })
    .toList();

Stream API를 사용했어도 인자로 전달한 람다 함수에서 객체를 변경했기 때문에 함수형 프로그래밍의 장점을 퇴색시키게 된다. map, filter, reduce 등은 Java뿐만 아니라 함수형 프로그래밍이 녹아든 다른 언어에서도 많이 볼 수 있는 API이다. 함수형 프로그래밍이 친숙한 개발자는 map, filter, reduce를 마주치면 side effect가 없을 것이라고 기대할 것이기 때문에 객체를 변경시키는 동작을 본다면 당황할 수도 있다. 개인적으로 side effect를 발생시키는 동작이 포함된다면 코드가 덜 간결하더라도 forEach문이나 for문을 사용하는 것이 좋다고 생각한다.

Optional과 모나드

Java 8에서 도입된 Optional은 함수형 프로그래밍 사용되는 개념인 모나드와 관련이 있다. 모나드는 nondeterministic한 요소를 deterministic하게 다룰 수 있게 만들어주는 추상 타입이다. 모나드를 활용하면 타입이 보장되는 안전한 프로그래밍을 할 수 있다. 왜 Optional이 모나드인지 더 쉽게 알기 위해 다음 코드를 보자.

final User user = userRepository.findByName("햄");
user.changeName("햄햄");

Null Pointer Exception에 많이 당해본 개발자라면 위와 같은 코드를 봤을 때 불안한 기분부터 들 것이다. user가 null일 수도 있기 때문이다. 하지만 코드는 완벽하게 컴파일되며 위험성에 대한 어떠한 타입 힌트도 얻을 수 없다. 하지만 Optional의 도움을 받는다면 어떨까?

final Optional<User> user = userRepository.findByName("햄“);
user.ifPresend(user -> user.changeName("햄햄"));

user가 null이 아닐 때만 연산하여 절대 Null Pointer Exception이 일어나지 않는다는 것을 보장할 수 있게 된다. 이처럼 모나드는 타입을 통해 버그로부터 안전한 앱을 만드는 데 도움을 준다. 모나드는 검색해도 이해할 수 없는 설명들만 나오는 어려운 개념인데, 모나드를 잘 설명한 유명한 유튜브 영상을 참고하면 좀 더 도움이 될 것이다.

profile
@Ktown4u 개발자
post-custom-banner

0개의 댓글