Kafka monitoring and administration

카프카(Kafka)는 생각보다 쉬운 툴이다. 하지만 장기적으로 운영할 시스템 환경은 간단히 한번 돌리고 마는 경우와 고려해야 할 것들이 제법된다.  운영 관점의 설정 값들을 엉뚱하게 해놓으면 잘 차려진 밥상에 꼭 재를 뿌리게 된다.  이런 잘못을 범하지 않으려면 Kafka라는 이름뿐만 아니라 이 도구가 어떤 방식으로 동작하는지 깊게 들어가볼 필요가 있다.  물론 그 동작 방식을 운영 환경과 어울려 살펴봐야만 한다.

상태 조회하기

운영을 할려면 가장 먼저 필요한 일이 시스템의 상태를 살펴볼 수 있는 도구가 필요하다.

alias monitor='kafka-run-class.sh kafka.tools.ConsumerOffsetChecker --zookeeper localhost:2181'
alias topic='kafka-topics.sh --zookeeper localhost:2181'
alias consumer='kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic'

이미 있는 명령들을 “쉽게 쓰자”라는 차원에서 alias를 정의하긴 했다.  consumer의 경우에는 Producer를 통해 생성된 Topic 데이터가 정상적으로 생성됐는지를 확인하기 위해 필요하다.  나머지 2 개의 명령은 어떤 경우에 사용하는게 좋을까?

topic 명령은 일반적인 토픽 관리를 위한 기능들을 모두 제공한다. 만들고, 변경하고, 삭제하는 모든 기능을 이 스크립트 명령으로 제어한다. 주목하는 토픽 상태는 describe 옵션으로 확인할 수 있다. 체크해야할 사항은 2가지다.

  • 현재 토픽의 Partition의 수와 Replica가 어떤 노드에 배정이 되어 있는지를 확인할 수 있다.
  • 개별 Partition의 현재 Leader가 어떤 놈인지를 확인할 수 있다.

이 두가지 정보는 이후에 Kafka cluster의 성능 튜닝을 위해 파악해야할 정보다.

monitor 명령은 토픽의 데이터를 subscribe한 특정 그룹으로의 데이터 전달이 제대로 이뤄지고 있는지를 각 Partition별로 확인할 수 있도록 해준다.

출력 결과의 Lag 항목을 보면 현재 처리를 위해 각 파티션에서 대기하고 있는 데이터 개수를 확인할 수 있다.  특정 그룹에서 소비해야할 토픽 데이터의 건수가 증가하지 않고 일정 수준을 유지한다면 Kafka 시스템이 클러스터로써 제대로 동작을 하고 있다는 것을 의미한다.  기본 명령의 출력 결과가 파티션별이기 때문에 총합을 볼려면 각 파티션들의 합을 구하는 기능을 만들어야 아래와 같은 스크립트로 만들 수 있다.

#!/bin/sh
kafka-run-class.sh kafka.tools.ConsumerOffsetChecker --zookeeper localhost:2181 --group $1 --topic $2 | egrep -v "Group" | awk -v date="$(date +"%Y-%m-%d %H:%M:%S ")" '{sum+=$6} END {print date sum}'

만일 특정 그룹과 토픽에 대한 상시적인 모니터링이 필요하다면 위 스크립트를 한번 더 감싸는 아래의 monitor.sh 스크립트를 작성해서 돌리면 된다.  이 스크립트는 문제가 될 그룹과 토픽에 대한 정보를 30초 단위로 조회하여 출력한다.

#!/bin/sh
while (true) do /home/ec2-user/total.sh groupname topic 2> /dev/null; sleep 30; done;

당연한 시간을 너무 짧게 분석하는 것도 되려 시스템 자체에 부하를 줄 수 있다는 사실을 잊지는 말자!

Kafka 최적화하기

Kafka를 단순히 로그 수집용이 아닌 여러 토픽들을 섞어서 메시징 용도로 사용하는 경우에 최적화는 큰 의미를 갖는다.  특히 여러 토픽을 다루는 환경에서 Kafka 운영자는 다음 사항들을 꼼꼼하게 챙겨야 한다.

  • 토픽의 종류별로 데이터의 양이 틀려질 수 있다.
  • 연동하는 서비스 시스템의 성능이 클러스터의 성능에 영향을 미칠 수 있다.

따라서 효율적이고 안정적인 메시징 처리를 위해서 토픽이 데이터 요구량에 대응하여 적절한 파티션들로 구성되어 있는지를 확인해야 한다.  한 토픽에 여러 파티션들이 있으면, 각각의 파티션이 독립적으로 데이터를 처리해서 이를 Consumer(or ConsumerGroup)으로 전달한다. 즉 데이터가 병렬 처리된다.  병렬 처리가 되는건 물론 좋다.  하지만 이 병렬 처리가 특정 장비에서만 처리되면 한 장비만 열라 일하고, 나머지 장비는 놀게 된다.

일을 하더라도 여러 장비들이 빠짐없이 일할 수 있는 평등 사회를 실현해야한다.  Kafka 사회에서 평등을 실현할려면 토픽을 클러스터내의 여러 장비에서 고르게 나눠 실행해야한다.  이런 나눔을 시스템 차원에서 알아서 실현해주면 운영하는 사람이 신경쓸 바가 없겠지만, 현재 버전(내가 사용하는 버전)은 해줘야한다.  -_-;;;

평등 실행을 위해서는 일단 일하길 원하는 노드에 데이터가 들어가야 한다.  따라서 Replica 조정을 먼저 해준다.  조정 완료 후 이제 리더를 다시 뽑는다.  리더를 뽑는 방법은 아래 move.json 파일에서 보는 바와 같이 replicas 항목들의 클러스터 노드의 sequence 조합을 균일하게 섞어야 한다.  예처럼 2, 1, 3, 2, 1 과 같이 목록의 처음에 오는 노드 아이디 값이 잘 섞이도록 한다.

{"partitions": [
    {"topic": "Topic", "partition": 0, "replicas": [2,1,3]},
    {"topic": "Topic", "partition": 1, "replicas": [1,2,3]},
    {"topic": "Topic", "partition": 2, "replicas": [3,1,2]},
    {"topic": "Topic", "partition": 3, "replicas": [2,1,3]},
    {"topic": "Topic", "partition": 4, "replicas": [1,2,3]},
    {"topic": "Topic", "partition": 5, "replicas": [2,1,3]},
    {"topic": "Topic", "partition": 6, "replicas": [3,1,2]},
    {"topic": "Topic", "partition": 7, "replicas": [1,2,3]},
    {"topic": "Topic", "partition": 8, "replicas": [2,1,3]},
    {"topic": "Topic", "partition": 9, "replicas": [3,1,2]}
  ],
  "version":1
}

그리고 아래 명령을 실행해서 섞인 Replica설정이 먹도록 만든다.

kafka-reassign-partitions.sh --reassignment-json-file manual_assignment.json --execute

물론 시간을 줄일려면 최초 단계에 Replicas 개수가 같은 replica 노드로 섞어주기만 하면 가장 좋다. 섞는 것만 한다면 작업 자체는 얼마 시간없이 바로 처리된다.

kafka-preferred-replica-election.sh --zookeeper localhost:2181 --path-to-json-file election.json

이제 리더를 각 클러스터의 여러 노드로 분산시키면 된다.  특정한 토픽만 지정해서 분할을 할려면 분할 대상을 아래 election.json 파일과 같은 형태로 작성해서 이를 위 명령의 파라미터로 넘긴다.  특정한게 아니라 전체 시스템의 모든 토픽, 파티션 수준에서 재조정을 할려면 별도 파일없이 실행시키면 된다.

{
 "partitions":
  [
    {"topic": "Topic", "partition": 0},
    {"topic": "Topic", "partition": 1},
    {"topic": "Topic", "partition": 2},
    {"topic": "Topic", "partition": 3},
    {"topic": "Topic", "partition": 4},
    {"topic": "Topic", "partition": 5},
    {"topic": "Topic", "partition": 6},
    {"topic": "Topic", "partition": 7},
    {"topic": "Topic", "partition": 8},
    {"topic": "Topic", "partition": 9}
  ]
}

이렇게 설정을 변경하면 Kafka 시스템이 라이브 환경에서 변경을 시작한다.  하지만 변경은 쉽지 않다는거… 좀 시간이 걸린다. 작업이 완료되지 않더라도 위의 설정이 제대로 반영됐는지 topic의 describe 명령으로 확인할 수 있다.

Replicas 항목들이 분산 노드들에 공평하게 배포되었고, 등장 순서에 따라 Leader가 적절하게 분산됐음을 확인한다.  만약 Replicas의 가장 앞선 노드 번호가 리더가 아닌 경우라면 아직 리더 역할을 할 노드가 제대로 실행되지 않았다는 것이다.  막 실행한 참이라면 logs/state-change.log 파일을 tail로 걸어두면 리더 변경이 발생했을 때를 확인할 수 있다.  어제까지 멀쩡했는데 오늘 갑자기 그런다면?? OMG 장비를 확인해보는게 좋을 듯 싶다. 🙂

변경이 마무리되는데까지는 좀 시간이 걸린다.  라이브 환경에는 특히 더할 수 있다. OLTP 작업들이 많다면 이런 튜닝 작업은 눈치 안보고 할 수 있는 시간에 스리슬적 해치우는게 좋다. 아니면 새벽의 한가한 때도??? -_-;;

Conclusion

이 과정을 거쳐서 정리를 마무리하면 대강 아래와 같은 최적화 과정이 절절한 그래프를 얻을 수 있다.

  • Section A – Leader 설정이 클러스터의 특정 노드에 집중된 상태
  • Section B – __consumer_offests 토픽의 replication 변경 및 leader 설정 변경
  • Section C – 설정 변경 적용 완료.

 

Kafka 어드민 도구

어드민 활동을 좀 더 쉽게 할 수 있는 도구로 LinkedIn 개발팀에서 공유한 도구들이 아래에 있다. 이 도구의 특징은 간단한 설치 방법과 함께 전반적인 설정들을 손쉽게 변경할 수 있는 방안을 제공해준다.

  • https://github.com/linkedin/kafka-tools
  • https://github.com/linkedin/kafka-tools/wiki

예를 들어 Topic의 데이터를 Auto commit 방식이 아닌 manual commit 방식을 사용하는 경우, __consumer_offsets 이라는 topic이 자동 생성된다.  못보던 놈이라고 놀라지 말자. 기본값이라면 아마도 이 토픽의 replication이 단일 노드(replication factor = 1)로만 설정되어 있다. 메시지 큐를 Kafka로 구현한 내 경우에 모든 토픽을 manual commit 방식으로 처리한다. 그런데 이 설정이면 __consumer_offsets 토픽의 리더 노드가 맛이 가면 커밋을 못하는 지경이 되버린다. 결국 전체 데이터 처리가 멈춰버린다.

이런 경우를 사전에 방지하기 위해 __consumer_offsets 토픽의 replicas 설정이 올바른지 꼭 확인해야 한다.  그리고 설정이 잘못됐다면 replication 설정을 반드시 수정해주자.  자책하지 말자.  Kafka의 기본 설정은 노트북에서도 돌려볼 수 있도록 안배가 되어 있다.  한번쯤 고생해봐야 쓰는 맛을 알거라는 개발자의 폭넓은 아량일 것이다.

이 어드민 도구는 아래 Replication creation script에서 보는 바와 같이 별도의 json 파일없이도 이를 실행할 수 있다.

kafka-assigner -z localhost:2181 -e set-replication-factor --topic __consumer_offsets --replication-factor 3

이 기능 이외에도 대부분이 토픽들을 한방에 정리할 수 있는 여러 기능들을 제공한다.  깃헙 Wiki 페이지에서 각 명령을 실행하는 방법을 확인할 수 있다.

Kafka administration summary

몇 가지 방법들을 썰로 좀 풀어봤지만, 기본적인 사항들을 정리해보면 아래와 같다.

Basic configuration

  • Cluster size = 3 – Cluster는 최소 3개 이상이어야 하며, 운영 노드의 개수는 홀수가 되도록 한다.
  • Partition size = 8 – 기본 Partition은 개별 노드의 CPU Core 개수 혹은 그 이상을 잡는다.
  • Replication factor = cluster size – Replication factor는 처리하는 데이터가 극적으로 작지 않는다면 Cluster 개수와 동일한 값이 되도록 한다.
  • JVM Option – 마지막까지 괴롭히는 놈이다. 현재 테스트중인 옵션
export KAFKA_HEAP_OPTS="-Xmx9g -Xms9g"
export KAFKA_JVM_PERFORMANCE_OPTS="-XX:MetaspaceSize=96m -XX:+UseG1GC -XX:MaxGCPauseMillis=20 -XX:InitiatingHeapOccupancyPercent=35 -XX:G1HeapRegionSize=16M -XX:MinMetaspaceFreeRatio=50 -XX:MaxMetaspaceFreeRatio=80"

Tuning

  • Partition의 개수를 늘린다. 토픽의 alter 명령을 사용해서 partition의 크기를 변경한다.
  • 토픽의 replication factor 확인하고, 각 partition의 리더가 전 클러스터에 균일하게 배분되었는지 확인한다.
  • 수동 commit 모드를 사용중이라면 __consumer_offsets 토픽의 partition, replication, leader 설정이 올바른지 확인한다.
  • 위에서 설명한 total.sh 등 명령으로 특정 topic 및 group의 consuming이 누적되지 않고 즉각적으로 처리되는지 확인한다.
  • Kafka의 garbage collection이 정상적으로 실행되고 있는지 확인한다. 데이터 적체가 발생되면 처리되던 데이터도 처리안된다.
  • 주기적으로 토픽의 leader 및 replication 설정이 적절한지 점검해준다.

