자바에서 함수형 프로그래밍 사용하기

juhyeon_k·2025년 1월 28일

자바

목록 보기
1/1

자바를 쓰는 프로그래머로서
자바8 부터는 함수형 프로그래밍을 일부를 지원한다고 해서 해당 기능으로 함수형 프로그래밍을 공부한다고 했을 때 모두들 말렸다 그래서
초기 함수형 프로그래밍 언어인 LISP 와 현대적인 함수형프로그래밍언어인 Scala의 함수형 프로그래밍을 공부하고 다시 자바의 관련 함수형을 보았을 때의 그 의미와 개인적인 생각을 정리한다.

1급 함수를 대체하기 위한 1급 객체를 위한 함수형 인터페이스

  • Java는 함수를 직접적인 1급 객체로 다룰 수 없어서, 이를 대신하기 위해 함수형 인터페이스(@FunctionalInterface)를 도입함 (자바는 기본적으로 함수를 매개변수로 던질 수 없기때문에 하나의 익명클래스의 하나의 함수를 가진 클래스를 함수를 던지는 것으로 취급함)
// 자바의 함수형 인터페이스 방식
Function<Integer, Integer> multiply = x -> x * 2;
List<Integer> numbers = Arrays.asList(1, 2, 3);
numbers.stream().map(multiply);

// Scala의 1급 함수 방식
val multiply: Int => Int = _ * 2
List(1, 2, 3).map(multiply)
  • Function, Consumer, Supplier, Predicate 등의 인터페이스를 통해 함수를 객체처럼 다룰 수 있게 했지만, 이는 실제 함수형 언어의 1급 함수와는 다른 제한적인 방식이다

  • 추가적으로 정의는 할 수 있겠지만 그 이상 사용하는 것은 오히려 코드의 복잡성만 더하게 되고 그런 상황이면 그냥 객체지향적으로 해결하는 것이 좋은상황이 많을 것이다

불변성을 대체하기 위한 레코드

  • Java 16에서 도입된 Record는 불변 데이터 구조를 쉽게 만들 수 있게 해주는 기능
// Java Record
public record Person(String name, int age) {}

// 일반 클래스에서 불변성 구현 - 더 많은 코드 필요
public class Person {
    private final String name;
    private final int age;
    
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    // getter만 제공
    public String getName() { return name; }
    public int getAge() { return age; }
}

// Scala case class - 기본적으로 불변
case class Person(name: String, age: Int)
  • 하지만 이는 함수형 언어들이 기본적으로 제공하는 불변성과는 다르게, 명시적으로 Record를 사용해야만 하는 제한이 있다
  • 일반 클래스에서는 여전히 가변성이 기본이며, 불변성을 위해서는 추가적인 노력이 필요함

최적화되지 않는 재귀함수

// Java에서 StackOverflowError 발생 가능한 재귀
public static long factorial(long n) {
    if (n <= 1) return 1;
    return n * factorial(n - 1);
}

// 반복문으로 우회해야 함
public static long factorial(long n) {
    long result = 1;
    for (long i = n; i > 0; i--) {
        result *= i;
    }
    return result;
}

// Scala의 꼬리 재귀 최적화
@tailrec
def factorial(n: Long, acc: Long = 1): Long = {
    if (n <= 1) acc
    else factorial(n - 1, n * acc)
}
  • Java는 꼬리 재귀 최적화(Tail Call Optimization)를 지원하지 않는다.(Project Loom에서 되감기 호출(Rewind Calls) 스택프레임을 저장하고 복원하는기능 지원한다고 했으나 구현되지않음 )
  • 이로 인해 깊은 재귀 호출은 StackOverflowError를 발생시킬 수 있다
  • 트리 순회 같은 대안적 방법을 사용할 수 있지만, 이는 함수형 프로그래밍의 자연스러운 재귀적 접근과는 거리가 있다 (굳이 이렇게 해야 되나라는 느낌이 강함)

Stream과 Optional의 제한적 구현

  • Java의 Stream과 Optional은 특정 사용 사례에 맞춰진 제한적인 구현체이다
  • 새로운 모나드나 타입을 정의하고 확장하기가 어려움
  • 함수형 언어들이 제공하는 것과 같은 일반적인 타입 클래스나 고차 추상화를 구현하기 어렵다
  • 기존의 함수형에서 사용하던 모나드 구현체와 동일하게 사용하기에는 법칙을 만족하지 않음과 한계가 존재함
