스위프트를 공부하다 보니 함수형 프로그래밍 이야기가 많이 나온다. 항상 함수형 프로그래밍을 쓰는데 애를 먹고 있는데 이번 기회에 함수형 프로그래밍을 자세히 알아보려고 한다. 일단 함수형 프로그래밍은 아래와 같은 특징이 있다.
불변성 (Immutability): 함수형 프로그래밍은 데이터의 불변성을 강조한다. 이는 데이터가 한 번 생성되면 변경되지 않는다는 것을 의미한다. 불변성은 코드의 복잡성을 줄이고, 사이드 이펙트(side effects)를 방지하며, 프로그램의 예측 가능성을 향상시킨다.
함수의 일급 객체 (First-Class Functions): 함수형 프로그래밍에서 함수는 "일급 객체"이다. 이는 함수를 변수에 할당하거나, 다른 함수의 인자로 전달하거나, 함수에서 함수를 반환할 수 있음을 의미한다. 이러한 특성으로 인해 고차 함수를 사용할 수 있게 된다.
고차 함수 (Higher-Order Functions): 고차 함수는 다른 함수를 인자로 받거나 함수를 결과로 반환하는 함수이다. 이들은 코드의 재사용성을 높이고, 추상화 수준을 끌어올려 더 간결하고 읽기 쉬운 코드를 작성할 수 있게 한다.
사이드 이펙트 감소: 함수형 프로그래밍은 순수 함수(pure functions) 개념을 사용한다. 순수 함수는 같은 입력에 대해 항상 같은 출력을 반환하며, 외부 상태를 변경하지 않는다. 이는 프로그램의 동작을 이해하고 예측하기 쉽게 만든다.
모듈성 (Modularity): 함수형 코드는 일반적으로 더 모듈화되어 있다. 작은 함수로 분리되어 있기 때문에 각 함수를 독립적으로 테스트하고 재사용하기가 쉽다.
병렬 처리 (Parallelism): 불변성과 순수 함수는 병렬 코드를 작성할 때 발생할 수 있는 문제를 줄여준다. 공유 상태를 변경하지 않기 때문에 데이터 경쟁(data race)이 발생할 가능성이 적어진다.
더 나은 추상화: 고차 함수를 사용하는 함수형 프로그래밍은 개발자가 보다 높은 수준의 추상화에 집중할 수 있게 해준다. 예를 들어, map
, filter
, reduce
같은 함수들은 데이터를 처리하는 복잡한 로직을 간단한 함수 호출로 대체할 수 있게 해준다.
물론, 함수형 프로그래밍이 모든 상황에 적합한 것은 아니다. 어떤 경우에는 전통적인 명령형(imperative) 스타일이 더 효율적이거나 읽기 쉬울 수 있다. 그러나 많은 경우 함수형 프로그래밍이 코드의 간결성, 안정성, 유지보수성을 높여준다.
함수형 프로그래밍의 접근 방식을 이해하고 적절히 활용하면, 시간이 지남에 따라 프로그램의 복잡성을 관리하고, 오류를 줄이며, 개발 프로세스를 개선하는 데 큰 도움이 될 수 있다.
변수에 할당될 수 있다: 변수에 할당하고 다른 변수처럼 조작할 수 있다.
다른 함수의 인자로 전달될 수 있다: 다른 함수에 인자로 넘길 수 있습니다. 이는 고차 함수를 가능하게 합니다.
다른 함수의 결과로 반환될 수 있다: 다른 함수의 반환값으로 사용될 수 있습니다.
익명으로 생성될 수 있다: 익명 함수(또는 람다)를 사용하여 이름이 없는 함수를 만들 수 있다.
스칼라에서 모든 것은 객체라는 객체 지향 프로그래밍의 원칙을 유지하면서도, 함수가 일급 객체라는 함수형 프로그래밍의 특성도 갖추고 있다. 이렇게 함수와 객체를 모두 일급 시민으로 취급하는 스칼라의 접근 방식은 개발자가 두 패러다임의 장점을 결합하여 유연하고 강력한 코드를 작성할 수 있게 한다.
Java에서 함수가 일급 객체(first-class citizen)로 취급되지는 않는다. 일급 객체가 되기 위한 조건 중 하나는 변수에 저장할 수 있어야 하며, 다른 객체들에게 인자로 넘겨줄 수 있고, 함수의 결과로서 반환될 수 있어야 한다는 것인데, Java에서 메소드는 이러한 조건을 만족시키지 못한다.
그러나 Java 8 이후로 Java에 람다 표현식(lambda expressions)이 도입되어 이전보다 함수적인 스타일을 지원하기 시작했다. 람다 표현식 덕분에 메소드를 일급 객체처럼 다룰 수 있는 기능이 추가되었다. 이것은 '함수형 인터페이스'라는 개념을 통해 가능해졌다. 함수형 인터페이스는 오직 하나의 추상 메소드를 가진 인터페이스이며, 람다 표현식을 이용하여 이 인터페이스의 구현체를 간결하게 제공할 수 있다.
예를 들어, Java의 Runnable
인터페이스는 다음과 같이 람다 표현식을 사용하여 인스턴스화할 수 있다:
Runnable run = () -> System.out.println("Hello, World!");
이 람다 표현식은 Runnable
인터페이스의 run
메소드를 구현한 것이다. 여기서 람다 표현식을 변수에 할당했고, 이 변수를 다른 메소드에 인자로 넘기거나 메소드에서 반환값으로 사용할 수 있다. 이런 방식으로 Java에서도 함수적인 프로그래밍 스타일을 구사할 수 있게 되었다.
그러나 Java에서 이런 식의 함수적 프로그래밍은 여전히 객체 지향 프로그래밍의 틀 안에서 이루어진다. 람다 표현식이나 메소드 참조(method references)는 내부적으로는 익명 클래스의 인스턴스로 표현된다. 따라서 Java에서의 "함수"는 여전히 객체로 취급되며, 이는 Java가 객체 지향 프로그래밍 언어라는 사실을 반영한다.
자바에서 함수를 일급 객체로 만들지 않은 결정은 초기 언어의 설계 철학과 주된 프로그래밍 패러다임을 반영하는 것이다. 이 결정은 프로그래밍 언어가 대면하는 다양한 요구사항과 목표들 사이의 균형을 맞추려는 결과라고 볼 수 있다. 다음은 이러한 결정의 주된 이유들이다:
언어의 철학: 자바는 객체 지향 프로그래밍 언어로서 설계되었으며, 모든 동작은 객체의 메서드를 통해 수행되어야 한다는 철학을 가지고 있었다. 따라서, 초기 자바에서는 메서드를 독립된 엔티티로 취급하는 것이 아니라, 객체의 일부로만 취급했다.
단순성과 접근성: 자바는 단순성과 쉬운 접근성을 중요한 가치로 여겼다. 복잡한 기능보다는 명확하고 이해하기 쉬운 프로그래밍 모델을 제공하는 것을 목표로 했다.
백워드 호환성: 자바는 기존 코드와의 호환성을 유지하려는 강한 의지를 가지고 있다. 메서드를 일급 객체로 만드는 것은 기존의 코드베이스와의 호환성 문제를 일으킬 수 있었기 때문에, 신중하게 접근해야 했다.
시기와 트렌드: 자바가 설계되었을 때의 프로그래밍 트렌드는 객체 지향에 더 많은 초점을 맞췄으며, 함수형 프로그래밍은 현대에 와서야 더 많은 관심을 받기 시작했다.
자바 8 이후로 자바에서도 람다 표현식과 메서드 참조 등을 통해 함수형 프로그래밍을 어느 정도 지원하기 시작했다. 이것은 프로그래밍 커뮤니티 내에서 함수형 프로그래밍에 대한 관심과 요구가 증가했기 때문이다. 따라서, 자바의 진화는 기술적 가능성, 프로그래밍 패러다임의 변화, 그리고 커뮤니티의 요구가 상호 작용하는 과정 속에서 이루어졌다고 볼 수 있다.
자바의 초기 설계 철학은 강력한 객체 지향 프로그래밍을 중심으로 하고 있었다. 이는 모든 것이 객체이며, 모든 작업이 메서드 호출을 통해 수행되어야 한다는 개념을 기반으로 한다. 이 철학 아래에서는 함수를 독립된 값으로 취급하는 것이 아니라, 언제나 어떤 객체의 메서드로 존재해야 했다.
하지만 프로그래밍 언어들은 시간이 지남에 따라 변화하고 새로운 패러다임을 수용하게 된다. 자바도 예외는 아니다. 자바 8에서 람다 표현식을 도입하면서 함수형 프로그래밍 개념을 일부 수용했지만, 여전히 강력한 객체 지향 철학 내에서 작동하도록 설계되었다. 람다 표현식은 메서드를 더 유연하게 사용할 수 있게 하지만, 여전히 자바는 모든 함수를 객체 안에 캡슐화해야 하는 제약을 가지고 있다.
따라서 일급 객체로서의 함수를 완전히 통합하기보다는, 자바는 객체 지향 철학을 유지하면서도 현대적인 프로그래밍 요구사항에 부응하려는 방향으로 진화하고 있다. 이는 기존의 자바 철학을 뒤집기보다는 확장하는 형태로 진행되었으며, 이런 타협을 통해 자바는 백워드 호환성을 유지하면서도 새로운 프로그래밍 스타일을 수용할 수 있게 되었다.
자바에서 함수형 프로그래밍 요소가 도입된 것은 주로 자바 8부터이며, 여기에는 람다 표현식과 메소드 참조가 포함된다. 그러나 이러한 기능들이 추가되었음에도 자바는 여전히 그 핵심에서 객체 지향 프로그래밍 언어이다. 이것이 무엇을 의미하는지 구체적으로 살펴보자.
람다 표현식은 기본적으로 함수형 인터페이스의 구현을 간결하게 표현하는 방법이다. 함수형 인터페이스는 단 하나의 추상 메소드를 가진 인터페이스이다. 예를 들어, Runnable
이나 Callable
처럼 말이다. 자바에서는 람다 표현식을 사용하여 이러한 인터페이스의 구현체를 제공할 수 있다.
자바에서 람다 표현식이 실행되는 방식은 내부적으로 익명 클래스를 사용하는 것과 유사하다. 예를 들어, 다음과 같은 람다 표현식이 있다고 가정해보자:
Runnable run = () -> System.out.println("Hello, Lambda!");
자바 컴파일러는 이 람다 표현식을 Runnable
인터페이스를 구현하는 익명 클래스의 인스턴스로 변환한다. 이 과정에서 run
참조 변수는 실제로는 Runnable
인터페이스를 구현한 어떤 객체를 가리키게 된다.
이러한 변환은 람다 표현식이 자바의 객체 지향 모델에 맞추어 적용되도록 한다. 즉, 람다 표현식 자체는 함수적 스타일을 제공하지만, 실제로는 객체 지향 프로그래밍 언어의 특징을 따르는 객체로 표현되는 것이다.
메소드 참조 역시 람다 표현식과 유사하게 동작한다. 메소드 참조는 특정 메소드를 직접 참조하여 람다 표현식의 간결성을 더욱 높이는 방법이다. 예를 들어, 다음과 같은 메소드 참조가 있다고 가정해 보자:
Consumer<String> printer = System.out::println;
이 코드에서 System.out::println
은 println
메소드를 직접 참조하는 메소드 참조이다. 이것은 자바가 메소드 참조를 내부적으로 람다 표현식과 같은 방식으로 처리한다는 것을 의미한다. 즉, Consumer<String>
인터페이스를 구현하는 익명 클래스의 인스턴스로 변환된다.
결국 자바에서 함수형 프로그래밍 요소들은 객체 지향 프로그래밍의 틀 안에서 작동하게 된다. 람다 표현식이나 메소드 참조가 사용될 때, 자바의 객체 지향 원칙에 따라 이들은 내부적으로는 익명 클래스의 인스턴스로 취급되어 실행된다. 이는 자바가 여전히 객체 지향 언어로서의 특성을 유지하면서도, 개발자에게 보다 표현력 있는 문법과 함수형 프로그래밍의 이점을 제공하기 위한 설계 결정이다