대강의 경험을 종합해봤다.

앞으로는 잘 돌아야 하는데 말이다.

https://media1.giphy.com/media/10gHM7SWWMi8hi/200_s.gif

 

Reading for considerations

  • Java G1 GC – http://www.oracle.com/technetwork/articles/java/g1gc-1984535.html
  • Kafka optimization and configuration – http://docs.confluent.io/current/kafka/deployment.html

앞으로 설정할 때 이 부분을 좀 더 파봐야할 것 같다.

 

 

 

좋은 코드에 대한 개인적인 생각 – 2

개발자는 코드를 작성해야한다.  그리고 코드들이 엮이고 엮여 시스템이 만들어진다.  시스템은 필요를 요청한 사용자에게 기능을 제공한다.  물론 시스템을 구성하기 위해 필요한 노력을 개발자만 하는 건 아니다.  인프라 엔지니어는 장비와 네트워크를 준비하고, 데이터베이스 엔지니어는 데이터를 보관할 수 있는 저장소를 준비한다.  최근에는 Data Scientist가 데이터를 분석하고 빅데이터 도구를 통해 적절한 값들을 생성해낸다. 이외에도 다양한 노력들이 합쳐져 시스템이 만들어진다.  이것들을 아우리고 잘 버물려서 하나의 응집된 최종적인 기능으로 만들어내는 역할을 개발자가 담당한다.  그리고 코드는 이것들을 결합시키는 접착제이다.

코드는 여러 재료들을 잘 활용해 작성되야 한다.  어떤 재료의 쓰임이 과해지면 들인 것에 비해 제대로 된 시스템의 맛을 낼 수 없다.  음식을 하다보면 죽은 맛도 살려내는 것이 바로 MSG다.  라면 스프가 그렇고, 다시다가 그렇다.  기막힌 능력이다. 시스템을 만드는 과정에서도 MSG처럼 빠지지 않고 등장하는 재료가 있다. 바로 데이터베이스(DBMS)다.

데이터베이스란 뭔가? 간단히 이야기하면 데이터 저장소이고 시스템을 동작하는데 필요한 데이터를 저장하고 읽을 수 있는 기능을 제공한다. 데이터베이스 가운데 가장 개발자들이 선호도를 가진 것이 아마도 RDBMS이다.  가장 유명한 Oracle, MySQL, MS-SQL 등이 이 범주에 해당한다. 일반적인 경우라면 RDBMS를 빼고는 시스템을 이야기하기 어렵다.

그런데 왜 이걸 시스템 구축하는 MSG라고 이야기했을까?  데이터의 저장소일 뿐인데!  맞다, 모든 데이터베이스가 MSG 역할을 하는 건 물론 아니다. 개발자들이 주로 MSG로 사용하는건 RDBMS이다.  왜? 이유는….

RDBMS는 SQL이라는 언어를 사용해서 데이터를 처리한다.  SQL이라는 언어는 C++나 Java와 마찬가지로 뛰어난 프로그래밍 언어다. 특히 데이터에 직접 접근하면서 이를 우리가 원하는 다양한 조건 혹은 형태로 추출해낼 수 있다는 건 개발자에게 아주 좋은 매력이다. 와중에 언어 자체가 아주 어렵지 않기 때문에 손쉽게 배우고 써 볼 수 있다.

만약 CSV 파일 형태로 데이터가 있다고 가정하자.  만약 즉시 사용할 수 있는 RDBMS가 있다면 이걸 쓰는 것이 가장 빠른 방법이다. 아무리 빠른 시간에 코딩을 한다고 하더라도 Java로 비슷한 코드를 작성하는 건 SQL늘 작성하는 것보다 훨 많은 시간과 노력이 필요하다.  하지만 기본적인 전제는 RDBMS가 있어야 한다는 사실. 하지만 있다면 정말 손쉽게 결과를 이끌어낼 수 있다.

이것처럼 있다면 손쉽게 것도 나이스하게 결과를 만들어낼 수 있다는 관점에서 RDBMS는 개발자에게 MSG다.  빠른 것만이 아니다. MSG가 일류 쉐프의 음식맛에 뒤지지 않는 맛을 만들어내는 것처럼 RDBMS를 뒷배경으로 깔면 좋은 성능의 시스템이 툭 튀어나온다.  많은 데이터를 효과적으로 다루기 위해 멀티 쓰레딩을 구현할 필요도 없고, 데이터 참조등을 위해 Hash Table을 구성할 필요도 없다. 심지어는 굳이 머리를 굴려 알고리즘을 생각해낼 필요도 없다. 이 모든걸 데이터베이스가 처리해준다.  그래서 RDBMS를 사용하는 서비스의 성능의 개떡같다면 DB 튜닝을 하면 완전 새로운 물건이 되기도 한다.  인덱스 혹은 쿼리 힌트(Query hint) 한 스푼이면 죽어가던 시스템이 당장 마라톤 풀 코스라도 뛸 정도의 스테미나를 발휘한다.

데이터베이스, 좋아~

데이터베이스가 이런 물건이라면 개발자들이 더욱 더 많이 써줘야 하지 않을까?  구글링을 통해 찾아낸 아래 SQL을 살펴보자.

from: http://www.apex-at-work.com/2015/03/oracle-sql-calculate-amount-of-workdays.html

 

start_date와 end_date로 주어진 두 날짜 사이에 존재하는 일하는 날을 구하는 쿼리다. 뭐 억지로 발굴해낸 쿼리긴 하지만 SQL의 매력에 빠져드는 개발자라면 이 기능을 SQL 쿼리를 사용해 작성할 수 있다. 흠… 나쁘지 않을 것 같은데???

정~말~ 나쁘지 않다고 생각하면??? 아래 그림을 한번 살펴보고 한번 다시 생각해보는게 좋겠다.

이 그림이 주는 가장 큰 시사점은 뭘까? 바로 장비 대수이다. Application Server는 4대이고, 데이터베이스는 1대이다. 위 쿼리는 극단적인 예이지만, 데이터를 읽어들이기보다는 데이터를 이용한 연산, 심지어 내부적으로 분기문에 해당하는 CASE 구문까지를 사용한다.  이런 계산을 1대 밖에 없는 데이터베이스에 부탁한다면 어떨까?  시키는 일이니까 하긴 하겠지만 데이터베이스라는 친구는 열불이 나서 속이 터져나갈 지경이 될 것이다.  이에 반해 어플리케이션 서버는? 데이터베이스가 결과를 안주니 놀면서 유유자적한 시간을 보낼 수 밖에 없다.

이런 식의 안이한 혹은 편리한 시스템 개발은 결국 병목을 만들어낼 수 밖에 없다. 물론 잘 설계된 DB 구조와 적절한 “어플리케이션 – 데이터 계층”간 역할 분담이 정의된다면 이런 일은 크게 발생하지 않는다.  하지만 데이터는 시스템의 동작하는 가장 바탕이 되고, 이를 가공처리하는 단계 단계를 통해 우리가 제공하는 서비스가 완성된다. 그 과정에서 MSG같은 RDBMS가 있다. 시간에 쫓기는 개발 세상에서 쉽지 않은 유혹이다.

그럼에도 불구하고 우리는 유혹에 맞써야 한다.  어플리케이션 서버가 HTTP/TCP Connection을 받아서 Business Logic에 해당하는 SQL 문장을 DBMS에 던지는 역할만 한다면 이름에서 “어플리케이션” 이라는 단어를 빼야한다.  그렇기 때문에 처리에 필요한 데이터를 불러들여 어플리케이션에서 실행해야할 로직을 합당하게 실행해야 한다.  이 과정에서 DBMS는 효과적인 데이터 관리를, 어플리케이션 서버는 합당한 로직의 완성을 위한 계산과 판단을 수행해야한다.

다른 관점에서 시스템은 횡적 확장(Scale out)이 가능하게 설계되고 개발되야한다. 시작은 미미했으나 그 끝이 창대할려면 말이다. 하지만 RDBMS에 의존한 시스템은 명확한 한계를 갖는다. RDBMS는 높은 성능을 발휘하기 위해서는 Scale Out 방식의 확장 모델보다는 Scale Up 방식이어야 하기 때문이다.. 왜? 당연히 데이터를 다루는 작업을 해야하니까! 어떻게든 많은 데이터를 한꺼번에 처리할려면 많은 메모리가 필요고 또한 인메모리 데이터를 빠르게 소모시킬려면 많은 CPU(Core)가 필요한건 당연한 이야기니까. 오라클의 RAC를 이야기할지 모르겠지만, 어찌됐든 오라클도 개별 장비는 좋아야 한다는 사실에는 변함이 없다.

데이터베이스, 다시 한번 생각해보자!

좀 더 높은 관점에서 우리가 데이터베이스를 어떻게 사용하는지 살펴보자. 시스템은 혼자서는 존재할 수 없으며, 많은 경우에 다른 서비스 시스템과 이야기를 주고 받아야 한다. 서비스간의 Collaboration을 통해 궁극적으로 사용자가 원하는 기능에 도달할 수 있다. 서비스간의 소통을 위해 데이터의 교환은 필수적이다. DBMS는 그런 관점에서 아주 요긴한 도구가 될 수 있다.

아래 그림에서 알 수 있는 바와 같이 서비스들이 하나의 데이터베이스를 공유한다면 RESTful과 같은 번잡한 프로토콜을 통해 데이터를 주고 받는 것을 최소화할 수 있다. 뭐하러 그 많은 데이터를 서로 주고받는가? 데이터가 존재하는 테이블의 스키마를 안다면 기초적인 정보를 가지고도 충분히 원하는 데이터를 획득할 수 있는데… 오직 필요한 건 DBMS에 연결만 하면 된다!

하지만 여기에서도 마찬가지로 성장의 딜레마에 빠진다.  더 많은 서비스들이 하나의 물리적인 데이터베이스로 매개화된다면 결국 이 부분이 병목 구간이 된다.  병목 구간일 뿐만 아니라 치명적인 아킬레스 건이 될 수도 있다.

  • 한 서비스에 공유된 데이터베이스에 과도한 데이터 처리를 유발한다고 가정해보자. 이 부하는 그 서비스만의 문제가 아니다. 궁극적으로는 연결된 모든 서비스들에 영향을 준다. 부하를 유발한 서비스의 입장에서야 자신의 기준에 부합한다고 생각하지만 다른 서비스들은 뭔 죄가 있겠는가? 이 상황에 대한 당장의 타계책은 결국 DBMS를 확장하는 방법 이다. 다른 대안은 궁색하다.
  • 보안등의 이슈로 DBMS 엔진을 업데이트 해야하는 경우가 있다고 하자. 통상적으로 엔진의 업데이트는 데이터베이스 접근을 위한 클라이언트 모듈의 업데이트를 동반한다. 뭔말이냐하면 엔진을 업데이트하고 정상적은 데이터베이스 연결을 위해서는 모든 서비스들에서 동작되는 모든 어플리케이션의 일괄 패치를 의미한다. 국내에서 이런 일괄 패치를 진행하는 시점은 추석이나 설날 같은 명절날이다.  개발자들이 종종 명절임에도 불구하고 고향에 내려가지 못하는 불상사가 발생한다.

데이터베이스에 대한 과도한 의존은 개발자 자신의 문제일 뿐만 아니라 조직 전체에 영향을 줄 수 있는 파급력을 갖는다. 그렇기 때문에 데이터베이스를 대하는 올바른 자세가 개발자들에게 더욱 더 요구된다고 볼 수 있다.

그래서 우리는…

이제 정리를 해볼려고 한다. 먼저 언급할 부분은 “개발자의 관점에서 DB를 어떻게 바라볼 것인가?” 라는 점이다.

RDBMS든 뭐든 데이터베이스는 데이터의 저장소이다. 물론 MSG 역할을 하는 좋은 데이터베이스라면 더욱 좋을 것이다. 하지만 우리가 생각해야할 점은 “데이터를 저장”하는 역할이 데이터베이스라는 사실이다. 그 이상을 바란다면 당신은 개발자가 아니라 DBA가 되야 맞다. 데이터라는 관점에서 RDBMS에 대해 다음의 사항을 고려하면 좋겠다.

  1. 어떤 데이터를 저장할 것인가? 경우에 따라 굳이 데이터베이스에 저장할 필요가 없는 데이터임에도 불구하고 이를 저장해두는 경우가 있다. 예를 들어 1년에 한번 특정한 이벤트에 맞춰 변경하는 1~2k 건수의 데이터들이 있다고 하자. 이 데이터를 굳이 데이터베이스에 저장할 필요가 있을까? 가장 빠른 데이터의 저장소는 어플리케이션의 메모리 영역이다. 그럼 당연히 메모리에 이 데이터를 상주시키는게 맞고, 이를 위한 코드를 작성하면 된다.
  2. 얼마나 간편하게 저장하고 데이터를 로딩할 수 있는가? 데이터는 데이터 자체로 접근하는게 맞다. 앞서 언급한 경우와 마찬가지로 복잡한 Biz Logic을 뒤섞은 SQL 쿼리로 데이터를 로딩한다고 보자.  개발자인 당신이 그 로직을 테스트 케이스를 통해 신뢰성이 있다는 것을 보장할 수 있을까? 당연히 없다. 가장 간단한 형태로 로딩하고 로딩된 결과를 Biz Logic으로 테스트 가능하도록 만들자.  이를 위한 방안으로 JPA와 같은 여러 프레임웍들이 이미 존재한다. 찾아보면 각 언어별로 다 나온다.
  3. iBatis/MyBatis를 사용하더라도 쿼리가 한 페이지를 넘는다면 이미 상당히 꾸리하다라는 증거다. 절대 쿼리는 1페이지 이내에서 결론을 내야한다. 그 이상 넘어가는 쿼리는 이해하기도 힘들뿐만 아니라 읽기도 힘겹다. SQL도 프로그래밍 언어다. 여기에서도 클린 코드를 실현해야하지 않을까?
  4. 마지막으로 원론적인 이야기지만 데이터를 무조건 RDBMS에 저장할려고 하지 마라. 아는게 무서운거라고 알기 때문에 RDBMS에 무조건 넣을려는 경향이 있다. 다양한 통계를 만들겠다고 웹 엑세스 로그를 RDBMS에 넣고 돌리는 행위가 과연 맞는 짓일까?  Transactional Queue를 만들겠다고 그 데이터를 RDBMS에 넣고 넣었다가 뺏다하는 짓이 과연 맞는 짓일까? MongoDB, Elasticsearch, DynamoDB 혹은 심지어 Hadoop도 사용할 수 있다.

 

