7장, 실용적 사고

2022.02.22

7.1 자바 8

자바 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에는 ArrayListStringBuilder를 위해 매번 새로운 결과를 내지 않고 기존의 요소를 업데이트하는, 가변 리듀스 작업을 하는 메서드가 포함되게 되었다.

7.1.1 함수형 인터페이스

Runnable이나 Callable 같이 메서드를 하나만 가지는 인터페이스는 자바에서 흔하게 볼 수 있는 관용 표현이다.
이를 흔히 단일 추상 메서드(Single Abstract Method) 인터페이스라고 부른다. 많은 경우, SAM은 이동 가능한 코드를 운송하는 메커니즘으로 주로 사용한다.

함수형 인터페이스(Functional interface)라는 영리한 메커니즘은 람다와 SAM이 유용하게 상호작용할 수 있게 해준다. 함수형 인터페이스는 여러 유용한 상황에서 람다 블록이 자연스럽게 녹아들 수 있게 해준다. 자바의 관용 표현과 잘 어울리기 때문에 함수형 인터페이스는 괄목할 만한 혁신이라 하였다.

7.1.2 옵셔널

자바 8에서 min()과 같은 내장 메서드는 값 대신 Optional을 리턴한다. Optional은 오류로서의 null과 리턴 값으로서의 null을 혼용하는 것을 방지한다. 자바 8의 종결 작업은 ifPresent() 메서드를 사용하여 제대로 된 리턴 값에먼 코드 블록을 실행하게끔 했다.

n.stream()
  .min((x, y) -> x - y)
  .ifPresent(z -> System.out.println("smallest is " + z));

7.1.3 자바 8 스트림

자바 8의 스트림은 많은 함수형 기능을 가능하게 한다.

  • 스트림은 값을 저장하지 않으며, 종결 작업을 통해 입력에서 종착점까지 흐르는 파이프라인처럼 사용된다.
  • 스트림은 상태를 유지하지 않는 함수형으로 설계되었다. 일례로 filter() 작업은 밑에 깔린 컬렉션을 바꾸지 않고 필터된 값의 스트림을 리턴한다.
  • 스트림 작업은 최대한 게으르게 한다.
  • 무한한 스트림이 가능하다. 일례로 모든 정수를 리턴하는 스트림을 만들어 limit()이나 findFirst() 같은 메서드를 사용하여 그 부분집합을 구할 수 있다.
  • Iterator 인스턴스처럼 스트림은 사용과 동시에 소멸되고, 재사용 전에 다시 생성해야 한다.

스트림 작업은 중간 작업 또는 종결 작업이다. 중간 작업은 새 스트림을 리턴하고 항상 게으르다. 예를 들어 스트림의 filter() 작업은 사실상 필터를 하지 않고, 종결 작업이 순회할 때 필터된 값만 리턴하는 스트림을 만드는 것이다. 종결 작업은 스트림을 순회하여 값이나 부수효과를 낳는다.

7.2 함수형 인프라스트럭쳐

자바 언어 설계자들이 좋은 메커니즘을 만들어서 애플리케이션을 조금씩 함수형 구조로 바꾸기 쉽게 해준 덕분에, 익명 내부 클래스를 람다 블록으로 바꾸기는 쉽다. 반면에, 소프트웨어 아키텍처나 데이터를 다루는 근본적인 방법을 점진적으로 바꾸기는 훨씬 어렵다.

7.2.1 아키텍처

함수형 아키텍처는 불변성이 그 중심에 있고, 이를 최대한 사용하려 시도한다.
함수형 사고로의 전환의 이점은 코드에서 생기는 변화가 제대로 이루어졌는지 확인할 테스트가 있다는 사실을 인지하게 되는 것이다.
다시 말해 테스트의 진정한 목적은 변이(mutation)를 확인하는 것이고, 변이가 많을수록 테스트가 많이 필요하게 된다.

가변 상태와 테스트는 직접적인 상호 관계가 있다. 전자가 많으면 후자가 많게 된다.

변이를 엄격하게 제한해서 변이점들을 고립시키면 오류가 발생할 장소가 적어지고, 결국 테스트할 곳이 줄어든다.

불변 객체가 상태를 알 수 없거나 예외 때문에 잘못된 상태를 가질 가능성도 없다. 모든 초기화가 생성 시에 일어나기 때문에 어떠한 예외라도 객체가 생성되기 전에 발생한다.
이를 실패의 원자성(failure atomicity)이라고 부른다. 가변성에 의존하는 성패가 객체가 생성되는 시점에 이미 해결된다는 뜻이다.

CQRS

CQRS는 그래그 영(Greg Young)이 개념을 도입했고, 마틴 파울러가 영향력 있게 그 개념을 기술했다.

