클린 아키텍처를 읽으며 정리한 내용입니다.
함수형 프로그래밍이라는 개념은 람다 계산법(알론조 처치, 1930)이라는 형태로 프로그래밍보다 앞서 등장했다.
정수를 제곱하는 프로그램을 자바에서는 아래와 같이 작성할 수 있다.
public class Squint {
public static void main(String args[]) {
for (int i=0; i<25; i++)
System.out.prinln(i*i);
}
}
리스프에서 파생한 클로저는 함수형 언어로, 다음과 같이 구현할 수 있다.
(println ;___ 출력한다
(take 25 ;___ 처음부터 25까지
(map (fn [x] (*x x)) ;___ 제곱을
(range)))) ;___ 정수의
println
, take
, map
, range
는 모두 함수이며, 리스프에서는 함수를 괄호 안에 넣는 방식으로 호출한다.(fn [x] (* x x))
는 익명 함수로, 곱셈 함수를 호출하면서 입력 인자를 두 번 전달하여 입력의 제곱을 계산한다.예시에서 보았듯이, 자바 프로그램은 프로그램 실행 중 상태가 변할 수 있는 가변 변수를 사용하는 반면 클로저에서는 가변 변수가 없다. 함수형 언어에서는 x와 같은 변수가 한 번 초기화되면 변하지 않는다.
왜 변수의 가변성이 아키텍처에서 중요할까?
경합 조건, 교착상태, 동시 업데이트 등의 문제는 모두 가변 변수로 인해 발생한다.
어떤 변수도 갱신되지 않는다면 동시성 애플리케이션에서 마주치는 경합 조건이나 동시 업데이트 문제가 일어나지 않는다.
이런 불변성은 자원이 무한대라면 실현 가능하다. 하지만 무한대가 아니라면.. 타협이 필요하다.
가장 중요한 타협 중 하나는 애플리케이션, 또는 애플리케이션의 내부 서비스를 가변 컴포넌트와 불변 컴포넌트로 분리하는 일이다.
불변 컴포넌트?
- 함수형 방식으로만 작업이 처리되며, 가변변수가 사용되지 않는다.
- 변수의 상태를 변경할 수 있는 하나 이상의 다른 컴포넌트와 서로 통신한다.
- 트랜잭션 메모리가 DB와 같이 트랜잭션을 사용하거나 재시도 기법을 통해 이 변수를 보호한다.
이러한 접근법의 예시로 클로저의 atom 기능을 들 수 있다.
(def counter (atom 0)) ; ___ counter를 0으로 초기화
(swap! counter inc); ___ counter를 안전하게 증가
예시에서 counter 변수는 atom으로 정의되었고, atom의 값을 변경하려면 반드시 swap! 함수를 사용해야 한다.
swap! 함수는 변경할 atom 변수와, atom의 새로운 값을 계산할 함수를 받아 전통적인 스왑 알고리즘을 실행한다.
counter의 값을 읽은 후 inc로 전달
inc 함수가 반환하면 counter의 값을 잠그고 inc 함수로 전달했던 값과 비교
애플리케이션을 제대로 구조화하려면 변수를 변경하는 컴포넌트와 변경하지 않는 컴포넌트를 분리하며, 가능한 많은 처리를 불변 컴포넌트로 옮겨야 한다.
이를 위해 가변 변수를 보호하는 적절한 수단을 동원해야 한다.
더 많은 메모리를 확보할수록, 기계가 더 빨라질수록, 필요한 가변 상태는 더 적어진다.
예시: 고객의 계좌 잔고를 관리하는 은행 어플리케이션
이벤트 소싱은 상태가 아닌 트랜잭션을 저장하는 전략으로, 상태가 필요해지면 상태의 시작점부터 모든 트랜잭션을 저장한다.
이렇게 하면 저장소에서 삭제되거나 변경되는 것은 하나도 없다. 변경과 삭제가 일어나지 않으므로 동시 업데이트 문제도 일어나지 않는다.
이처럼 저장 공간과 처리 능력이 충분하면 애플리케이션이 완전한 불변성을 갖도록 만들 수 있고, 완전한 함수형으로 만들 수 있다. 소스 코드 버전 관리 시스템도 이런 식으로 동작한다!
세 프로그래밍 패러다임은 다음과 같은 것들을 앗아가며 코드를 작성하는 방식을 한정시켰다.
시간이 많이 흘렀음에도 지금의 소프트웨어 규칙은 최초의 코드를 작성할 때와 조금도 달라지지 않았으며, 컴퓨터 프로그램은 순차, 분기, 반복, 참조 네 개의 조합이다.