마이크로서비스 아키텍쳐에서 DBMS의 역할

개발자의 관점뿐만 아니라 이제 데이터베이스의 관점을 아키텍쳐의 관점에서도 새롭게 봐야할 시점이다. 특히나 마이크로서비스 아키텍쳐 환경에서는 더욱 더 데이터베이스의 역할을 어떻게 정의할지를 좀 더 깊게 생각해야 한다.

마이크로서비스 아키텍쳐는 세분화된 서비스가 독립적인 형태가 되는 것을 지향한다.  개별 서비스는 자신과 관련된 자원을 독립적으로 운영하고, 자원에 대한 다른 서비스에 대한 의존성을 없애야 한다.  이유는 간단하다. 그래야만 단위 서비스가 보다 빠르게 움직일 수 있기 때문이다.

앞서 설명한 예시와 같이 서비스간 데이터베이스와 같은 자원이 공유되는 상황에서는 자원에 의한 의존성 문제로 원하는 시점에 원하는 형태로 서비스를 개발하기 어렵다. 특히나 정서상 이미 있는걸 쓰라고 강요하는 경우가 비일비재하다. 이런 간섭이 존재하는 상황에서는 자유로운 데이터 저장소로써의 데이터베이스의 사용이 어렵다.

독립적인 서비스의 개발/배포/운영이 이뤄지는 상황이라면 각 상황에 적합한 데이터 저장소를 활용할 필요가 있다. 마이크로서비스 세상은 이를 권장할 뿐만 아니라 개발자로써 환경에 적합한 물건을 사용해야만 한다. 그래야 최적의 성능을 낼 수 있을 뿐만 아니라 서비스간 유기적인 협업이 가능하니까.

 

개발자가 거버넌스 혹은 좋은게 좋은거니까에 익숙해지면 개발자가 정말 해야할 개발 혹은 코딩을 잊게 된다. 좋은 도구는 활용하면 최선이겠지만, 그에 앞서 자신의 발전을 위해 놓지 말아야 하는 “개념”에 대해서는 항상 생각해보는게 좋겠다.

– 끝 –

 

ps. 글의 처음을 쓴지는 반년이 넘은 것 같은데 완전하지 않지만 가락이 대강 갖춰진 것 같다. 말이 말은 만드는 것 같기도 하고 원래 내가 하고 싶은 말이 이 말이었나 싶기도 하다. 코딩하는 것과 마찬가지로 글도 생각이 있을 때 마무리짓는게 최선인 것 같다.

 

 

 

 

PathVariable에 Slash(/)가 값으로 처리하기

RESTful 방식에서 URI는 Resource에 대한 접근을 어떤 방식으로 허용할지를 결정하는 중요한 요소이다.  당연히 특정 리소스의 구성 요소를 지정하는 방식으로 PathVariable을 사용해야한다. 대부분의 경우에는 별 문제없이 사용할 수 있지만 PathVariable에 특수문자가 들어오는 경우에 예상외의 오류가 발생하는 경우가 있다.  이런 대표적인 특수 문자가 Slash(/)이다.  Slash가 문제가 되는 이유는 짐작하겠지만, 이걸로 인해서 URI의 Path Separation이 발생하기 때문이다.

API Server Application

일반 설정으로는 Springboot 기반의 API 서버에서 Slash가 포함된 값을 PathVariable로 받을 수 없다.  Springboot에서는 기본적으로 /가 포함된 경우, 기본적으로 다음과 같은 정책을 적용한다.

  • Encoding된 Slash가 포함되었다 하더라도, 이를 Decode해서 변수 자체를 세부 Path로 구분해버린다.
  • 중복된 Slash가 존재하면 이를 합쳐서 하나의 Slash로 만들어버린다.

이걸 제대로 원래의 Original Value로 획득하기 위해서는 다음과 같은 옵션이 추가되어야 한다.

@Configuration
@EnableAutoConfiguration
@ComponentScan
@SpringBootApplication
public class ServiceApplication extends WebMvcConfigurerAdapter {
    ...
    public static void main(String[] args) throws Exception {
        System.setProperty("org.apache.tomcat.util.buf.UDecoder.ALLOW_ENCODED_SLASH", "true");
        SpringApplication.run(ServiceApplication.class, args);
    }

    @Override
    public void configurePathMatch(PathMatchConfigurer configurer) {
        UrlPathHelper urlPathHelper = new UrlPathHelper();
        urlPathHelper.setUrlDecode(false);
        urlPathHelper.setAlwaysUseFullPath(true);
        configurer.setUrlPathHelper(urlPathHelper);
    }

코드에서 주의깊게 봐야 할 포인트는 두 가지다.

  • main() 메소드에서 SpringApplication 실행 전 단계에 ALLOW_ENCODE_SLASH 속성 값을 false로 만들어야 한다. Springboot의 Embeded Tomcat이 URI값을 임의로 decode하는 것을 차단하고, 이를 그 값 그대로 Spring 영역으로 보내도록 설정을 잡는다.  이걸 왜 굳이 System property에서 잡아야 하는지 애매하지만 톰캣과 스프링이 그리 친하지 않는걸로 일단 생각한다.
  • WebMvcConfigurerAdapter를 상속받아서 configurePathMatch() 메소드를 Overriding한다.  Overriding 메소드에서 UrlPathHelper 객체를 생성하여 다음의 두가지 속성을 추가로 false로 반영한다.
    • urlDecode – 스프링의 기본 URI filter에서 추가적인 decode를 수행하지 않는다. (그런데 이 옵션이 맞는지는 좀 까리하다. 앞단의 ALLOW_ENCODE_SLASH 만으로 충분한 것 같기도 하고…)
    • alwasyUseFullPath – // 경우에 자동으로 이걸 / 로 치환하도록 하는 규칙을 적용하지 않도록 한다.

이 두가지 설정을 반영하면, 일단 PathVariable을 입력으로 받는데 문제는 없다.

RESTful Endpoint Request

받는걸 살펴봤으니, 이제 보내는 걸 살펴보도록 하자.  PathVariable로 값을 전달하는 경우, 마찬가지로 / 가 들어가면 여러 가지가지 문제를 일으킨다.  가장 간단한 방법은 /가 포함된 값을 URLEncode로 encoding해버리면 될거다… 라고 생각할 수 있다.  하지만 Spring에서 우리가 흔히 사용하는 RestTemplate을 끼고 생각해보면 예상외의 문제점에 봉착한다.

간단히 고생한 걸 정리해보자면…

Get 요청을 하면 되는 것이기 때문에 RestTemplate에서 제공하는 endpoint.get(…)을 사용해 처음 작성을 했었다. 간단한 테스트 케이스에 대해서는 잘 작동을 했지만, 같이 개발하는 친구의 playerId값에는 / 가 포함되어 500 오류를 발생시켰다.

    @Autowire
    RestTemplate endpoint;
    ....

    public SomeResponse queryList(String playerId) {
        ResponseEntity<Log[]> response;
        response = endpoint.getForEntity("http://localhost:8080/api/v1/log/" + playerId, Log[].class);

        Log[] logs = response.getBody();
        ....
    }

위의 코드가 문제 코드인데 보면 아무 생각없이 playerId라는 값을 GET Operation의 path variable의 값으로 넣었다. / 가 없는 경우에는 별 문제가 없지만, 이게 있는 경우에는 “http://localhost:8080/api/v1/log/뭐시기뭐시기/지랄맞을”와 같은 형식이 되버린다. 받는 쪽에서 이걸 제대로 인식할리가 없다.
앞에서 이야기한 것처럼 URLEncoder.encode()를 걸어봤지만, %2F 값을 RestTemplate내에서 %252F로 encoding을 한번 더 해버리는 경우가 발생한다. 뭐 받는쪽에서 한번 더 decoding을 하면 되는거 아냐?? 라고 이야기할 수도 있겠지만, 그건 제대로 된 방법이 아니다.

이래 저래 해결 방법을 찾아봤는데 단순 getForObject, getForEntity의 소스 코드 내용을 확인해봤을 때는 제대로 사용이 어렵고, URITemplate을 사용하는 메소드를 가지고 처리를 해주는게 정답이었다. 다행히 exchange 계열 메소드에서 이걸 지원한다.

public <T> ResponseEntity<T> exchange(String url, HttpMethod method, HttpEntity<?> requestEntity, ParameterizedTypeReference<T> responseType, Object... uriVariables) throws RestClientException {
        Type type = responseType.getType();
        RequestCallback requestCallback = this.httpEntityCallback(requestEntity, type);
        ResponseExtractor<ResponseEntity<T>> responseExtractor = this.responseEntityExtractor(type);
        return (ResponseEntity)this.execute(url, method, requestCallback, responseExtractor, uriVariables);
    }
...
    public <T> T execute(String url, HttpMethod method, RequestCallback requestCallback, ResponseExtractor<T> responseExtractor, Object... urlVariables) throws RestClientException {
        URI expanded = this.getUriTemplateHandler().expand(url, urlVariables);
        return this.doExecute(expanded, method, requestCallback, responseExtractor);
    }
...

RestTemplate의 execute 메소드의 소스 코드에서 포인트는 UriTemplateHandler를 통해 변수를 바인딩한다는 것이다.

        response = endpoint.exchange("http://localhost:8080/api/v1/log/{playerId}", HttpMethod.GET, HttpEntity.EMPTY, Log[].class, playerId);

exchange 메소드는 전달된 parameter를 내부적으로 variable mapping을 한다는 점이다. 그리고 / 를 encoding하게 만들려면 추가적으로 RestTemplate 객체를 생성할 때 강제로 / 를 처리하라고 지정을 해줘야한다. RestTemplate 객체를 생성하는 @Bean 메소드쪽에서 아래와 같이 defaultUrlTemplateHandler 객체를 생성한다.

        RestTemplate template = new RestTemplate(factory);

        DefaultUriTemplateHandler defaultUriTemplateHandler = new DefaultUriTemplateHandler();
        defaultUriTemplateHandler.setParsePath(true);
        template.setUriTemplateHandler(defaultUriTemplateHandler);

setParsePath() 메소드의 값을 true로 설정한 default handler를 RestTemplate의 기본 핸들러로 설정해준다. 기본 설정이 false이기 때문에 / 가 Path variable에 들어가 있다고 하더라도 따로 encoding처리가 되지 않아 문제가 발생했다.

이렇게 설정 및 실행 방법 등등을 변경하고 실행하면… 문제 해결.

ㅇㅋ

이렇게 마무리하면 된다.

Reading note for Summary of Drive

간만에 책을 읽긴 읽었는데, 제대로 읽은건 아니고… 사고보니 이게 Summary 북이네?

Drive: The Surprising Truth About What Motivates Us

Drive: The Surprising Truth About What Motivates Us

Conference에서 Leadership 관련된 세션을 듣다가 이 책은 꼭 읽어야 한다는 이야기를 들었는데, Summary라고 하더라도 왜 그렇게 추천을 했는지 이해가 갈만한 것 같다.

  • Motivation 1.0 – the early operating system(started fifty thousand years ago) which means that we work because we were trying to physically survive and get our basic needs like food, clothing and sex. Biological urges worked well. Until it didn’t. Thus, arrived the revised operating system.
  • Motivation 2.0 – We still have our first drive but we also developed a second drive – to seek reward and avoid punishment. This operating system is still widely used in the society. As a matter of fact, at work employers will reward you if you work harder but will punish if you do not do your job properly.  It served us well. Until it didn’t. Thus, we need another upgrade.
  • Motivation 3.0 – the upgrade that we need for the 21st century to meet the demand on how we organize, think about and do what we do. This operating system presumes that human has the inner drive to explore and work even without the injection of external rewards.

개인적으로 떠들고 다니긴 했지만, 인센티브는 사람을 망치게 한다는 생각에 적극적으로 동의한다.  사람은 막지만 않는다면 뭔가를 해볼려고 하는 내재적 의지를 가지고 있다.  그 동기가 실행이 되도록 해주고, 돈이 아닌 칭찬만으로도 개인의 자존감이 높아진다.  또한 당연히 이런 사람들은 개인적인 성과 뿐만 아니라 조직에도 커다란 도움이 된다.

그럼 잘 하면 보상을 하고, 못하면 열라 갈구는 2.0 버전이 가지는 문제점은 뭘까?

