On this page:
3.1 함수형 프로그래밍과 값의 변경
3.2 Middleware pattern
7.3

3 Functional Patterns

3.1 함수형 프로그래밍과 값의 변경

클로저의 let 바인딩의 값을 바꾸는 것은 특수한 경우를 제외하고는 기본적으로 불가능하다. 그 이유는 역시 복잡성을 제거하기 위해서이다. 예를 들어 설명해 보겠다.

(let [a 10]
  (some-fn a)
  (another-fn a)
  a)

위의 코드에서 some-fn이나 another-fn이 a를 인자로 받아 어떤 작업(아마도 side effect 작업이겠죠)을 하더라도 let이 반환하는 a는 항상 10임이 보장된다. 이것을 참조 투명성(referential transparency)이라고 부른다.

만약 some-fn이나 another-fn 함수가 a를 인자로 받아 그 값을 변경하는 것이 가능하다면, 이 코드를 이해하기 위해 이 함수들의 정의까지 살펴봐야 하는데, 그에 따른 인지적 부담도 커질 뿐만아니라, 이 코드가 외부에 정의된 이 함수들에 의존성을 갖게 된다는 것이 더 큰 문제이다. 결국 복잡하게 얽히게 된다. let 바인딩의 값을 변경이 초래하는 후폭풍이 만만치 않다는 이야기이다. 그래서 기본적으로 변경을 막아 놓았다. 함수형 프로그래밍에서는 이런 방식이 기본이다.. 약간 불편해지기는 하지만 이로 인해 얻을 수 있는 이익이 워낙 막대하기 때문이라고 보면 된다.

굳이 변경하고 싶다면 다음과 같이 atom을 사용하면 된다.

(let [a (atom 10)]
  (reset! a 20))

아울러 위에서 ’특수한 경우를 제외하고’라고 했는데, 그 예외적인 경우를 들어 보겠다.

먼저 dynamic var는 ’binding 구문 안’에서만 set! 함수를 통해 바인딩된 값을 변경하는 것이 가능하다. 즉 dynamic var가 thread-local 바인딩으로 사용될 때에만 변경이 가능하다. 앞에서 ’binding 구문 안’이라고 했는데 이 binding 구문이 바로 thread local 환경을 제공한다. 그리고 이 환경 안에서 변경한 값은 해당 binding 구문 내에서만 유효하고, 그 구문을 벗어나면 원래의 값으로 자동으로 환원된다.

(def ^:dynamic a 10)

;; binding 구문 밖에서는 set!을 사용해도 변경이 불가능합니다.
(set! a 20)
; >> #’user/aIllegalStateException Can’t change/establish root binding of: a with set

(binding [a 20]
  (println "a =" a)
  (binding [a 30]
    (println "a =" a)
    (set! a 100)
    (println "a =" a))
   (println "a = " a))

(println "a =" a)

; >> a = 20
; >> a = 30
; >> a = 100
; >> a =  20

; >> a =  10

또 하나의 경우는 원래 mutable이 기본인 자바 객체의 멤버 변수를 set! 함수를 통해 직접 바꾸고자 할 때 변경이 가능하다. 객체 지향애서는 일반적으로 멤버 변수의 값을 직접 바꾸지는 않고 setter 메소드를 통해서 변경하므로, 이 함수를 쓰게 되는 경우는 극히 드물지만, 그렇지 않게 구현된 클래스의 경우에는 직접 바꿀 수 밖에 없다.

예를 들어 다음과 같이 구현된 자바 클래스가 있다고 할 때

public class Bicycle {
    public int speed;
    ......
}

다음과 같은 형식으로 field 값을 바꿀 수 있습니다.

(let [b (Bicycle.)]
  (set! (.-speed b) 10))

3.2 Middleware pattern

함수형 프로그래밍 기법 중에 middleware pattern이 있다. Ring 라이브러리에서 사용되고 있는 이 패턴은 다른 라이브러리들에서도 많이 볼 수 있을 정도로 함수형 프로그래밍에서 흔히 쓰이는 기법이다. 이 패턴에 대해 분석해 보겠다.

Ring의 middleware 패턴은 handler와 middleware라는 함수들의 결합으로 이루어져 있다. 각각의 특징을 알아 보자.