// Optional의 모나드 법칙 위반 예시
Optional<String> opt = Optional.of("test");

// 모나드 법칙 중 왼쪽 항등 법칙:
// Optional.of(x).flatMap(f) 는 f(x)와 같아야 함 

Function<String, Optional<String>> f = s -> null; // null을 반환하는 함수

// 왼쪽 연산
try {
    Optional<String> left = opt.flatMap(f);
    System.out.println("Left: " + left);
} catch (NullPointerException e) {
    System.out.println("Left: NPE 발생");
}

// 오른쪽 연산
try {
    Optional<String> right = f.apply(opt.get());
    System.out.println("Right: " + right);
} catch (NullPointerException e) {
    System.out.println("Right: NPE 발생");
}

// Stream의 제한적 지연 평가
Stream<Integer> numbers = Stream.of(1, 2, 3)
    .map(x -> {
        System.out.println("mapping " + x); // 중간 연산이 즉시 실행되지 않음
        return x * 2;
    });
// 하지만 재사용 불가능
numbers.forEach(System.out::println);
numbers.forEach(System.out::println); // IllegalStateException

// Scala의 LazyList는 진정한 지연 평가와 재사용 가능
val numbers = LazyList(1, 2, 3)
  .map(x => {
    println(s"mapping $x")
    x * 2
  })
numbers.foreach(println) // 재사용 가능
numbers.foreach(println) // 다시 사용 가능

분석 및 감상

실제 함수형언어의 함수형의 접근방식과 자바의함수형을 보고 느낀점은 실제 함수형프로그래밍의 모나드의 구현체중에 유용한 대표적인 구현체만의 성질을 흉내내서 만들었다고 느껴졌다

Optional

  • Maybe 모나드의 일부 기능만 구현
  • 모나드 법칙을 완전히 따르지 않음
  • 기본적인 연산만 제공

Stream

  • 모나딕한 성질 일부만 구현
  • 일회성 제약
  • 제한적 지연 평가

이런 제한적인 성격 안에서도 함수적 프로그래밍이 같는 장점(간결성, 일관성, 정확성, 병렬성 동시성 다루기)
은 가져갈수 있고 적용하는데 적절할지는 판단이 필요하다
예를 들어 간단한 반복문을 억지로 바꿀필요는 없다던가 동시성 처리를할때 이걸 써서 더 효율적인 처리 데이터 양인가를 가늠해야 한다.
굳이 억지로 바꿀필요는 없다
자바 자체에 없던 형식을 함수형 언어에서는 불변성, 일급함수등을 바로 지원하기 때문에 더 강력하고 추가적인 코드 필요 작성 없이 가능하다
자바에서 만약에 추가적인 조합기를 추가하거나,합성관련을 구현하라고하면 복잡해진다
오히려 코드를 작성하는 수고가 더 들고 코드가 더 알아보기 어렵게 된다
자바에서 만약에 함수형을 사용한다고 하면 기존에 지원하는 기본적 조합기 이외에 무언가를 해야한다고하면 지양하는 것이 좋은 것 같다
현재 상황에서는 실제 자바에서 함수형을 사용하는 이것을 적용하는 경우 이득이 있는가를 판단하는 능력이 중요할 것 같다

실용적 고려사항

  • 장점: 간결성, 일관성, 정확성, 병렬 처리
  • 적용 판단 기준:
    1. 단순 반복문의 경우 기존 방식 유지
    2. 병렬 처리 시 데이터 크기 고려
    3. 기본 제공 기능 외 확장 필요성 검토

결론 (주의 비유를 사용하여 정보가 굉장히 손실되어 있음)

"함수형언어의 함수형 프로그래밍이 새라면 자바의 함수형은 하늘다람쥐이다"

태생이 날기 위해 태어난 언어와 그렇지 않은데 조금 날수도 있도록 진화된 상황이라 다르다
접근방식을 다르게 보아 이런 함수형언어의 일부 장점을 취한 정도이기 때문에 적절하게 써야될 상황에 대한 판단이 심히 요구된다!

3줄 요약

  • 태생적 차이를 인정하고 적절한 사용 범위 설정 필요
  • 무리한 확장보다는 제공되는 기능 내에서 활용
  • 상황에 따른 적절한 판단이 핵심
profile
각종 개념들을 자기화하기 위해 정리하는 블로그

0개의 댓글