  • They can extinguish intrinsic motivation. – 사람들이 포상이 될 수 있는 일이 아니면 재미있더라도 안하거나 관심을 등한시한다.  그리고 도전적이라 부를만한 일을 하지 않을려고 한다.
  • They can diminish performance. – 예전에 HP, IBM에서 근무하시던 분들이 1년치 쿼터를 계약 한방으로 끝내시더니 1년을 놀 던 기억이 난다. 그 분들 입장에서 추가로 계약을 맺는 수고를 위해 사람들을 만나고 술을 먹어야 할 이유는 없으니까.  그렇다고 굳이 계약을 하지 않을 이유도 없는데 말이다.
  • They can crowd out good behavior.
  • They can encourage cheating, shortcuts, and unethical behavior.
  • They can become addictive.
  • They can foster short-term thinking.

나머지 들은 다 돈에 관련된다.  사람을 황금 만능주의의 중심에 가져다 놓는다. 돈에 중독된 사람들이 돈과 관련된 일에는 별짓을 다해가며 완수할려고 한다.  하지만 돈과 상관없는 일에는? 아무 관심도 없다.  특히 사람을 근시안적인 인간으로 만들어 당장 눈에 보이는 돈에 집착하게 만든다.

그럼 3.0을 하는 사람들은 어떻게 대해줘야하나?

  • Consider nontangible rewards. 친창과 긍정적인 피드백.  말로 하는 칭잔이 금전적인 보상보다 사람안에 내제된 잠재력을 활성화시키는 더 큰 촉진제가 될 수 있다.
  • Provide useful information. 진행하는 일과 관련된 유용한 정보를 제공해주고 이 사람이 나아가는 방향이 올바른 방향으로 나가는지 이야기해라.  그냥 결과만 가지고 이러쿵저러쿵 이야기하는 건 사람의 기를 콱 꺽는다.

3.0 방식으로 잘 돌아갈려면 무엇보다도 사람들이 자율성(Autonomy)를 가지고 스스로 결정해서 일을 진행할 수 있는 재량권이 있어야 한다. 재량권이 필요한 곳을 정리한다면.

  • Their task(what they do)
  • Their time(when they do it)
  • Their technique (how they do it)
  • The team(who they do it with) – 가장 어려운 부분이긴 한데 “누구와 일을 할지 그 사람 스스로 정할 수 있는 환경을 만들어라.” 라는 건데 말이다.  대부분 팀 소속인데 그 안에서 사람을 고른다는게 쉽지는 않은 일이라 이건 패스~

2.0 방식은 규정(compliance)을 두고, 이를 적용하는 방식이다.  기준이 있어야 포상을 할지 벌을 줄 지 결정을 할 수 있으니까! 반면에 3.0 방식은 본인의 헌신(engagement)에 기대하는 바가 크다.  새로운 것에 대한 관심과 도전은 누가 시켜서 하는게 아니다. 본인이 결정해서 회사와 계약된 시간 이외의 시간을 투자해서 그걸 해낼려고 할 수도 있으니까. (그렇다고 야근하라는건 아니고…)  이 과정을 통해 자신의 실력을 올려질 것이다.  이정도로 수련을 하는데 안올라가면 그게 이상한거다.  이것이 Mastery다.

회사에서 진행하는 Role & Mastery라는 것있다. 이 조사가 이런 뒷배경을 깔고 하는 조사는 아니겠지??

 

책의 마지막 장에 나오는 글을 적어두는 것으로 마무리하겠다.

The science shows that the secret to high performance isn’t our biological drive or our reward-and-punishment drive, but our third drive which is fueled more by intrinsic desires to direct our own lives, to extend and expand our abilities, and to live a life of purpose.

– 끝 –

NamedApiEndpoint: 마이크로서비스를 더욱 더 마이크로하게!

마이크로서비스 아키텍처가 개발자에게 주는 가장 좋은 점 가운데 하나는 배포의 자유로움이다.

일반적으로 마이크로서비스를 지향하는 서비스 시스템은 Monolithic 서비스과 대조적으로 제공하는 기능의 개수가 아주 작다.  따라서 고치는 것이 그만큼 훨씬 더 자유롭다.  Jenkins의 Build now 버튼을 누르는데 주저함이 없다고나 할까…

하지만 얼마나 잘게 쪼갤 것인가? 큰 고민거리다. QCon 컨퍼런스에서도 이야기가 있었지만, 최선의 방식은 가능한 작게 쪼개는 것이다.  서비스 시스템이 정의되면 이를 위한 http(s):// 로 시작하는 엔드포인트가 만들어진다. 그리고 다른 서비스 시스템들이 이를 코드(정확하게 이야기하자면 Configuration이겠지만)에 반영해서 사용한다.  이 과정에서 문제가 발생할 가능성이 생긴다.

  • 엔드포인트는 고정된 값이다.  보통 엔드포인트를 VIP 혹은 ELB를 가지고 사용하면 변경될 가능성이 적긴하다.  그렇지만 변경되면(!!) 사건이 된다.  변경에 영향을 받는 시스템들을 변경된 값에 맞춰 작업해줘야 한다. 이렇게 보면 이게 Monolithic 시스템과 뭔 차이인지 하는 의구심마저 든다.
  • 초보자는 이름을 헤메게 된다.  잘게 쪼개진 그 이름들을 다 알 수 있을까?  이런건 어딘가에 정말 잘 정리되어 있어야 찾을까 말까다.  연동할 시스템을 찾아 삼만리를 하다보면 짜증도 나고, 이게 뭐하는 짓인지 의구심마저 들게 된다.  특히나 좀 후진(개발 혹은 QA) 시스템들은 DNS를 등록해서 사용하지도 않기 때문에 더욱 난맥상을 빠질 수 있다.

이런 고민을 놓과 봤을 때 가장 합리적인 결론은 “정리된 목록“이다. 언제나 승리자는 정리를 잘 하는 사람이다. 하지만 시스템이 사람도 아니고… 목록이 있다고 읽을 수 있는건 아니지 않은가???

읽을 수 있다. 물론 그 목록이 기계가 읽을 수 있는 포맷이라면!!! 읽을 수 있다는 사실은 중요하다.  이제 사람이 알아들을 수 있는 “말”을 가지고 기계(시스템)를 다룰 수 있을 것 같다.

http://pds26.egloos.com/pds/201404/12/99/c0109099_5348f03fbc7cd.jpg

가능할 것 같으니 해봐야지!  NamedApiEndpoint라는 Github 프로젝트로 해봤는데, 나름 잘 동작하는 것 같다.

https://github.com/tony-riot/api-discoverous

  • 동작하는 건 앞서 이야기한 것처럼 서비스 시스템의 엔드포인트를 이름으로 참조한다.
  • 이름 참조를 위해서는 { name, endpoint } 쌍을 보관하는 별도의 Repository가 물론 있어야 한다.
  • NamedApiEndpoint에서는 스프링 프레임워크(Spring framework)를 바탕한다.
  • 어플리케이션 시작 시점에 이름을 기초로 엔드포인트를 쿼리한다.
  • 쿼리된 엔드포인트 정보와 URI 정보를 이용해 Full URL을 구성해서 RestTemplate와 동등한 Operation set을 제공한다.

Repository는 별도 시스템으로 만들수도 있지만, Serverless 환경으로 이를 구성시킬 수도 있다.  내부적으로는 AWS DynamoDB를 API G/W를 연동해서 Repository 서비스로 만들었다.  이 부분에 대한 내용은 다른 포스팅에서 좀 더 다루겠다.  분명한 사실은 재미있다라는 거!

빙빙 돌려 이야기를 했지만 Service discoverous라는 개념이다.  관련해서 정리된 글을 링크해본다.  기본적으로 Discovery와 Registry의 개념을 어떤 방식으로 구현하는지에 따라 달리겠지만 Spring 혹은 Springboot를 주로 많이 사용하는 환경에서 사용하기에 큰 문제는 없는 것 같다.  굳이 Well-known 방식을 선호한다면 링크한 페이지를 잘 체크해보면 되겠다.

– 끝 –

QCon 컨퍼런스를 다녀와서

3월 6일부터 8일까지 3일 동안 영국 런던에서 있었던 QCon 컨퍼런스에 다녀왔다.

컨퍼런스라는걸 다녀볼 수 있는 기회가 없었다.  국내 컨퍼런스는 몇번인가 다녀봤지만 기대했던 것에 비해서 얻는게 너무 없었다.  그렇다고 해외 컨퍼런스를 회사 다니면서 가볼 기회가 쉽게 주어지지 않았다.  사실 기회가 한번인가 있긴 했지만 “치사하고 더러워서” 포기하고 말았었다.  회사를 이직 한 후 작년 4월에 O’Reilly Architecture 컨퍼런스에 참석할 기회가 생겨서 다녀왔었는데, 정말 신세계가 내 앞에 펼쳐졌다.  말로야 아키텍쳐가 거기서 거긴데 뭘 배울게 있겠나 싶었지만 정말 정말 값진 시간을 가졌다.  덕분에 마이크로서비스 아키텍쳐 기반으로 시스템을 개발하는데 많은 도움이 됐다.  이 컨퍼런스가 아니었다면 아마 지금까지도 거지발싸게 같이 시스템을 구현하고 있지 않았을까 싶다.

바쁘게 개발 작업을 하면서 “마이크로서비스 아키텍처 방식으로 개발한다” 라는 것을 올바르게 실행하고 있는지 의문이 들었다.

  • 잘게 쪼개서 RESTful 방식으로 소통하도록 시스템을 개발하는것이 맞긴 한데 그럼 어느 정도 수준으로 쪼개는게 맞는거지?
  • 그리고 이렇게 많아지는 시스템들을 어떻게 관리하는게 좋은거지?
  • 개별 시스템들이 RESTful이라는 그물망으로 엮여지게 된다.  그 망 가운데 하나라도 고장이 난다면 결국 전체 시스템의 피해가 날 수 밖에 없는 상황 아닌가?  따지고 보면 개발의 자유도는 올라가지만 시스템적인 측면에서 본다면 기존 방식(Monolithic)과 차이점이 없는거 아닌가?

이런 질문들을 마음에 품고 있었는데, 이번 QCon 컨퍼런스에서 이많은 궁금증을 해소할 수 있었다.

QCon 컨퍼런스는 InfoQ.com 에서 진행하는 개발 컨퍼런스다.  개발에 관련된 최근 핫한 혹은 이슈가 되는 주제들과 개발 조직, 문화 그리고 코딩에 대해 이야기한다.  학술 컨퍼런스와 달리 대부분 발표하는 사람들이 실무에서 개발을 하고 있거나 개발에 직접적으로 연관되어 있다.  중요한 포인트는 간접적으로 연관된게 아니다라는 사실이다!  그리고 전세계를 돌면서 각 지역의 특색을 반영해서 컨퍼런스가 진행된다.  런던의 경우는 금융 산업이 발전되어 있는 덕분인지 금융권쪽의 몇 가지 구현 사례들이 소개되어 나름 흥미로웠다.  역시나 우리 나라 금융권 부장/과장/대리/사원 개발자들과는 달랐다.  그들도 양복을 입지 않고 청바지에 티셔츠를 입는다.

여러 주제에 관련된 트랙이 3일 동안 진행됐지만 내가 주로 관심있게 본 트랙은 주로 아키텍쳐, 코딩, 엔지니어링 문화에 관련된 부분들이었다.  앞에 짧게 언급했지만 그동안 개발을 하면서 이게 맞나?? 하는 의문점들의 해소가 급선무였다.  그리고 내가 코드짜는게 과연 올바른 코드인지 그리고 팀의 문화를 어떻게 발전시키면 좋을지에 대해서도 새로운 돌파구가 필요하다고 생각했다.

마이크로서비스 아키텍쳐와 관련해서 주로 사람들이 이야기하는 키워드는 “Resilient, Distributed, Reactive“에 집중됐다.  기존 시스템의 문제점을 이야기하고, 문제점을 해결함과 동시에 키워드에서 제시된 기술적인 목표들을 달성하기 위해 어떤 방식으로 접근했는지를 설명했다.  흥미로웠던 점은 거의 대부분의 회사들이 Kafka를 기반으로 Messaging 시스템을 구축했고, 이를 통해 이벤트 중심(즉 비동기)의 분산 처리 방식으로 시스템을 구축하고 있다는 사실이다.  분산 환경 및 세분화된 마이크로서비스 환경을 관리하기 위한 방안으로 Service discovery, Integrated logging, Integrated monitoring 체계를 제시하고 구현하고 있었다.  이심전심으로 환경에 대한 관리 부담을 크게 인식하고 있었고, “통합(Integrated)” 개념을 관리 측면에 도입해 살아남으려고 했다.  나도 살아남아야겠다!!

업무의 가시성과 관련된 세션도 있었는데, 흥미로운 점은 이제 다들 에자일 혹은 XP 이야기를 굳이 세션에서 꺼내지 않는다는 점이다.  어떤 세션에서는 강연자가 “나는 에자일을 싫어한다” 라고 이야기를 해서 객석에 있는 사람들로부터 환호성을 받기도 했다. “이건 뭐지???” 라는 생각이 휙 머리를 스쳤다.  하지만 그 친구가 이야기하는 것 자체가 이미 에자일스러웠다.  반어적인 것 같지만 그만큼 유연하게 업무를 진행하는 방식은 이미 일상화가 됐다고 보여진다.  청중의 환호는 강제적인 에자일 “프로세스의 실행 강요”에 대한 이질감의 표현이지 않을까 싶었다.  자꾸 틀이라는 것을 강조하는 구태적인 사고는 필요없을 것 같다.  자유로운 개발을 위해 필요한 것들만 취하면되지 형식이 있다고 굳이 거기에 몸을 맞출 필요는 없다.

프로그래밍 언어적인 측면에서 자바는 계속 살아남기 위해 분투중이었다.  하지만 마이크로서비스 분산 환경에서의 대세는 아마도 Go 언어인 것 같다.  어떤 언어가 앞으로 세상을 주름잡을 것인가에 대한 토론에서 앞도적으로 언급된 언어적인 특성은 Concurrency에 대한 지원이었다.  그리고 그걸 가장 대표적으로 잘 하고 있는 언어가 바로 Go라고 이야기를 했다.  심지어 마이크로소프트 프로그래밍언어/컴파일러 전 임원이!  앞으로 더 재미있게 코딩을 할려면 Go 언어를 배워둬야겠다.

혼자 갔기 때문에 내 궁금증 해소가 우선이라 관련된 세션들을 집중적으로 들었다. 하지만 같은 시간에 진행되는 재미있는 다른 세션들도 있어서 무척 아쉬움이 남는다.  한 사람이라도 같이 갔다면 나눠서 듣고, 다른 세션에서는 어떤 재미있는 이야기들이 있었는지 맥주 한잔하면서 토론할 수 있었을텐데…  지난번 컨퍼런스에도 느낀 점이긴 했지만 이번에도 비슷한 감정을 또 한번 더 느꼈다.

 

전체 이야기들을 다 풀어낼 수는 없고, 짧은 글 솜씨 이야기로 해봐야 전달되기도 어렵다. 각 세션들에 대한 동영상이 있으니까 그 안에서 관심가는것들을 챙겨보면 좋을 것 같다.

https://www.infoq.com/conferences/qconlondon2017

물론 동영상으로 보는 것보다는 현장에서 직접 보고 듣는 것들이 훨씬 더 많이 남는다.  궁금증에 대한 답을 발견하고, 그걸 Case Study로 설명까지 듣고 있을 때는 여러 감정이 복잡하다.

