Knex를 이용해서 MySQL에 접속해 쿼리를 실행시킬 수 있고 스키마나 데이터를 변경할 수 있다. 아주 편리한 이 라이브러리는 커넥션풀 또한 지원하고 있으며 직접적이진 않지만 응답시간을 체크할 수도 있다. 바로 쿼리 실행 시, 종료 시 호출될 콜백함수를 등록할 수 있기 때문이다. 이런 식으로 사용하는 것이 최선인지는 잘 모르겠지만 개발과정과 프로덕션 운영 중 발생한 문제를 해결하는데 큰 도움이 됐기에 정리해봤다. 


구현 


우선 HTTP 서버를 생성했다. 

1
2
3
4
5
6
7
8
9
const server = http.createServer(async (req, res) => {
  res.writeHead(200, {'Content-Type': 'text/html'})
  const q = url.parse(req.url, true).query
  const userId = q.user_id
  const { id, title, firstName, surname, dateOfBirth } = await getUser({ userId })
  res.write(`Found user: ID: ${title} ${surname} ${firstName} ${dateOfBirth}`)
  res.end()
})
server.listen(serverPortNo)
cs


db-conn.js 파일을 생성하고 knex 객체를 생성한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const db = knex({
  client: 'mysql',
  connection: {
    timezone: 'UTC',
    host: process.env.SERVICE_MYSQL_ADDRESS,
    user: process.env.SERVICE_MYSQL_USER,
    password: process.env.SERVICE_MYSQL_PASSWORD,
    database: process.env.SERVICE_MYSQL_DATABASE
  },
  pool: {
    min: parseInt(process.env.KNEX_POOL_MIN, 10|| 2,
    max: parseInt(process.env.KNEX_POOL_MAX, 10|| 10
  }
})
cs


knex 객체에 쿼리 실행 시 호출될 함수를 정의하고 등록한다. 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const queries = {}
let count = 0
 
const onQuery = query => {
  if (count > knexTrackingConnMaxCnt) count = 0
  const uid = query.__knexQueryUid
  queries[uid] = {
    position: count,
    method: query.method,
    sql: query.sql,
    queriedAt: new Date().getTime()
  }
  count += 1
}
 
db.on('query', onQuery)
cs


쿼리가 실행되면 쿼리의 ID를 queries 객체에 키로 저장하며 값으로는 로그로 사용할 수 있는 값들을 넣었다. queriedAt 프로퍼티는 종료 시 시간과의 차이를 구할 때 사용한다. 얼마나 많은 쿼리가 누적이 될지 알 수 없기 때문에 tracking connection의 최대값을 설정했다. 


knex 객체에 쿼리 종료 시 호출될 함수를 정의하고 등록한다. 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const onQueryResponse = (response, query) => {
  const uid = query.__knexQueryUid
  if (queries[uid]) {
    const diff = new Date().getTime() - queries[uid].queriedAt
    if (diff > knexResponseTimeThresholdMsec) {
      const perfLog = {
        position: queries[uid].position,
        method: queries[uid].method,
        sql: queries[uid].sql,
        diff
      }
      console.log(`RESPONSE_TIME: ${diff} ms > ${knexResponseTimeThresholdMsec}`, perfLog)
    }
    delete queries[uid]
  }
}
 
db.on('query-response', onQueryResponse)
cs

쿼리가 종료될 때 query의 ID를 키로하여 queries 객체에서 값을 찾아 종료 시간과 queriedAt 차이를 구해 설정된 threshold와 비교한다. 시간이 초과된 경우 데이터를 화면에 출력한다. 


테스트


다음의 명령을 실행해서 필요한 패키지를 설치하고 서버를 실행한다.

npm i

npm start


메시지를 서버로 전송하기 위해 cURL을 이용한다. 

curl -i "http://localhost:8080/?user_id=3"


결과 확인


유저 정보를 읽는 단순한 쿼리는 수 ms 내에 처리되기 때문에 로그를 볼 수 없다. 그래서 KNEX_RESPONSE_TIME_THRESHOLD_MSEC=1로 설정한 뒤 cURL 명령을 실행하여 로그가 출력되는 것을 확인했다.

RESPONSE_TIME: 3 ms > 1 { position: 0,

  method: 'select',

  sql: 'select `id`, `title`, `first_name`, `surname`, `date_of_birth` from `users` where `id` = ?',

  diff: 3 }


예제이기 때문에 화면에 출력하도록 했지만 실제로는 로그파일에 쓰거나 또는 ElasticSearch에 로그를 전송해서 데이터 분석이나 성능 튜닝에 사용할 수 있다. Busy hour인 경우에는 Slack에 바로 메시지를 전송해 백앤드 엔지니어들을 괴롭힐 수도 있다. 


이 예제에서는 dotenv 라이브러리를 이용해서 .env 파일에 저장된 변수들을 사용했다. 그리고 예로 든 db_user 데이터베이스에 users 테이블이 존재한다고 가정했다. 테이블을 생성하는 방법은 간단하니 여기에서는 패스하기로.


전체 소스 코드는 이 곳에서 확인할 수 있다.


https://github.com/ksleeq21/knex-response-time



Posted by 코딩새싹
,

처음 Clojure를 접한 것은 2년 전 겨울 런던 시내에서 있었던 Clojure 밋업에 갔을 때였다. 그 때까지는 Haskell를 공부하고 있어서 함수형 프로그래밍이 어떤 것인지 대충 알고 있었지만 실제로 업무에서 사용해 보지는 못하고 있었다. 밋업에서 나를 포함한 몇몇 초보자들은 온라인 사이트에서 아주 간단한 문제를 푸는 것으로 시간을 보냈는데 간단하게 문법을 익힐 수 있는 사이트였다. 괄호가 정말 많구나. 괴상하다. 솔직히 첫인상은 이랬다. 그 때부터 관심을 갖기 시작해서 책도 여러 권 사서 읽었다. Java를 싫어해서 JVM에 대한 막연한 불신이 있었는데 Clojure는 설치하기가 쉽고 텍스트 에디터에서 가볍게 코딩해서 결과를 볼 수 있고 Node.js처럼 REPL도 지원하니까 혼자 공부하기에도 Haskell보다 훨씬 쉬웠다. 괄호 때문에 시작과 끝이 어디인지 잘 모르겠는 괴상한 문법이지만 스타일에 익숙해지니까 문제가 되지 않았다. 좀 더 잘 쓰고 싶어서 업무에 도입을 시도했지만 잘 되진 않았다. 하지만 잘 알고 있는 로직을 Clojure로 구현해보면서 정말 많이 배웠다. 내가 Clojure를 쓰는 이유는.


Clojure는 함수형 언어다. Haskell처럼 pure functional 하지는 않다. 

함수는 pure하다는데, 헷갈리면 수식의 함수를 생각하면 된다. 입력이 같으면 출력이 같다. 

함수는 First-class object이고 나는 이 개념에 익숙하다.

데이터는 immutable 하다. 처음에는 이것 때문에 고생을 많이 했다.

괄호가 많은 이상한 문법을 가졌지만 스타일에 익숙해지면 문제가 되지 않고 그 덕분에 긴 함수를 만들지도 않는다.

처음 시작했을 때에는 개념과 예제코드부터 이해하기 어려웠지만 조금만 익숙해지면 에러가 잘 나지 않는 코드, 재사용이 쉬운 코드를 만들 수 있다. 


Haskell과 비교해도 장점이 있다. Haskell은 순수 함수형 언어이기 때문에 어떤 상태의 변화를 다루는데 어려움이 있다. 그런데 Clojure는 refs, agents, atoms 같은 기능을 제공하기 때문에 이 문제로부터 자유롭다. 그리고 dynamic typing을 지원한다. 경력의 반을 C로 프로그래밍 했지만 최근 몇 년 동안은 Node.js만 써와서 그런지 static type 언어를 쓸 때 무척 어색하다. 하지만 Clojure는 습관처럼 코딩해도 문제가 없다. Node.js의 destructuring object 개념처럼 Clojure에서도 destructuring을 해서 변수와 값을 바인딩해서 사용하는데 이 방식이 아주 마음에 든다. 그리고 병렬 프로그래밍에 대한 개념도 정말 멋지다. 이건 나중에 자세히. 


지금도 Haskell의 문법이 가장 아름답다고 생각하고 Clojure의 문법은 괴상하다고 생각하지만 쓰면 쓸수록 Clojure는 장점이 많은 언어라고 생각한다.


참고


Clojure 공식 사이트

https://clojure.org


Clojure 문서 

http://clojure-doc.org

http://clojuredocs.org/quickref


Clojure 툴박스

https://www.clojure-toolbox.com


초보자가 문제 풀면서 문법을 익히기 좋은 사이트

http://www.4clojure.com


15분완성

https://adambard.com/blog/clojure-in-15-minutes



Posted by 코딩새싹
,

주로 쓰는 개발 프레임워크는 Node.js 이다. 그래서 백앤드를 구성하는 20개 남짓의 microservice들은 모두 노드로 구현돼 있다. 서비스 간 통신을 위해 만든 라이브러리가 HTTP와 Redis로 구현돼 있기 때문에 프로토콜만 맞출 수 있다면 서비스를 구현할 때 어떤 언어든지 고려해볼 수 있다. 그래서 내가 만든 몇 개의 서비스들 중에서 가장 중요한 서비스를 테스트 삼아 Clojure로 구현해 보았다. 처음부터 끝까지 혼자 다 만든 서비스라서 Node.js로 구현된 로직을 Clojure로 다시 쓰는 데에는 오랜 시간이 걸리지 않았고 성능도 비슷한 수준으로 나왔다. 오히려 다시 쓰기 시작하면서 구조가 더 명확해지고 눈엣가시 같던 구현들도 제거할 수 있었으니 상당히 만족스러웠다. 그 상태에서 두 개 서비스를 더 Clojure로 다시 썼고 충분히 좋은 성능을 보여주었기 때문에 프로덕션 릴리즈를 해보고 싶었다. 하지만 백앤드 팀의 유일한 Clojure 개발자라서 혼자 모든 것을 다 해야 하는 상황이 팀에게도 나에게도 이상적이지 않았다. 제법 여럿이 흥미를 보였지만 더 발전되지 못했고 지금은 아무도 찾지 않는 저장소가 되고 말았다.


최근에 Spark을 공부하면서 Scala를 써보려고 했는데, Clojure 만큼은 아니란 생각이 들었다. Functional에 왜 OOP를 가져다 붙였는지 잘 이해할 수가 없다. 언어들이 서로 좋은 것들을 배껴와 서로 닮아 가는 건 알겠는데 그럴 거면 Java를 더 functional 하게 만들지 하는 생각이 들었다. 그래서 관심이 식어버렸다. 다시 Clojure를 꺼내 들었는데 하나도 기억이 안나서 처음 했던 것처럼 간단한 headless 서버를 만들어 보기로 했다. 이번에 쓸 프레임워크는 Pedestal이다. 


이곳의 코드를 참고했다. 

https://github.com/pedestal/pedestal/tree/master/samples/hello-world


프로젝트 생성


다음 명령을 실행하여 프로젝트를 생성할 수 있다. 

lein new pedestal-service echo-service


다음과 같이 폴더와 파일들을 생성한다. 

echo-service

- src

  - services

    - echo.clj

    - ping.clj

  - server.clj


코드 수정


각 파일에 들어갈 코드는 다음과 같다. 


1
2
3
4
5
;; echo.clj
(ns echo-server.services.echo)
 
(defn handler [request]
  (let [message (get-in request [:params :message] "Echo")]
    {:status 200 :body (str message "\n")}))
cs


1
2
3
4
;; ping.clj
(ns echo-server.services.ping)
 
(defn handler [request]
  {:status 200 :body (str "pong\n")})
cs


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
;; project.clj
(ns echo-server.server
  (:gen-class)
  (:require [io.pedestal.http :as http]
            [io.pedestal.http.route :as route]
            [io.pedestal.http.body-params :as body-params]
            [io.pedestal.http.route.definition :refer [defroutes]]
            [echo-server.services.ping :as ping]
            [echo-server.services.echo :as echo]))
 
(defroutes routes
  [[["/"
      ["/ping" {:get ping/handler}]
      ["/echo" {:get echo/handler}]]]])
 
(def service {:env :prod
              ::http/routes routes
              ::http/resource-path "/public"
              ::http/type :jetty
              ::http/port 8080})
 
(defonce runnable-service (http/create-server service))
 
(defn run-dev
  "The entry-point for 'lein run-dev'"
  [& args]
  (println "\nCreating dev server")
  (-> service
      (merge {:env :dev
              ::http/join? false
              ::http/routes #(deref #'routes)
              ::http/allowed-origins {:creds true :allowed-origins (constantly true)}})
      http/default-interceptors
      http/dev-interceptors
      http/create-server
      http/start))
 
(defn -main
  "The entry-point for 'lein run'"
  [& args]
  (println "\nCreating prod server")
  (http/start runnable-service))
cs

 

자동으로 생성되는 파일명을 변경했기 때문에 project.clj 파일을 다음과 같이 일부 수정해야 한다. 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
;; project.clj
(defproject echo-service "0.5.1"
  :description "Simple hello-world service in Pedestal"
  :url "http://pedestal.io/samples/index"
  :license {:name "Eclipse Public License"
            :url "http://www.eclipse.org/legal/epl-v10.html"}
  :dependencies [[org.clojure/clojure "1.8.0"]
                 [io.pedestal/pedestal.service "0.5.1"]
                 [io.pedestal/pedestal.jetty "0.5.1"]
                 [ch.qos.logback/logback-classic "1.1.7" :exclusions [org.slf4j/slf4j-api]]
                 [org.slf4j/jul-to-slf4j "1.7.21"]
                 [org.slf4j/jcl-over-slf4j "1.7.21"]
                 [org.slf4j/log4j-over-slf4j "1.7.21"]]
  :min-lein-version "2.0.0"
  :resource-paths ["config", "resources"]
  :profiles {:dev {:aliases {"run-dev" ["trampoline" "run" "-m" "hello-world.server/run-dev"]}
                   :dependencies [[io.pedestal/pedestal.service-tools "0.5.1"]]}}
  :main echo-server.server)
cs


실행 


이후 다음 명령으로 서버를 실행시킨다. 

lein run


HTTP 서버를 개발 모드에서 실행시키는 방법은 다음과 같다.

lein repl

echo-server.server=> (def serv (run-dev))


동작 확인


서버가 정상적으로 동작하는지 보기 위해 cURL을 이용한다. 

curl -i "localhost:8080/ping" 

curl -i "localhost:8080/echo?message=hello"


소스코드 

Pedestal을 이용한 간단한 예제이지만 API endpoint를 이해하기에 부족함이 없다. 구현 코드는 이 곳에서 볼 수 있다. 


참고


http://pedestal.io/index

https://github.com/pedestal/pedestal

https://nordicapis.com/10-frameworks-for-building-web-applications-in-clojure/


Posted by 코딩새싹
,