2022.02.22
자바 8 언어 설계자들은 언어에 고계함수를 그냥 덧붙이지 않고, 교묘하게 기존의 인터페이스들이 함수형 기능을 사용할 수 있도록 만들었다.
public class Process {
public string cleanNames(List<String> names) {
if (names == null) return "";
return names
.stream()
.filter(name -> name.length() > 1)
.map(name -> capitalize(name))
.collect(Collectors.joining(","));
}
}
자바 8의 스트림을 사용하면, collect()
나 forEach()
처럼 출력을 발생하는 함수(종결 작업terminal operation이라고 부른다)를 호출할 때까지
다른 함수들을 연결해서 합성할 수 있다.
자바는 이미 언어가 가지고 있는 클래스와 컬렉션에 맵과 리듀스와 같은 함수형 구조를 더하여, 컬렉션을 효율적으로 업데이트하는 문제를 처리한다.
스칼라나 클로저의 대부분의 컬렉션은 불변형이기 때문에 런타임이 효율적으로 조작할 수 있다. 자바8에는 ArrayList
나 StringBuilder
를 위해
매번 새로운 결과를 내지 않고 기존의 요소를 업데이트하는, 가변 리듀스 작업을 하는 메서드가 포함되게 되었다.
Runnable
이나 Callable
같이 메서드를 하나만 가지는 인터페이스는 자바에서 흔하게 볼 수 있는 관용 표현이다.
이를 흔히 단일 추상 메서드(Single Abstract Method) 인터페이스라고 부른다. 많은 경우, SAM은 이동 가능한 코드를 운송하는 메커니즘으로 주로 사용한다.
함수형 인터페이스(Functional interface)라는 영리한 메커니즘은 람다와 SAM이 유용하게 상호작용할 수 있게 해준다. 함수형 인터페이스는 여러 유용한 상황에서 람다 블록이 자연스럽게 녹아들 수 있게 해준다. 자바의 관용 표현과 잘 어울리기 때문에 함수형 인터페이스는 괄목할 만한 혁신이라 하였다.
자바 8에서 min()
과 같은 내장 메서드는 값 대신 Optional
을 리턴한다. Optional
은 오류로서의 null
과 리턴 값으로서의 null
을 혼용하는 것을 방지한다.
자바 8의 종결 작업은 ifPresent()
메서드를 사용하여 제대로 된 리턴 값에먼 코드 블록을 실행하게끔 했다.
n.stream()
.min((x, y) -> x - y)
.ifPresent(z -> System.out.println("smallest is " + z));
자바 8의 스트림은 많은 함수형 기능을 가능하게 한다.
filter()
작업은 밑에 깔린 컬렉션을 바꾸지 않고 필터된 값의 스트림을 리턴한다.limit()
이나 findFirst()
같은 메서드를 사용하여 그 부분집합을 구할 수 있다.Iterator
인스턴스처럼 스트림은 사용과 동시에 소멸되고, 재사용 전에 다시 생성해야 한다.스트림 작업은 중간 작업 또는 종결 작업이다. 중간 작업은 새 스트림을 리턴하고 항상 게으르다. 예를 들어 스트림의 filter()
작업은 사실상 필터를 하지 않고, 종결 작업이 순회할 때 필터된 값만 리턴하는 스트림을 만드는 것이다.
종결 작업은 스트림을 순회하여 값이나 부수효과를 낳는다.
자바 언어 설계자들이 좋은 메커니즘을 만들어서 애플리케이션을 조금씩 함수형 구조로 바꾸기 쉽게 해준 덕분에, 익명 내부 클래스를 람다 블록으로 바꾸기는 쉽다. 반면에, 소프트웨어 아키텍처나 데이터를 다루는 근본적인 방법을 점진적으로 바꾸기는 훨씬 어렵다.
함수형 아키텍처는 불변성이 그 중심에 있고, 이를 최대한 사용하려 시도한다.
함수형 사고로의 전환의 이점은 코드에서 생기는 변화가 제대로 이루어졌는지 확인할 테스트가 있다는 사실을 인지하게 되는 것이다.
다시 말해 테스트의 진정한 목적은 변이(mutation)를 확인하는 것이고, 변이가 많을수록 테스트가 많이 필요하게 된다.
가변 상태와 테스트는 직접적인 상호 관계가 있다. 전자가 많으면 후자가 많게 된다.
변이를 엄격하게 제한해서 변이점들을 고립시키면 오류가 발생할 장소가 적어지고, 결국 테스트할 곳이 줄어든다.
불변 객체가 상태를 알 수 없거나 예외 때문에 잘못된 상태를 가질 가능성도 없다.
모든 초기화가 생성 시에 일어나기 때문에 어떠한 예외라도 객체가 생성되기 전에 발생한다.
이를 실패의 원자성(failure atomicity)이라고 부른다. 가변성에 의존하는 성패가 객체가 생성되는 시점에 이미 해결된다는 뜻이다.
CQRS는 그래그 영(Greg Young)이 개념을 도입했고, 마틴 파울러가 영향력 있게 그 개념을 기술했다.
전통적인 애플리케이션 아키텍처
CQRS 아키텍처
CQRS는 읽기와 명령 부분을 분리함으로써 아키텍처의 일부를 단순화한다. 개발자가 불변성을 가정할 수 있기 때문에 쿼리쪽의 논리는 훨씬 단순하다. 업데이트는 다른 경로를 통해서 적용된다.
아키텍처는 항상 트레이드 오프를 염두에 두어야 한다. CQRS로 인해 한 부분은 쉬워지지만 다른 부분은 복잡해진다. 예를 들어 한 덩어리인 데이터베이스를 사용하면 트랜잭션이 쉽다. CQRS를 사용하면 트랜잭션형보다는 최종 일관성 모델로 전환해야 할 것이다.
읽기를 변이로부터 분리하면 논리적으로 단순해진다. 읽기쪽에서는 모든 것을 불변형으로 처리할 수 있다.
웹 프로그래밍은 함수형 프로그래밍에 잘 어울린다. 웹 전반을 요구를 응답으로 바꾸는 일련의 변형으로 볼 수 있기 때문이다.
모든 함수형 언어는 다양한 웹 프레임워크를 가지고 있고, 이들의 공통적인 특성은 다음과 같다.
관계형 데이터베이스는 업데이트할 때마다, 예전 값은 없어지고 새 값으로 대체된다. 왜 그런식으로 설계 되었을까?
데이터가 증가하는 것을 억제하고 저장 장소를 극대화하기 위해서이다.
이런 아키텍처 결정이 수십 년 전의 데이터베이스 설계에 깊숙이 들어 있다.
하지만 이제 세상이 바뀌었고, 컴퓨팅 자원(특히 가상 자원)은 이제 가격이 낮다.
클로저 커뮤니티에서 상업적 NoSQL 데이터베이스에 처음으로 내놓은 데이토믹은 아키텍처 전반을 뒤집으면 언어 설계자들이 함수형 개념을
어디까지 밀고 나갈 수 있는지를 알 수 있는 예이기 때문에 흥미롭다.
데이토믹은 들어오는 모든 사실들에 시간을 붙여서 저장하는 불변형 데이터베이스이다.
데이터 대신 값을 저장함으로써, 저장소를 상당히 효율적으로 사용한다. 값이 한번 들어오면, 그 값의 다른 모든 인스턴스는 원본(불변형)을 가리키게 할 수 있으므로
저장 공간을 효율적으로 사용할 수 있는 것이다.
이러한 설계에서 몇 가지 흥미로운 결과가 나왔다.