  • 해결하지 못한 것에 대한 자책감도 들고,
  • 당장 우리쪽에 적용하기 위한 코드를 작성하고 싶다는 열망도 들고,
  • 까먹기 전에 빨리 사람들에게 공유해야겠다라는 절박감도 들고…

개발자라면 꼭 이런 곳을 찾아다니면 좋겠다. 가장 먼저는 비용이 문제고, 장시간 타야하는 비행기도 문제기도 하지만 정말 얻을 게 많다.  그런 관점에서 현재 직장인 라이엇 게임즈는 정말 좋은 회사다.  그리고 갈 수 있도록 배려해준 동료들에게 감사하고, 출장을 승인해주신 디렉터님도 정말 훌륭하시다!!!

쪼금 아쉬운 점은 아침 8시부터 저녁 7시까지 꼬박 진행되는 일정 때문에 어디 다니지를 못했다는 거.  하지만 즐거운 여행은 언제라도 가능하지만, 이번 기회와 같은 배움의 기회, 배움의 즐거움은 다시 살 수 없기에 정말 재미있는 시간이었다.

이런 배움을 쌓아서 기회가 된다면 나도 QCon 이나 O’Reilly 같은 컨퍼런스에 발표자로 도전해보고 싶다.

 

좋은 코드에 대한 개인적인 생각 – 1

사람들과 코드 리뷰를 하거나 면접을 보거나 하면서 다양한 코드를 접한다. 좋은 코드도 많이 봤다.  하지만 그보다 더 많은 나쁜 코드들도 봤다.  더구나 그런 코드들을 작성하는 분들이 경력 10년차 이상이라는 사실이 더 사람을 참담하게 만들었다.

경력이 비래해서 공통적으로 IT, 개발 사상에 대한 나름의 기준을 정립한 분들이다.  아마도 다른 곳에서는 본인이 다른 사람을 리딩하는 역할도 하고, 멘토링도 할 것이다.  하지만 코드가 그 모양인데 이 분들이 하는 리딩, 멘토링이 과연 맞는 것일까?  QCon 컨퍼런스에서 들은 말 가운데 “코드를 작성할 줄 모르는 아키텍트의 말은 사기다!” 라는 말이 정말 와 닿았다.

그러므로 코딩을 잘 해야한다.  하지만 어떻게 작성하는 코딩이 좋은 것인지 나름의 생각이 있을 것이다.

코드도 넓은 의미에서 글쓰기의 연장선이라고 생각한다.  그만큼 코드에는 작가(프로그래머)의 심미성이 반영될 수 있다.  그리고 기호에 따라 그 맛이 달라진다.  하지만 모든 글쓰기에 기본이 있듯이 코딩에서 기본이라는 것이 있다.  읽을 수 없는 글, 읽어도 뭔 이야기인지 알아들을 수 없는 글이 있는 것처럼 코딩에도 그런 쓰레기들이 존재한다.  쓰레기 코드라는 이야기를 듣지 않으려면 어떻게 해야할까?  글쓰기와 마찬가지로 코드를 읽고 작성해봐야 한다.

좋은 코드에 대한 예를 내 기준으로 기록해보고자 한다.  물론 이견이 있을 수 있고, 더 좋은 의견이 있을 수도 있다.  좋은 코딩에 대한 토론이 이어진다면 더할 나위 없을 것 같다.  하지만 “좋은 코드”에 대해 굳이 동의를 바리지 않는다.  다만 내가 기록해두고 앞으로 그 이상의 코딩을 할 수 있길 바랄 뿐이다.

몇 번까지 번호를 붙힐 수 있을지는 모르겠다.  생각나고 짬이 나는대로 써 볼 뿐이다.


먼저 간단히 다음의 코드를 살펴보자.

if (val &lt; 13) { // for kids
  ...
} else if (13 &lt;= val &amp;&amp; val &lt;= 18) { // for youth
  ...
} else { // for adult
  ...
}

잘못된 점을 탓하기전에 잘된점을 짚어보자.  얼핏보니 코드에 적절한 코멘트를 잘 사용한 것 같다?  확신이 들지는 않지만 그게 다인 것 같다.

그럼 잘못된 점들을 까보자!

  • 변수명이 개떡같다. 코멘트로 봐서 val 이라는 변수는 나이를 뜻한다. 그럼 당연히 변수 이름이 age가 되어야 한다.
  • 13, 18 이라는 숫자는 마찬가지로 나이를 뜻한다.  근데 뭘 의미하는 나이지? 의미를 알 수 없다.
  • if ~ else if ~ else 를 통해 하나의 코드 흐름에 3가지 분기를 치고 있다.

앞 선 두개의 지적은 초보적인 문제다.  바로 고칠 수 있다.

이 가운데 가장 잘못된 부분은 하나의 코드 흐름을 3개의 서로 다른 블럭으로 나뉜 부분이다.  다른 것들은 적절히 수정하면 금방 바로잡을 수 있지만 이 3가지 분기는 확실히 코드 읽기를 방해한다.  방해할 뿐만 아니라 이 코드를 그대로 방치하면 추가적으로 적용될 코드들도 마찬가지로 오염시킬 것이다.  그렇기 때문에 가장 큰 문제점이다.

디자인 패턴을 아는 사람이라면 제대로 고치는건 간단하다.

 

이 구조를 채택하면 위의 코드 구조를 아래와 같은 방식으로 변경할 수 있다.

...
Builder builder = new Builder();
ActionExecutor executor = builder.executor(age);
executor.execute();
...

앞에서 봤던 것과 같은 한 코드 영역에서 if .. else if .. else 와 같은 복잡 다단한 구조를 없앴다.  이에 대한 복잡도를 Builder -> Factory -> Executor 이어지는 연계 구조를 활용해서 깔끔하게 정리했다.  이런 방식이 주는 이점은 단순히 코드를 깔끔하게 만드는 것 이상의 의미가 있다.

  • 코드 읽기를 하나의 흐름으로 맞췄다.  읽는 과정에서 여기저기 눈을 움직일 필요없이 위에서 아래로 쭉 읽으면 된다.
  • 단위 테스트가 쉬워졌다.  이전 코드 구조에서 단위 테스트를 할려면 앞 전에 대한 조건등을 다 맞춰줘야 각 if .. else if .. else 사이 블럭에 대한 동작을 테스트할 수 있었다.  변경된 체계에서는 Executor 위주로 각 Executor가 정의된 동작을 하는지만 살펴보면 된다.  깔끔한 테스트를 만들어서 오류 자체를 효과적으로 제거할 수 있다.
  • 원래의 코드를 Mocking을 이용해 보다 다양한 상황에 대한 테스트 케이스를 보완할 수 있다.  실제 객체를 Injection하는 것보다는 Factory, Executor 인터페이스들을 Mocking으로 Injection하면 다양한 경우에 대한 테스트 케이스를 보다 쉽게 만들 수 있다.

혹자는 별것도 아닌 코드에 Factory니 Builder니 하는 계층을 이야기하는건 배보다 배꼽이 더 큰 이야기가 아니냐고 이야기할 지도 모른다.  하지만 한번 쓰고 버릴 코드가 아니라면 해야한다.  개인적으로는 한번 쓰고 버릴 코드도 해야한다고 주장한다.  코드를 작성하는 것도 습관이다.  그리고 잘못된 코딩 습관만큼 고치기 힘든 버릇도 없는 것 같다.

경우에 따라 빠르게 진행해야하는 코딩의 경우 Technical debt(s)를 감수하고 진행해야한다. 하지만 반드시 뒤따라 Refactoring이 이어져야한다.  하지만 한번 돌고 나면 만족하게 마련이다.  인간이라는게 이기적인 동물이기 때문이다. 목표를 달성하면 그 이후의 뒤정리는 그 이기심을 넘어서는 열정이 있어야 하는데 말이다. 그런 사람이 대부분이라고 생각하지 않는다.

따라서 대부분의 경우에도 이런 식으로 코딩해야한다.

– 끝 –

SpringBoot 1.4 기반의 Integration Test 작성하기

기본적인 내용은 Spring 블로그에 포스팅된 내용을 바탕으로 한다.   한글로 읽기 귀찮다면 링크된 본문을 참고하자!!

 

스프링 부트는 복잡한 설정없이 손쉽게 웹 어플리케이션 서버를 실행할 수 있다는 점때문에 자바 언어 세계에서 널리 사용되어오고 있다.  부트 역시 스프링 프레임워크에서 지원했던 방식과 유사한 형식의 테스트 방식을 지원하고 있었다.  하지만 부트의 테스트는 부트 자체가 웹 어플리케이션 개발을 쉽게 했던 만큼은 더 나아가지 못했다.

가장 간단한 테스트를 작성하는 방법은 물론 스프링을 배제한 형태다.  스프링의 각종 @(어노테이션 – Annotation)을 벗어난 코드라면 이 방식의 테스트가 가장 적절하다.  사람들이 오해하는 것 가운데 하나는 @Component 혹은 @Service라고 어노테이션이 붙은 클래스를 테스트할 때 꼭 스프링을 끼고 해야한다고 생각한다는 점이다.

굳이 그럴 필요가 없다.

  • 객체는 그냥 new 를 이용해서 만들면 된다.
  • @Value 어노테이트된 값은 그냥 값을 셋팅하면 된다.
  • 테스트 대상 메소드는 대부분 public 키워드를 갖는다. 테스트 메소드에서 접근을 걱정할 일이 거의 없다.
  • 외부에 공개되지 않은 메소드를 테스트해야하거나 아니면 @Value 값을 강제로 설정해야하는 경우라면 아예 테스트 대상 클래스를 상속받는 클래스를 클래스를 테스트 패키지에 하나 만든다.  그럼 원형을 해칠 필요도 없고, 딱 테스트에 적합한 형태로 맘대로 가지고 놀 수 있다.

 

하지만 스프링과 엮인 부분들이 많다면 테스트에 스프링을 끼지 않을 수 없다. 난감하다. 스프링과 함께 테스트를 돌릴 때 가장 난감한 점은 테스트 실행 시간이 꽤 든다는 점이다.  특히 매 테스트마다 어플리케이션 전체가 올라갔다가 내려갔다를 반복된다.  단위 테스트라는 말을 쓸 수 있을까?  그래서 그런지 통합 테스트(Integration Test)라고 이야기하는 경우가 많다.  이런 것이 싫었던 것도 있어서 정말 Integration Test가 아니면 웬만하니 테스트를 잘 작성하지 않았다.

이러던 것이 스프링부트 1.4 버전을 기점으로 좀 더 쉬운 형태로 테스트 작성 방법이 바뀐 사실을 알게 됐다.  예전게 못먹을 거라고 생각이 들었던 반면에 이제는 좀 씹을만한 음식이 된 것 같다.  사실 부트 버전을 1.4로 올린 다음에 테스트를 돌려볼려고 하니 Deprecated warning이 이전 테스트에서 뜨길래 알았다.  그냥 쌩까고 해도 별 문제가 없을 것을 왜 이걸 굳이 봤을까…

이미 봐버렸으니 역시나 제대로 쓸 수 있도록 해야하지 않을까?  이제부터 언급하는 스프링 테스트는 부트 1.4 바탕의 테스트 이야기다.

@SpringBootApplication
public class Application {
   public static void main(String[] args) throws Exception {
      SpringApplication.run(IPDetectionApplication.class, args);
   }
}

테스트를 작성할려면 가장 먼저 어플리케이션 자체의 선언이 @SpringBootApplication 어노테이션으로 정의되야만 한다.  부트 어플리케이션이 되는 방법은 이 방법말고 여러 방법이 있지만 1.4의 테스트는 꼭 이걸 요구한다. 안달아주면 RuntimeException을 낸다.  강압적이지만 어쩔 수 없다.

환경을 갖췄으니 이제 테스트를 이야기하자.  가장 간단히 테스트를 돌리는 방법은 테스트 클래스에 다음 두개의 어노테이션을 붙혀주면 된다.

@RunWith(SpringRunner.class)

@SpringBootTest

이 두 줄이면 스프링 관련 속성이 있는 어떤 클래스라도 아래 코드처럼 테스트할 수 있다.

@RunWith(SpringRunner.class)
@SpringBootTest
@Log4j
public class SummonerCoreApiTest {
    @Autowired
    private SummonerCoreApi summonerApi;