전통적인 애플리케이션 아키텍처

CQRS 아키텍처

CQRS는 읽기명령 부분을 분리함으로써 아키텍처의 일부를 단순화한다. 개발자가 불변성을 가정할 수 있기 때문에 쿼리쪽의 논리는 훨씬 단순하다. 업데이트는 다른 경로를 통해서 적용된다.

아키텍처는 항상 트레이드 오프를 염두에 두어야 한다. CQRS로 인해 한 부분은 쉬워지지만 다른 부분은 복잡해진다. 예를 들어 한 덩어리인 데이터베이스를 사용하면 트랜잭션이 쉽다. CQRS를 사용하면 트랜잭션형보다는 최종 일관성 모델로 전환해야 할 것이다.

읽기변이로부터 분리하면 논리적으로 단순해진다. 읽기쪽에서는 모든 것을 불변형으로 처리할 수 있다.

7.2.2 웹 프레임워크

웹 프로그래밍은 함수형 프로그래밍에 잘 어울린다. 웹 전반을 요구를 응답으로 바꾸는 일련의 변형으로 볼 수 있기 때문이다.

모든 함수형 언어는 다양한 웹 프레임워크를 가지고 있고, 이들의 공통적인 특성은 다음과 같다.

  • 경로 설정 프레임워크
    • 현대 웹 프레임워크들은 경로 설정(routing) 라이브러리를 사용하여 경로 설정을 애플리케이션 기능으로부터 분리시킨다.
    • 대부분, 경로에 관한 정보는 라이브러리를 사용하여 순회 가능한 표준적 자료구조에 저장된다.
  • 함수를 목적지로 사용
    • 웹상의 요구를 Request로 받아서 Response를 리턴하는 함수로 생각하면 이해가 쉽다.
  • 도메인 특화 언어
    • 흔하게 볼 수 있는 DSL은 내부 DSL이다. 호스트 언어의 문법적 설탕을 사용하여 언어 위에 구현한 새로운 의사언어. ex) 루비 온 레일즈, C#의 LINQ
    • 현대 웹 프레임워크들은 경로 설정, HTML의 임베디드 요소, 데이터베이스 작업 등에 DSL을 사용한다.
    • 함수형 언어들은 특히 선언적 코드를 선호하며, 이것은 종종 DSL의 목적 그 자체이기도 하다.
  • 빌드 도구와의 밀접한 연동
    • 대부분 함수형 언어는 IDE에서의 사용에 국한되지 않고 커맨드라인 빌드 도구와 밀접하게 연결되어 있다.
    • 새 프로젝트를 만드는 것부터 테스트를 돌리는 데까지 모든 단계에서 사용된다.

7.2.3 데이터베이스

관계형 데이터베이스는 업데이트할 때마다, 예전 값은 없어지고 새 값으로 대체된다. 왜 그런식으로 설계 되었을까?
데이터가 증가하는 것을 억제하고 저장 장소를 극대화하기 위해서이다.
이런 아키텍처 결정이 수십 년 전의 데이터베이스 설계에 깊숙이 들어 있다.
하지만 이제 세상이 바뀌었고, 컴퓨팅 자원(특히 가상 자원)은 이제 가격이 낮다.

클로저 커뮤니티에서 상업적 NoSQL 데이터베이스에 처음으로 내놓은 데이토믹은 아키텍처 전반을 뒤집으면 언어 설계자들이 함수형 개념을 어디까지 밀고 나갈 수 있는지를 알 수 있는 예이기 때문에 흥미롭다.

데이토믹은 들어오는 모든 사실들에 시간을 붙여서 저장하는 불변형 데이터베이스이다.
데이터 대신 을 저장함으로써, 저장소를 상당히 효율적으로 사용한다. 값이 한번 들어오면, 그 값의 다른 모든 인스턴스는 원본(불변형)을 가리키게 할 수 있으므로 저장 공간을 효율적으로 사용할 수 있는 것이다.

이러한 설계에서 몇 가지 흥미로운 결과가 나왔다.

  • 모든 스키마와 데이터의 변화를 영원히 기록하기
    • 스키마 조작을 포함한 모든 것이 저장 유지되어 데이터베이스의 이전 버전을오 돌아가는 것이 간단해졌다.
  • 읽기와 쓰기의 분리
    • 읽기와 쓰기 작업을 분리한다. 업데이트는 쿼리 때문에 지연되는 일이 결코 없다.
    • 데이토믹은 내부적으로 CQRS 아키텍처이다.
  • 이벤트 주도 아키텍처를 위한 불변성과 타임스탬프
    • 이벤트 주도 아키텍처는 애플리케이션 상태 변화를 이벤트 스트림으로 저장한다.
    • 이런 데이터베이스는 되감기와 재생 기능이 가능하다.