핸들러의 특징부터 보자.

미들웨어의 특징은 다음과 같다.

실제 예제 코드를 보자. 참고로, Ring에서는 middleware의 이름을 wrap으로 시작하는 관행이 있다. 밖에 있는 미들웨어 함수가, 샌드위치처럼, 안에 있는 핸들러 함수를 감싸고 있는 데서 비롯된 말이다.

(def app                              ;           Response
  (-> handler                         ;    ^         |
    (wrap-content-type "text/html")   ;    |         |
    (wrap-keyword-params)             ;    |         |
    (wrap-params)))                   ;    |         v
                                      ; Request

위의 쓰레딩 매크로 ->는 다음과 같이 확장된다.

(wrap-params (wrap-keyword-params (wrap-content-type handler
                                                     "text/html")))

쓰레딩 매크로 -> 안에서는 handler가 맨 처음에 오지만, 실제 처리시 request map을 처음으로 받는 것은 거꾸로 wrap-params(좀 더 정확히는, wrap-params 함수가 실행된 후 반환하는 ’핸들러 함수’)이다. 그래서 실제 처리는 다음과 같은 흐름을 타게 된다.

<처리 Flow>

wrap-params      wrap-keyword-params   wrap-content-params   handler
—–
request 1차  –> request 2차         -> request 3차       –> request 4차
response 4차 <– response 3차        <- response 2차      <– response 1차

위의 코드 예제에서 wrap-params나 wrap-keyword-params 함수의 구현된 코드를 봐야 전체의 흐름이 제대로 이해될 수 있는데, 각각의 함수의 코드의 구체적인 구현 내용이 전반적인 패턴을 파악하기에는 너무 복잡하다. 그래서 아래에 흐름의 골격만을 파악할 수 있도록 코드를 간단하게 직접 짜보았다.

(def counter (atom 0))

;; 프로그램의 실행 순서를 명시적으로 표시하기 위한 보조 함수.
(defn inc-counter []
  (swap! counter inc))

(def handler-1
  (fn [req] ; handler-1
    (let [req’ (assoc req :handler-1-req (inc-counter))
          _    (println "handler-1 req:" req’)
          res  (assoc req’ :handler-1-res (inc-counter))
          _    (println "handler-1 res:" res)]
      res)))

;; middleware-1
(defn wrap-1 [handler-1]
  ;; handler-2
  (fn [req]
    (let [req’ (assoc req :handler-2-req (inc-counter))
          _    (println "handler-2 req:" req’)
          res  (handler-1 req’)
          res’ (assoc res :handler-2-res (inc-counter))
          _    (println "handler-2 res:" res’)]
      res’) ))

;; middleware-2
(defn wrap-2 [handler-2]
  ;; handler-3
  (fn [req]
    (let [req’ (assoc req :handler-3-req (inc-counter))
          _    (println "handler-3 req:" req’)
          res  (handler-2 req’)
          res’ (assoc res :handler-3-res (inc-counter))
          _    (println "handler-3 res:" res’)]
      res’) ))

위에서 준비한 middleware들과 handler들을 Ring 형식으로 호출하면 다음과 같다.

(def app
  (-> handler-1
    (wrap-1)
    (wrap-2)))

(app {})
; >> handler-3 req: {:handler-3-req 1}
; >> handler-2 req: {:handler-3-req 1, :handler-2-req 2}
; >> handler-1 req: {:handler-3-req 1, :handler-2-req 2, :handler-1-req 3}
; >> handler-1 res: {:handler-3-req 1, :handler-2-req 2, :handler-1-req 3,
;                    :handler-1-res 4}
; >> handler-2 res: {:handler-3-req 1, :handler-2-req 2, :handler-1-req 3,
;                    :handler-1-res 4, :handler-2-res 5}
; >> handler-3 res: {:handler-3-req 1, :handler-2-req 2, :handler-1-req 3,
;                    :handler-1-res 4, :handler-2-res 5, :handler-3-res 6}
; => {:handler-3-req 1, :handler-2-req 2, :handler-1-req 3,
;     :handler-1-res 4, :handler-2-res 5, :handler-3-res 6}