    @Test
    public void shouldSummonerApiLookUpAnAccountWithGivenAccountId() {

다 좋은데 이 방식의 문제점은 실제 스프링 어플리케이션이 실행된다는 점이다.  물론 테스트 메소드(should…)가 끝나면 어플리케이션도 종료된다.  @SpringBootTest 라는 어노테이션이 주는 마력(!!)이다.  만약 Stage 빌드를 따르고 있다면 테스트를 위한 환경을 별도로 가질 수 있다.  메이븐을 이용해서 환경을 구분하는 경우에는 별 문제가 없지만 만약 스프링 프로파일(Spring profile)을 이용하고 있다면 별도로 지정을 해줘야 한다.  @ActiveProfiles(“local“) 어노테이션을 활용하면 쉽게 해당 환경을 지정할 수 있으니 쫄지 말자.

OMG

어떤 클래스라도 다 테스트를 할 수 있다!!  몇 번에 걸쳐 이야기하지만 어플리케이션 실행에 관련된 모든게 다 올라와야하기 때문에 시간이 오래 걸린다.  좀 더 현실적인 타협안이 있을까?

일반적으로 Integration Test는 RESTful 기반 어플리케이션의 특정 URL을 테스트한다.  따라서 테스트 대상이 아닌 다른 요소들이 메모리에 올라와서 실행 시간을 굳이 느리게 할 필요는 없다.  타협안으로 제공되는 기능이 @WebMvcTest 어노테이션이다.  어노테이션의 인자로 테스트 대상 Controller/Service/Component 클래스를 명시한다.  그럼 해당 클래스로부터 참조되는 클래스들 및 스프링의 의존성 부분들이 반영되어 아래 테스트처럼 실행된다.

@RunWith(SpringRunner.class)
@WebMvcTest(AccountInfoController.class)
@Log4j
public class AccountInfoControllerTest {
    @Autowired
    private MockMvc mvc;

    @Test
    public void shouldControllerRespondsJsonForTheGivenUsername() throws Exception {
        final String givenUsername = "chidoo";

        GIVEN: {
        }

        final ResultActions actualResult;
        WHEN: {
            actualResult = mvc.perform(get("/api/v1/account/name/" + givenUsername)
                    .contentType(MediaType.APPLICATION_JSON_UTF8)
                    .accept(MediaType.APPLICATION_JSON_UTF8));
        }

        THEN: {
            assertThat(actualResult.getPlayerId(), is(...));
        }
    }
    ...

이 테스트는 Integration Test이다.  역시나 Integration Test는 비용이 많이 들고, 테스트 자체가 제대로 돌도록 만드는 것 자체가 힘들다.  DB를 연동하면 DB에 대한 값도 설정을 맞춰야하고, 외부 시스템이랑 연동을 한다면 것도 또 챙겨야한다.

포기할 수 없다. 이걸 단위 테스트할려면 어떻게 해야하는거지?  Mocking을 이용하면 된다!!!  테스트 대상 코드에서 “스프링 관련성이 있다.“는 것의 대표는 바로 @Autowire에 의해 클래스 내부에 Injection되는 요소들이다.   이런 요소들이 DB가 되고, 위부 시스템이 된다.  해당 부분을 아래 코드처럼 Mocking 방법을 알아보자.

  • 테스트 대상 코드의 내부 Injecting 요소를 @MockBean이라는 요소로 선언한다.  이러면 대상에서 Autowired 될 객체들이 Mocking 객체로 만들어져 테스트 대상 클래스에 반영된다.
  • 이 요소들에 대한 호출 부위를 BDDMockito 클래스에서 제공하는 정적 메소드를 활용해서 Mocking을 해준다.
  • given/willReturn/willThrow 등 과 같이 일반적인 Mockito 수준에서 활용했던 코드들을 모두 활용할 수 있다.

그럼 명확하게 클래스의 동작 상황을 원하는 수준까지 시뮬레이션할 수 있다.

@RunWith(SpringRunner.class)
@WebMvcTest(AccountInfoController.class)
@Log4j
public class AccountInfoControllerTest {
    private JacksonTester<AccountInfo> responseJson;
    private JacksonTester<AccountInfo[]> responseJsonByPlayerId;

    @Autowired
    private MockMvc mvc;

    @MockBean
    private AccountInfoService accountInfoService;

    @Before
    public void setup() {
        JacksonTester.initFields(this, new ObjectMapper());
    }

    @Test
    public void shouldControllerRespondsJsonForTheGivenUsername() throws Exception {
        final String givenUsername = "chidoo";
        final AccountInfo expectedAccountInfo;

        GIVEN: {
            expectedAccountInfo = new AccountInfo("dontCareAccountId", givenUsername, "dontCarePlayerId");
            given(accountInfoService.queryByUsername(givenUsername)).willReturn(expectedAccountInfo);
        }

        final ResultActions actualResult;
        WHEN: {
            actualResult = mvc.perform(get("/api/v1/account/name/" + givenUsername)
                    .contentType(MediaType.APPLICATION_JSON_UTF8)
                    .accept(MediaType.APPLICATION_JSON_UTF8));
        }

        THEN: {
            actualResult.andExpect(status().isOk())
                    .andExpect(content().string(responseJson.write(expectedAccountInfo).getJson()));
        }
    }
    ...

특정 서비스를 테스트하는데 전체를 다 로딩하지 않고, 관련된 부분 모듈들만 테스트 하고 싶은 경우에는 아래 코드를 참고한다.

@RunWith(SpringRunner.class)
@SpringBootTest(classes = { SomeService.class, InterfacingApi.class, CoreApi.class})
@Log4j
public class SomeServiceTest {
    @Autowired
    private SomeService service;

   @Test
    public void shouldServiceQueryAccount() {
        final long accountIdAsRiotPlatform = 1000l;
        GIVEN: {}

        final Account account;
        WHEN: {
            account = service.lookup(accountIdAsRiotPlatform);
        }

        THEN: {
            assertThat(account.getAccountId(), is(accountIdAsRiotPlatform));
        }
    }
}

만약 Controller를 메소드를 직접 호출하는게 아니라 MockMvc를 사용해 호출하는 경우라면 다음 2개의 어노테이션을 추가하면 된다. 이러면 테스트할 대상 하위 클래스를 지정할 수 있을뿐만 아니라 MockMvc를 활용해서 Controller를 통해 값을 제대로 받아올 수 있는지 여부도 명시적으로 확인할 수 있다.

@AutoConfigureMockMvc
@AutoConfigureWebMvc

Swagger 관련 오류에 대응하는 방법

JPA 관련된 테스트 혹은 WebMvcTest를 작성하는 과정에서 아래와 같은 Exception을 만나는 경우가 있다.

Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'java.util.List<org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping>' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.raiseNoMatchingBeanFound(DefaultListableBeanFactory.java:1466)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1097)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1059)
	at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:835)
	at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:741)
	... 58 more

별거 한거 없이 정석대로 진행을 했는데 이런 뜬금없는 오류가 발생하는 상황에서는 프로젝트에 Swagger 관련된 설정이 있는지 확인해봐야한다. Swagger에 대한 Configuration에서 다른 설정을 해주지 않았다면 스프링부트 테스트는 Swagger와 관련된 Bean 객체들도 실행시킬려고 한다. 이걸 회피하기 위해서는 다음의 두가지 설정을 Swagger쪽과 문제가 되는 테스트쪽에 설정해줘야 한다.

Swagger 설정

@Configuration
@EnableSwagger2
@Profile({"default", "local", "dev", "qa", "prod"})
public class SwaggerConfiguration {
...
}

스프링부트가 기본으로 스프링 프로파일을 사용한다. 본인이 스프링 프로파일을 사용하든 안하든… 위의 케이스처럼 스프링이 적용될 환경을 명시적으로 정의해둔다. 대부분의 환경이 위의 5가지 환경으로 분류되고, 별도로 명시하지 않으면 default가 기본 프로파일로 잡힌다. 물론 테스트 케이스를 실행하는 경우에도 명시적으로 해주지 않으면 default 프로파일이 적용된다. 때문에 이제 테스트가 이를 회피하기 위한 프로파일을 명시해준다.

테스트 클래스 설정

@RunWith(SpringRunner.class)
@DataJpaTest
@ActiveProfiles("test")
@Log4j
public class AccountInfoServiceTest {
...
}

이렇게 실행되면 테스트가 실행될 때 test라는 프로파일을 가지기 때문에 Swagger 관련 모듈들을 로딩하지 않고, 테스트가 실행된다.

일단 여기까지 정리해봤다.

Kafka를 이용한 메시징 시스템 구성하기

최근의 개발 경향은 확실히 마이크로서비스를 지향한다.  가능하면 작은 어플리케이션을 만든다.  그리고 이 어플리케이션들의 소위 콜라보(Collaboration)로 하나의 시스템이 만들어진다.  혹은 만들어지게 구성을 한다.  이와 같은 마이크로서비스 모델이 주는 이점은 나도 몇 번 이야기를 했고, 많은 사람들이 장점에 대해서 구구절절하게 이야기하기 때문에 말을 더 하지는 않겠다.

????여기에서 급질문!!  작은 어플리케이션… 근데 작은 어플리케이션을 지향하는 마이크로서비스의 문제점은 없을까?

먼저 생각나는 첫번째 문제는 작게 만든다고 했으니까 반대급부로 “어플리케이션의 수가 많아진다.” 아닐까 싶다.  지극히 당연한 지적이다. 여기서부터 문제들의 가지를 뻣어나가보자.  뭔가 새로운 걸 만든다면 그럼 이 많은 것들 가운데 연동해야 할 것들이 또한 많다는 것을 의미한다.  잘 외우고 있던지 아니면 잘 적어놔야한다.

많은 대상들과 서로 정보를 주고 받으면 새로운 시스템이 동작해야한다.  대상이 많아지면서 우리는 돌이켜봐야한다.  어울려 일하는 대상과 어떤 방식으로 일을 해야하는지.  일부 처리는 인과 관계가 존재하기 때문에 순서에 맞게 일이 되야 한다.  하지만 특정 작업은 다른 일과 아무 관련성이 없는 경우에는?  개발자의 세상에서 이 상황은 “관련이 없는 이 일과 저 일을 한꺼번에 해도 된다는 것“을 의미한다.  어플리케이션 수준에서 동시에 뭔가를 함께 실행되도록 만드는 건… 생각보다 어렵다.

아래와 같은 통상적인 A, B, C, D, E라는 어플리케이션에 의해 구성된 마이크로서비스를 보자.  A 어플리케이션은 “B → C → E” 순서로 다른 어플리케이션과 연동한다.  물론 B, C, E 어플리케이션들도 나름 열심히 연동한다.  이 와중에 D 어플리케이션이 고장되면 어떻게 될까?  A 어플리케이션은 과연 문제가 D 때문에 본인의 인생이 꼬인 걸 알 수 있을까?

MicroserviceModel

 

A는 B 혹은 C 혹은 E의 문제라고 알 것이다.  당연히 A가 직접 연동하는 대상에 D는 포함되어 있지 않기 때문이다.  책임은 전가되고 긴가민가 하는 상황이 길어지면 해결은 안되고 서로 비난하는 상황만 연출된다.  D 어플리케이션은 그 와중에 자신이 문제의 핵심이라는 사실을 알지 못할 수도 있다.  이 혼돈은 “문제의 본질을 알지 못한다.“가 최종적인 결론이다.

마이크로서비스 구조가 활발해질수록 당면하게 되는 이슈를 다음의 3가지 어려운 말로 정리해본다.

  • 소규모 어플리케이션의 증가로 인한 연동 구조의 복잡성이 증가하고 이로 인한 관리 비용이 증가한다.
  • 어플리케이션의 구현 로직이 필연적으로 동기화될 수 밖에 없으며, 상호 독립적인 어플리케이션들의 직렬 실행이 전반적인 성능 저하를 유발시킨다.
  • 다중 계층에 의한 연동 구조는 문제 요인에 대한 즉각적인 발견을 어렵게 하며, 핵심 요소의 장애 발생시 시스템 수준의 재앙을 초래할 수 있다.

이렇게 열거해놓고 보니 아무리 마이크로서비스 구조가 좋다고 이야기를 해봐도 연동 계층이 많아질수록 벗어나고 싶던 Monolythic 시스템과 별반 차이없는 괴물이 되어버린다.  Monolythic 구조의 문제점을 해결하는데 적절한 대안으로 마이크로서비스 구조가 딱이긴 하다.  하지만 과하면 탈이 나긴 마련인데…  이 상황을 다른 각도에서 볼 여지는 없을까?

지금까지의 논의에서 작은 시스템이 많아졌을 때 이런 문제 저런 문제라고 이야기를 했지만 그 본질은 “연동”에 있다.  연동을 위한 관리 포인트, 직렬화된 순차적 연동, 연동의 깊이에 따른 문제 진단의 어려움!  그렇다 문제는 바로 연동이다!!

그럼 이 시점에서 연동을 왜 하는지를 생각해보자.  연동의 목적의 기능(Function)에 있는지 아니면 데이터 흐름(Flow)에 목적이 있는지?  후자의 경우라면 데이터 흐름을 제공하는 시스템이 있다면 문제를 좀 더 손쉽게 풀 수 있지 않을까?  서론이 무지 길었는데 이것이 하고 싶은 이야기다. (정말 말이 많은 편이다. ㅠㅠ)

System as a data perspective

넓은 의미에서 우리가 만드는 시스템은 데이터의 흐름이다.  외부 입력은 시스템에 내재된 로직의 계산 과정을 거친다.  시스템의 한 어플리케이션에서 처리된 데이터는 다른 어플리케이션으로 흐른다.  데이터의 흐름은 큰 맥락상 순차적이거나 혹은 확산적인 형태일 수 있다.

Consecutive data flow

Data pipelining

순차적 데이터 전달 모델은 시스템은 입력을 이용해 결과물을 만든다.  한 어플리케이션의 출력은 다른 어플리케이션의 입력으로 이어진다.  크게 색다를 것도 없다.  대부분의 마이크로서비스가 가지는 전형적인 모델이다.  시스템의 전체 형상에서 필요한 로직을 코드로 반영했고, 안타깝게도 흐름 관련 부분도 개발자가 코드로 추가 제어를 해야만 한다.

우리의 업무 로직은 반드시 존재해야한다.  하지만 전체 데이터 흐름을 제어하는 것까지 꼭!!! 제어해야만하는 걸까?  그림에서 보이듯이 우리의 입력이  D API Service에 도달하면 그만이다.  흐름의 제어해줄 수 있는 존재가 있다면, 우리는 이 흐름만을 정의하면 된다.  그리고 흐름 사이에 새롭게 필요해진 업무 로직을 살짝 끼워넣는다.  기존에 잘 돌아가던 기능들을 변경하지 않는다.

  • 새로운 로직을 Input/Output Data Format에 근거해서 만들면 된다.
  • 그리고 이 로직을 흐름 사이에 반영하면 된다.

기능들을 조합하기 위해 별도의 시스템을 새로 만들 필요도 없고, 기존 시스템들을 변경시키지도 않는다.

Data proliferation

쓸모가 많은 데이터의 경우 많은 사람들/시스템들이 해당 데이터를 원한다.  데이터를 얻는 대부분의 방법은 구하는(Get) 것이다.  원하는 사람이 데이터를 구하는 것이지 원한다고 데이터를 가진 사람이 떠먹여주진 않는다.  그런데… 떠먹여주면 안될까???

data proliferation

쓸모가 많은 데이터의 경우라면 관심 시스템들이 요청로직을 개별적으로 만드는 것보다는 아예 전달받는 데이터를 가진쪽에서 전달해주는 것이 훨씬 더 효율적일 수 있다.  특히 관심 데이터가 생성(Available)되었을 때 이를 전달자가 즉시 구하는 쪽에 바로 전달할 수 있다.  이는 시스템간의 데이터 동시성을 보장한다는 큰 매력이 된다.  이외에 데이터를 가진 쪽에서 바로 데이터를 넣어주기 때문에 구하는 쪽에서 일일히 Get(or Query) 기능을 구하지 않더라도 바로 들어오기 때문에 구현적 관점에서도 일부 이점을 제공한다. (하지만 크지는 않다 ㅠㅠ)

Kafka

Kafka는 오픈 소스 분산형 메시징 시스템이다.  주절이주절이 설명하는 것보다는 공식 홈페이지에 들어가보면 관련 내용을 잘 찾아볼 수 있다.  관련 책을 한권 사서 읽어도 봤지만 책보다는 그냥 홈페이지랑 구글링 통해서 얻는 자료가 더 유익하다.  그만큼 오픈 소스의 힘은 위대한 거겠지?

만든 사람들이 이야기하는 Kafka의 주요 능력이라고 개요 페이지에 적어둔걸 옮겨본다.

  1. It lets you publish and subscribe to streams of records. In this respect it is similar to a message queue or enterprise messaging system.
  2. It lets you store streams of records in a fault-tolerant way.
  3. It lets you process streams of records as they occur.

정리해보면 Publish & Subscribe 모델을 지원하는 메시징 시스템이고, 분산형 시스템들이 가지는 대부분의 특징처럼 한 두개 장비가 망가지더라도 잘 돌아간다라는 것, 그리고 Queue를 기본한 메시징 시스템으로 데이터 순서를 보장한다라는 것이다.  개인적인 매력을 느끼는 부분들을 열거해보면…

  • 시스템이 별도의 DB등 번거로운 외부 시스템을 필요로 하지 않는다.   DB. 정말 싫다.
  • 설치할게 별로 없다. 압축풀고 실행시키면 바로 돈다.
  • 특이한 프로토콜을 사용하도록 강요하지 않는다.  Producing/Consuming 과정에서 String 혹은 Byte Array를 바로 사용할 수 있다.  정 특이한걸 만들고 싶으면 역시나 만들어서 쓸 수 있다.

가장 잘 적용되리라고 생각되는 분야는 real-time streaming data pipeline 이다. 실시간 데이터의 시스템/어플리케이션간 손실없는 전달이 필요한 환경에 최적이다라고 개발자들이 이야기하고 많이 써보지는 않았지만 동감한다.

Why Kafka?

왜 그럼 이 시스템을 구현하는데 Kafka를 선택했을까?

  • 빠르다 – 분산형 메시지 처리와 관련된 시스템들이 Kafka 이전에도 Active MQ와 같이 다양하게 존재해왔다.  이런 분산형 시스템들에 비해서 Kafka 시스템이 갖는 강점은 빠른 데이터 처리에 있다.  여러 이유가 있을 수 있겠지만, Kafka를 최초로 개발한 LinkedIn 팀에서 기존 Messaging system의 문제점을 분석하고 이를 개선하기 위해 어떤 부분이 시스템의 핵심이 되어야하는지, 그 맥락을 잘 집었다고 볼 수 있겠다.
  • Publish & Subscribe 모델 지원 – 앞서 언급한 데이터 활용 모델을 구현하기 위해서는 메시징 시스템이 Publish & Subscribe 모델을 지원해주는게 최선이다.  지원하지 않는다면 따로 이 부분을 구현해야하는데 데이터의 연속성 보장등을 어플리케이션에서 단독으로 보장하기에는 많은 무리수가 있다.  Topic을 기반으로 Kafka가 이 부분에 대한 강력한 뒷배경이 되어준다.
  • 잠깐 다운되도 된다 – 데이터를 처리하는 Consuming 시스템의 입장에서 다운타임은 운영적 관점에서 치명적인 시간이된다.  하지만 Kafka 역시 분산형 메시지 시스템들이 공통적으로 지원하는 메시지 보관 기능을 마찬가지로 지원한다.  다만 언제까지고 데이터를 보관해주지는 않는다.  시스템 단위에서 설정된 보관 시간이 존재하고, 해당 시간 이내의 데이터는 분산환경내에 보관된다.

Topic and Partitions

본격적인 이야기를 진행하기전에 Kafka 내부에서 데이터가 다뤄지는 Topic과 Partition에 대한 개념에 대해 짧게 알아보고 가자.

Topic은 Kafka에서 데이터에 대한 논리적인 이름이다.  Kafka를 통해 유통되는 모든 데이터는 Topic이라는 데이터 유형으로 만들어지며 Producer/Consumer들은 자신이 생성 혹은 소비할 Topic의 이름을 알고 있어야만 한다.

Partition은 Topic을 물리적으로 몇 개의 분할된 Queue 형태로 나눠 관리할지를 지정한다.  특정 Topic이 1개의 Partition으로 되어 있다는 것은 데이터 처리를 위한 Queue가 한개 존재한다는 것이다.  3개의 Partition으로 구성되어 있다면 3개의 Queue가 존재하고, 각각의 Queue에는 독립적으로 데이터가 들어갈 수 있다.   이론적으로 Partition의 개수가 많을수록 동시에 클러스터에 데이터를 더 빠르게 받아들일 수 있음을 의미한다. 하지만 과하면 망한다는거… 따라서 클러스터를 구성하는 서버, CPU/Core, Memory 등 Resource의 제약을 고려해서 최적의 Partition 정책을 세우는게 필요하다.

우리가 다루는 Kafka내의 메시지/데이터는 모두 Topic이라는 이름을 통해 유통된다.  그리고 이 유통 과정에서 성능이 제대로 나게 할려면 시스템에 걸맞는 Topic별 Partition 정책을 어떻게 가져가야할지를 잘 판단해서 결정하는게 필요하다!

Messaging system with Kafka

자 여기까지를 바탕으로 Kafka를 이용한 메시징 시스템 구현 내용을 본격적으로 탐구 생활해본다.

지금까지 Kafka 시스템에 대해 쭉~ 이야기했다.  계속 이야기를 했지만 Kafka 자체가 메시징 시스템이다.  그런데 여기다가 뭘 더할게 있다고 이야기를 더할려고 할까?   메시지 시스템은 다른 시스템이 쓰라고 존재한다.  그렇다면 관건은 이 시스템을 어떻게 하면 손쉽게 사용할 수 있을지다.  아무리 좋다고 하더라도 사용하기 불편하다면 말짱 도루묵이다. 결국 시스템이 사용하는 시스템이더라도 그 시스템을 사용하도록 만드는건 사람이고, 복잡한 절차나 기술적인 난해함이 있다면 환영받지 못한다.

환영받는 메시지 시스템이 될려면 어떤게 필요할까?

  • 접근성이 좋아야 한다.  개발자라면 누구나 아는 기술로 접근이 가능해야 한다.
  • 표현의 자유를 보장해야 한다.  메시지에 어떤 데이터가 담길지 나도 모르고 당신도 모른다.  그런고로 다양한 데이터 형식을 기술할 수 있는 자유도를 가지고 있어야 한다.

사실 Kafka가 좋긴 하지만 이걸 사용하는 사람이 굳이 Kafka를 이해하면서까지 이 시스템을 쓰라고 하는건 어불성설이다.  따라서 메시지 시스템의 내부는 감추면서 개발자들이 보편적으로 활용할 수 있는 기술들을 가지고 접근할 수 있도록 해주는게 가장 좋다고 생각한다.

이런 특성들을 고민해봤을 때 가장 좋은 것은 Publish & Subscribe 구조에 대응하는 Producer / Consumer를 연동하는 개별 어플리케이션이 Kafka 수준에서 연동하도록 할게 아니라 이를 중계해주는 Delegating Layer를 지원해주는게 좀 더 효과적이다.  Delegating Layer를 통해 Kafka에 대한 의존성 대신 흔히 어플리케이션 연동을 위해 사용하는 HTTP 프로토콜을 사용할 수 있도록 한다면, 연동하는 어플리케이션들이 보다 손쉽게 이를 이용할 수 있다.  또한 JSON 메시지를 활용하면 앞서 이야기했던 표현의 자유를 적극적으로 챙길 수 있다.  물론 개발자들의 익숙함은 덤일 것 이고 말이다.  설명한 전체적인 윤곽을 간단히 스케치해보면 아래와 같은 형상이 될 수 있다.

 

MessagingSystemOverview

 

  • Producer – HTTP POST 방식으로 연동 어플리케이션으로부터 Payload 값을 받는다.  Payload가 Kafka 시스템을 통해 유통될 Topic의 이름은 URI Path Variable을 통해 전달받는다.  RESTful의 개념상 Path Variable이 자원의 이름을 내포하기 때문에 이 형태가 가장 적절한 표현 방법일 것 같다.  POST 메소드를 사용하는 것도 메시지를 생성(Create)한다는 의미이기 때문에 맥락상 어울린다.
  • Consumer – Kafka의 특정 Topic을 Subscribe하고 있으며, 생성된 데이터를 수신하면 이를 요청한 어플리케이션에 마찬가지로 POST로 JSON Document를 POST 메소드의 Payload로 전달한다.  여기에서는 POST 메소드로 전달 메소드를 전체적인 일관성을 위해 고정해서 사용한다.  PUT 혹은 PATCH 등과 같은 다른 메소드를 사용할 수 있도록 허용할 수도 있으나 되려 이용에 혼선을 줄 수 있기 때문에 하나만 지원한다.

이와 같은 구조는 데이터를 생산/소비하는 쪽 모두에게 웹서비스의 기본적인 개념만 있다면 누구나 손쉽게 데이터를 접근할 수 있는 길을 열어준다.  생산자 측에서는 Producer의 endpoint로 JSON Payload로 데이터를 전달하면 된다.  소비자도 마찬가지로 JSON Payload를 받을 수 있는 endpoint를 웹어플리케이션의 형태로 만들어두면, 자신이 관심을 둔 메시지가 생성될 때마다 이를 수신하게 된다.

한가지 눈여겨 볼 부분은 메시지 시스템의 Consumer와 실제 Consumer Application의 관계다.  RESTful 기반의 웹 어플리케이션은 입력된 Payload를 이용해 일련의 처리를 수행하고, 그 결과를 요청자에게 반환한다.  그리고 앞서 Consecutive Data flow의 그림에서 설명했던 것처럼 반환된 Payload는 연속된 처리를 위한 입력으로 사용되는 경우가 많다.  이를 개발자적인 관점에서 살펴보자.  일종의 함수(Procedure가 아닌 Function) 호출과 아주 비슷한 모델 아닌가?

topicedMsg1 returnedMsg1 = consumer1.function1(initialTopicedMsg);
topicedMsg2 returnedMsg2 = consumer2.function1(returnedMsg1);
topicedMsg3 returnedMsg3 = consumer2.function1(returnedMsg2);
...

약간 억지로 끼워맞추기식이긴 하지만 Method Chain 비슷해보이지 않나?? -_-;;  중요한 점은 특정 Topic이 있다면 해당 Topic을 입력으로 받는 Consumer를 찾을 수 있다는 것이다.  그리고 그 Consumer에게 Topic의 메시지를 전달하면 그 Response Payload를 얻을 수 있다.  하지만 Response로부터 Payload가 어떤 Topic인지를 명시하는 것이 좀 까리하다.

물론 Response Header에 Custom Header를 정의하는 것도 방법일 수 있겠다.  하지만 이렇게 하는 경우에 이를 암묵적인 규약으로 개발자들에게 강요하는 것이고 더구나 명시성이 떨어진다.  Consumer Application의 코드를 알기 전에는 Topic 값으로 어떤 것을 반환하는지 알기 어렵다.  이런 경우에 명시적으로 아예 Consumer Application의 정의에 반환되는 Payload에 대응하는 Topic의 이름이 뭔지를 명시하는 것이 오히려 바람직하다고 개인적으로 생각한다.

이 방식으로 Consumer와 Consumer Application간에 상호작용하는 예시를 순서도로 그려보면 아래와 같은 형상이 될 수 있다.

ConsumerInteractions

 