위의 쓰레딩 매크로가 확장된 실제 모습을 코드로 재구성하면 다음과 같은데, 코드가 약간 복잡하게 보이지만, 그 안에서 대단한 일을 하고 있지는 않은 코드이므로, 조금만 인내심을 갖고 분석하면 어렵지 않게 이해할 수 있을 것이다. 결국 위의 app이라는 심볼은 아래의 ’;; handler-3’ 이라고 표시한 함수 객체 전체가 되고, 다음 예제의 (1)의 비어있는 맵 {}이 그 인자로 들어가게 된다.

(reset! counter 0)

 ;; handler-3
((fn [req] ; (2)
              ;; request 1차 변경
   (let [req’ (assoc req :handler-3-req (inc-counter))
         _    (println "handler-3 req:" req’)
               ;; handler-2
         res  ((fn [req] ; (4)
                            ;; request 2차 변경
                 (let [req’ (assoc req :handler-2-req (inc-counter))
                       _    (println "handler-2 req:" req’)
                             ;; handler-1
                       res  ((fn [req] ; (6)
                                          ;; request 3차 변경
                               (let [req’ (assoc req :handler-1-req
                                                 (inc-counter))
                                      _   (println "handler-1 req:" req’)
                                          ;; response 1차 변경
                                     res  (assoc req’ :handler-1-res
                                                 (inc-counter))
                                     _    (println "handler-1 res:" res)]
                                 res))
                               req’) ; (5)
                            ;; response 2차 변경
                       res’ (assoc res :handler-2-res (inc-counter))
                       _    (println "handler-2 res:" res’)]
                   res’))
               req’) ; (3)
               ;; response 3차 변경
          res’ (assoc res :handler-3-res (inc-counter))
          _   (println "handler-3 res:" res’)]
     res’))
 {}) ; (1)
; >> handler-3 req: {:handler-1-req 1}
; >> handler-2 req: {:handler-1-req 1, :handler-3-req 2}
; >> handler-1 req: {:handler-1-req 1, :handler-3-req 2, :handler-2-req 3}
; >> handler-1 res: {:handler-1-req 1, :handler-3-req 2, :handler-2-req 3,
;                    :handler-1-res 4}
; >> handler-2 res: {:handler-1-req 1, :handler-3-req 2, :handler-2-req 3,
;                    :handler-1-res 4, :handler-2-res 5}
; >> handler-3 res: {:handler-1-req 1, :handler-3-req 2, :handler-2-req 3,
;                    :handler-1-res 4, :handler-2-res 5, :handler-3-res 6}
; => {:handler-3-req 1, :handler-2-req 2, :handler-1-req 3,
;     :handler-1-res 4, :handler-2-res 5, :handler-3-res 6}

결론적으로 보면, 예전 코드에서 app 심볼은 함수 객체를 가리킨다. 이 함수 객체는 (def app ...)에서 정의만 되었을 뿐 호출되지는 않은 상태이므로, 호출하려면 (app {...})와 같이 해 주어야 하는데, 이런 식의 실제 호출은 Ring이 알아서 해 주게 된다.

결국 미들웨어 패턴의 핵심은, 함수형 프로그래밍에서 함수 객체를 1급 객체로 처리하게 해 주는 기능을 십분 활용한 것이다. 함수 객체 자체를 ’인자’로 전달하고, 함수 객체 자체를 ’리턴값’으로 반환하는, 함수형 프로그래밍 방식을 적극 채용하고 있는 것이라 볼 수 있다.

우리는 함수가 1급 객체가 아닌 언어에 길들어져 있어서, 이런 방식을 꿈에도 꾸지 못한 것일 뿐, 익숙해지면 적극 활용해 볼 만한 가치가 있는 것으로 보인다. 이러한 패턴을 분석하면서, Heidegger가 이야기한 ’언어는 존재의 집이다’ 라는 말이 새삼 떠오른다. 언어가 사고 방식마저 지배할 수 있다는 사실이 한편으로는 두려우면서, 동시에 이 벽을 깨고 함수 객체를 1급 객체로 사용할 수 있다는 것을 일깨워준 천재 프로그래머들에게 경의를 표하고 싶다.