  • 일반적인 경우는 Consumer 1과 같이 입력 메시지를 처리 후 Response Payload를 반환한다.  Consumer 1이 반환한 Payload의 타입은 설정을 통해 어떤 Topic에 해당하는지 메시지 시스템은 알고 있다.  반환 메시지를 시스템에 지정된 Topic 이름으로 Publish함으로써, 자동적으로 다음 순서에 해당하는 Consumer 2를 찾아 들어가게 된다.
  • 특정 Topic에 대해서는 여러 Consumer들이 관심이 있을 수 있고, 당연히 여러 Consumer에게 동시에 전달된다.
  • Consumer Application에서 반환된 값이 무시되어야 할 경우도 있다. (물론 반환 Payload는 존재할 수 있다.)  이 경우를 위해 명시적으로 해당 Response Payload를 무시하기 위한 장치가 필요하다.  이미 우리는 프로그래밍 과정에서 void 라는 단어와 많이 익숙하다.  Topic의 이름 가운데 void 라는 reserved keyword를 둔다.  그리고 Topic의 이름이 void인 경우에는 반환된 payload를 무시한다.
  • 일괄적으로 메시지를 무시하는 경우가 아니라 특정 케이스에 한해서만 시스템이 이를 무시하도록 만들고 싶은 경우도 발생한다.  이건 코딩에서 if 문과 비슷한 역할이다.  이에 대한 처를 위해 HTTP Status code 가운데 특정 값을 Response 값으로 반환하는 경우에 Response Payload를 무시하도록 하자.  HTTP Status code는 개발자들의 공통 사항이기, 별도의 장치를 두는 것보다는 훨씬 설득력이 있다. (이 경우가 필요할까 싶기도 하지만 실제로 활용하다보면 정말 많은 도움이 된다.  이게 있으면 메시지 시스템을 가지고 for loop도 만들 수 있다!!!)

시스템에 대한 이야기 와중에 이 부분의 이야기가 가장 길었다.  그만큼 메시징 시스템 가운데 개발을 해야하는 측면에서 가장 많은 시간 투자가 필요한 부분이다.  하지만 이 시나리오에 맞춰서 잘 만들어두면 정말 편리한 세상을 당신이 만들고 있다는걸 알게 될 것이다.

Pros

메시징 시스템을 중심으로 마이크로서비스 방식의 어플리케이션 시스템을 구성한다고 했을 때 다음과 같은 이점을 기대할 수 있다.

  • 서로 의존성이 없는 작업들을 동시에 처리할 수 있다.  시스템 수준의 상당한 성능 향상을 기대해 볼 수 있다.
  • 단위 작업에 집중할 수 있다.  작업/배포의 단위를 더욱 더 작게 만들 수 있다. 노드 혹은 go 언어가 더 득세할 수 있지 않을까?
  • 설정 변경만으로 새로운 업무 처리가 가능하던가 혹은 기존 업무 흐름을 손쉽게 조작할 수 있다.
  • 데이터를 주고 받아야 할 상대방의 endpoint를 굳이 알 필요도 없고, 상대방을 직접 호출할 필요도 없다.

Cons

메시징 시스템이 많은 장점을 주는 것처럼 보이지만, 이것도 실제로 쓸려고 했을 때 이런 저런 문제점이 있다.

  • 시스템 자체가 문제다.  구축을 위해서는 Kafka 및 Producer/Consumer 클러스터를 구축하는데 상당히 시간이 걸린다.  장기적이 아니라면 그냥 일반적인 마이크로서비스 체계를 구축해서 사용하는게 훨 낫다.
  • 트랙잭션 이런걸 생각하지말자.  SOA로부터 현재의 마이크로서비스 세상까지 웹 도메인에서 트랜잭션을 완성시키고자하는 많은 노력이 있었다.  하지만 제대로 된 놈을 못봤다.  마찬가지로 여기에서도 이걸 실현시킬 생각을 했다면 번지수를 한참 잘못 찾았다.  되려 트랜잭션이 없어도 돌아가도록 본인의 어플리케이션을 설계하는게 훨씬 더 재미있을 것 같다.
  • 비동기 방식이다.  성능적으로는 아주 좋은 이점이지만 실제 운영될 때 상상을 초월하는 모습을 보여주는 경우가 있다.  특히 타이밍이 맞아야 하는, 즉 동기성이 필요한 부분에 이 시스템을 이용한다면 낭패를 볼 수 있다.  “내가 확인했을 때 항상 그랬다!!” 라는 말을 비동기 세상에서는 하지 말자.  항상 그렇게 동작하다 중요한 순간에 당신을 배반하는게 비동기 세상이다.

 

여기까지다.  이 과정을 통해 만들었던 시스템이 궁극의 마이크로화를 향해나가는 서비스 세상에서 서로 얽히고 설킨 거미줄 같은 실타래를 큰 파이프를 꼽아서 정리하는데 도움이 됐으면 좋겠다.  또한 동기적 세상에서만 머무리던 사고를 비동기를 활용할 수 있는 다른 기회의 세상이 되기를 또한 바란다.

이제부터는 Kafka 클러스터를 실제로 설치하는 과정에서 참고했던 정보와 주의해야할 몇가지 경험들을 공유하기로 하겠다.

Tips in building a Kafka cluster

기본 설치 방법: http://kafka.apache.org/quickstart 로컬 환경에서 간단히 돌려보기 위한 방법은 이걸로 충분하다.

AWS EC2 setup

물리 장비를 사용한다면 각 장비를 셋업해야하지만… 요즘 다 AWS 쓰는거 아닌가???  그러니 AWS를 기반으로 설명하도록 하겠다.

  • Kafka를 1개 instance로 셋업한다.
  • 셋업된 instance를 AMI로 만들고, instance를 재생성할 때 이를 활용하면 좋다.
  • 생성시킬 전체 클러스터의 각 EC2 instance의 IP를 DNS 작업을 해준다. 설마 IP로 관리할 생각을 가지고 있지는 않다고 생각하는데.

Configuring cluster

  • 클러스터링: http://epicdevs.com/20  해당 사이트에서 기본적인 설치 방법에 대해 잘 설명해뒀다.
  • Cluster 노드 장비들은 클러스터 외부에서 인식 가능한 이름을 부여하고 이를 DNS에 등록해서 활용하는게 좋다.  만약 DNS 등록이 여의치않다면 각 노드들의 이름을 개별 노드의 hosts 파일에 모두 등록하는 작업을 최소한 해둬야한다.

Possible issues

진행을 하다가 이런 문제점을 만날 수 있다.

Partition offset exception

kafka.common.KafkaException: Should not set log end offset on partition [test,0]'s local replica 2
at kafka.cluster.Replica.logEndOffset_$eq(Replica.scala:66)
at kafka.cluster.Replica.updateLogReadResult(Replica.scala:53)
at kafka.cluster.Partition.updateReplicaLogReadResult(Partition.scala:239)
at kafka.server.ReplicaManager$$anonfun$updateFollowerLogReadResults$2.apply(ReplicaManager.scala:905)

여러 이유가 있을 수 있지만, FAQ를 읽어보면 좋은 Insight를 얻을 수 있다. 내 경우에는 topic을 생성할 때 쉽게 하자고 현재 zookeeper 호스트의 이름을 localhost로 줬다. 이 때문에 cluster내의 다른 instance에서 해당 호스트를 제대로 찾아내지 못했던 것 같다. 이 경우에 topic자체는 생성된 것이기 때문에 전체 instance를 내렸다가 올리면 문제를 해결할 수 있다. 할려면 제대로 된 호스트 이름을 주고 작업해주는게 좋다.

Metadata error

기본 설정이라면 클러스터 환경에서Producer/Consumer client에서 Metadata를 확인할 수 없는 경우 아래와 같은 오류를 만날 수 있다.

2016-11-23 09:12:18.231  WARN 3694 --- [ad | producer-1] org.apache.kafka.clients.NetworkClient   : Error while fetching metadata with correlation id 168 : {test=LEADER_NOT_AVAILABLE}
2016-11-23 09:12:18.334  WARN 3694 --- [ad | producer-1] org.apache.kafka.clients.NetworkClient   : Error while fetching metadata with correlation id 169 : {test=LEADER_NOT_AVAILABLE}

오류가 발생하는 이유는 클러스터 외부의 장비에서 접속한 클러스터 장비 가운데 하나를 찾질 못해서 발생한다. 문제를 해결할려면 다음의 설정을 server.properties 파일에 반영해줘야 한다.

advertised.host.name = known.host.name

advertised.host.name 필드는 Broker에 접근한 클라이언트에게 “이 서버로 접근하세요” 라는 정보를 알려주는 역할을 담당한다. 별도로 설정을 잡지 않으면 localhost 혹은 장비의 이름이 반환된다. Producer 혹은 Consumer쪽에서 반환된 hostname을 인식할 수 없으면 추가적인 작업을 할 수 없으므로 이 오류가 발생된다. 따라서 해당 속성의 값에 DNS에 등록된 호스트의 이름을 넣어주거나 클러스터내에 존재하는 장비들을 클라이언트 환경의 /etc/hosts 파일에 반영해줘야 한다.

Amazon Kinesis

Kafka를 가지고 작업을 하다보니 아마존에서 재미있는 걸 만들었다라는걸 알았다. 키네시스(Kinesis)?  대강 살펴보니 하는 짓이 내가 지금까지 이야기했던 것들과 상당히 유사한 일을 해준다.  물론 아마존 클라우드에 있는 여타 기능들과의 연계성도 아주 좋다.  만약 장기적인 관점에서 Kafka 클러스터를 구축할게 아니라면 Kinesis도 다른 대안이 될 수 있을 것 같다.

특히 이 글을 읽는 여러분들의 시스템이 AWS에서 동작하는 것들이 많다면 Kinesis가 상대적으로 더 좋은 선택이 될 수 있다. 가끔 생각지도 않은 요금 폭탄에 대해 쓰기전에 고민은 반드시 필수다!!

 

자주 사용하는 Shell script 모음

이것도 정리를 해두지 않으니 매번 찾게된다.

때 될때마다 정리해두자.

특정 디렉토리의 하위 디렉토리를 Pretty printing 하기

#!/bin/sh

depth=$2

space=" "
count=1
while [ $count -le $depth ];
do
	count=$((count+1))
	space="$space    "
done;

for dir in `ls $1`
do
	if [ -d $1/$dir ]; then
# pretty printing version
		echo "$space$dir"
# full path version
#		echo "$1/$dir"
		nextdepth=$((depth+1))
		$PWD/list.sh $1/$dir $nextdepth
	fi
done

다음 명령으로 실행시킴

./list.sh <aboulte-path] 1

.svn 디렉토리 지우기

find . -name .svn -exec ls '{}' \;

for-loop 돌리기

for i in array
do 
... looping commands here ...
done

특정 문자열을 공백으로 치환해서 이를 배열로 뽑아내기

email=abc@def.xyz
tokens=(${$email//@/ })
id="${tokens[0]}"
doimain="${tokens[1]}"
  • 주의할 점은 배열로 나눌려면 각각의 토큰이 공백( )으로 구분되어야 한다는 점이다.

간단한 사칙연산하기

index=$((index+1))

이 예제는 index++과 동일한 기능을 수행한다.

가능하면 까먹지 말아야 하는데 쓰는 경우가 자주 있질않다보니…


curl을 이용해서 반복적으로 웹 API 호출하기

간단한 배치로 웹 API를 호출하는 경우가 필요한데 이런 경우에 대표적으로 이용할 수 있는 것이 curl이다.  웹 API를 호출할려면 기본적으로 이런저런 헤더 정보를 추가로 줘야하는데 그런 경우에 문자열을 표시하기 위해 따옴표(“)를 써야한다.  그런데 따옴표로 스크립트상의 파라미터를 감싸버리면 이게 명령상에서 제대로 인식하지 못하는 경우가 발생한다.

우회적인 방법으로 생각할 수 있는게 스크립트 상에서 스크립트를 만들고, 그 스크립트를 실행하는 방법으로 이를 구현할 수 있다.

#!/bin/sh
for ip in `cat ip`
do
  data=" '{ \"clientIp\": \"$ip\" }' "
  out="curl -s -X POST http://localhost:8080/api/v1/ip -H \"Content-Type: application/json; charset=utf-8\" -d $data | awk -F, '{ print \$3 };'"
  echo $out > run.sh
  chmod +x run.sh
  result=`./run.sh`
  echo $ip " " $result
done

주의할 점은 출력할 결과에 마찬가지로 따옴표 혹은 쌍따옴표가 포함되야 하는 경우에는 Backslash(\)를 이용해서 이를 별도로 표시해줘야 한다는 것! 특히나 생성되는 스크립트 파일에서 참조되는 스크립트 변수와 같은 경우에도 마찬가지로 backslash를 넣어줘야한다.


awk 사용해서 마지막 필드 출력하기

awk '{ print $NF }' filename.txt