다중 도메인을 위한 HTTPS Certification 생성 방법

HTTP 환경에서 서비스를 제공하는건 여러모로 불안하다.  단순한 Packet sniffing에도 데이터가 탈취되니 말이다.  이렇게 말하지만 이 사이트 자체도 https를 사용하지 않으니 할말은 없다.

그럼에도 불구하고 회사를 통해 제공되는 서비스는 안전함을 우선해야 한다. 그렇기 때문에 HTTPS 방식으로 서비스를 제공해야하고.  개별 장비에 대한 인증서 생성을 위해서는 Certification 생성을 위해서는 CSR과 KEY 파일을 생성해야 한다.  한 도메인에 대한 생성은 아래 명령을 이용하면 간단히 만들 수 있다.

openssl req -newkey rsa:2048 -nodes -out your_domain_co_kr.csr -keyout your_domain_co_kr.key \
-subj "/C=KR/ST=Seoul/L=Seoul/O=Riot Games Inc./OU=Dev/CN=your.domain.co.kr"

주의할 점은 도메인 이름이 넘 길면 안된다.  아마 64글자까지만 지원하는 것으로 기억한다.

메인 도메인을 포함해 개발(dev) 및 QA(qa) 환경들에 대한 복수 도메인에 대한 발급하는 경우에는 설정 파일을 이용하는 방법을 사용한다.  왜 사용자들이 접하는 도메인도 아닌데 굳이 https를 사용하는지에 대해서는 다른 의견이 있을 수 있다. 하지만실제 환경과 동일한 조건에서 개발과 테스트가 이뤄지는 것이 합당하다.  환경이 차이가 존재하면 이를 맞춰주기 위한 차이가 존재한다.  이런 티끌들이 모여 결국에는 장애를 일으키니까.

복수 도메인에 대한 CSR 및 KEY 파일의 생성은 아래 명령을 참조한다.

openssl req -newkey rsa:2048 -nodes -out your_domain_co_kr.csr -keyout your_domain_co_kr.key \
-config your_domain_co_kr.cfg

your_domain_co_kr.cfg 파일의 내용은 아래와 같이 설정하면 된다.

[req]
default_bits = 2048
prompt = no
default_md = sha256
req_extensions = req_ext
distinguished_name = dn

[ dn ]
C=KR
ST=Seoul
L=Seoul
O=Riot Games Inc.
OU=Dev
emailAddress=owner@domain.co.kr
CN = your.domain.co.kr

[ req_ext ]
subjectAltName = @alt_names

[ alt_names ]
DNS.1 = your.domain.co.kr
DNS.2 = dev.your.domain.co.kr

 

여러 도메인을 사용할거면 차라리 와이드카드(*)를 쓰는게 더 좋지 않을까에 대한 의견도 있을 수 있다.  개인적으로 처음에 사용할 각 도메인을 모두 열거해야한다는 지침을 들었을 때 마찬가지 생각을 가졌다.  당장은 이 도메인만을 사용하지만 앞으로 더 많은 도메인들을 만들어 쓸 계획이었으니까.

하지만 곰곰히 생각해보니 보안적인 측면에서 이 방식이 주는 해로움이 더 크다고 생각했다.  만약 와일드카드 인증서가 탈취된 경우에 해커가 이걸 이용해서 다른 도메인을 만들어 피싱사이트를 만들 수 있으니 말이다.  번거롭더라도 사용할만큼 만들어서 쓰는게 올바른 방법인 것 같다.

그럼에도 불구하고 굳이 와이드카드 방식으로 사용하고 싶다면 CN = *.your.domain.co.kr로 설정하면 된다.

 

보안이라는건 보안 담당자에게 중요한게 아니라 믿고 써주는 사용자들을 지켜주는 것이니까 말이다.

AWS EC2에서 S3 Webhosting에 접속하기

라고 쓰지만 다른 이름으로  “같은 집안끼리 왜 이래!!”로 잡는다.

시스템을 구성하는 과정에서 목적에 따른 다양한 도메인을 별개로 잡기보다는 하나의 도메인에서 각 기능 제공 영역을 reverse proxy로 구성하는 방안을 적용했다. 여러 도메인들을 관리해야하는 피로감이 있었고, 각 도메인별로 따로 Certification을 받아야 하는 프로세스가 귀찮은 것도 있었다.  대강의 구조는 아래와 같이 셋업했다.

기존 설정에서 AWS S3 webhosting에 대한 리소스 접근을 제어하기 위해 Bucket Policy의 aws:SourceIp를 통해 접근 제어를 했다. Public Web Server는 IDC에 존재했기 때문에 웹서버의 Public NAT IP를 sourceIp로 등록해서 나름 안전하게 사용하고 있었다.

이번에 겪은 문제는 IDC의 장비를 AWS EC2 Instance로 이전하면서 발생했다.

당연히 S3 Webhosting으로 접근하는 IP는 EC2 Instance에 부여된 Public IP일 것이기 때문에 이걸 등록해주면 끝! 일거라고 생각했다.

curl http://ifconfig.io

이 명령으로 110.10.10.123이 Public IP로 확인됐다고 했을 때, 아래의 IP에 110.10.10.0/24 대역으로 추가해줬다.

{
"Version": "2012-10-17",
"Id": "Policy1492667932513",
"Statement": [
	{
		"Sid": "Stmt1492667929777",
		"Effect": "Allow",
		"Principal": "*",
		"Action": "s3:GetObject",
		"Resource": "arns3:::reverse1.sample.co.kr/*",
		"Condition": {
			"IpAddress": {
				"aws:SourceIp": [
					"120.10.10.0/24",
					"110.10.10.0/24"
				]
			}
		}
	}
    ]
}

잘 돌겠지? 라고 생각했지만 웬걸… EC2 장비에서 curl로 테스트를 해보니 Access Denied라는 메시지만 나올 뿐이다.

$ curl http://reverse1.sample.co.kr
... Access Denied ...

아무리 S3가 특정 Region에 구속되는 놈이 아니라고 하더라도 왜 같은 집안 사람들끼리 친하지 않지?  이때부터 집안 사람들끼리 왜 이러는지 한참을 구글링을 해봤지만 IP를 가지고 접근 제어를 풀어내지는 못했다.  회사의 다른 사람들에게 물어봤어도 뾰족한 답을 얻을 수 없었는데, 한 분이 힌트를 주셨다.  같은 AWS 환경은 같은 Region에 있는 장비들끼리는 AWS 자체의 내부망을 통해 통신한다는거!

여기에서 힌트를 얻어서 그럼 내부망을 사용한다는 건 VPC를 통해 뭔가 통신이 이뤄질꺼라구 추측하고 관련된 구글링을 해본결과,  VPC 수준에서 Bucket Policy가 있다는 걸 확인했다.

 

이를 반영한 전체 설정 내용은 아래와 같다.

{
"Version": "2017-10-17",
"Id": "PolicyBasedOnIpAndVPC",
"Statement": [
	{
		"Sid": "",
		"Effect": "Allow",
		"Principal": "*",
		"Action": "s3:GetObject",
		"Resource": "arns3:::reverse1.sample.co.kr/*",
		"Condition": {
			"IpAddress": {
				"aws:SourceIp": [
					"120.10.10.0/24"
				]
			}
		}
	},
	{
		"Sid": "",
		"Effect": "Allow",
		"Principal": "*",
		"Action": "s3:GetObject",
		"Resource": "arns3:::reverse1.sample.co.kr/*",
		"Condition": {
			"StringEquals": {
				"aws:sourceVpc": [
					"vpc-a6ccdbcf"
				]
			}
		}
	}
    ]
}

추가한 Allow 설정에서 접근하는 대상의 VPC정보를 확인해서 내부 VPC의 경우에 허용하는 정책을 설정해준다.  이런 설정으로 변경 적용한 이후에 EC2 Instance에서 curl 명령으로 확인해보면 정상적으로 컨텐츠가 노출되는 것을 확인할 수 있다.  EC2 환경을 위한 추가적인 IP 설정등은 필요없는 일이었다.

간단한 것 같지만 몰랐기 때문에 바보처럼 이틀이나 허비를 해버렸다.  VPC를 통한 Policy 설정에 대한 상세 정보는 http://docs.aws.amazon.com/AmazonS3/latest/dev/example-bucket-policies-vpc-endpoint.html 에서 확인하면 된다.

 

변수명으로 Readability 높이기?

코드 리뷰를 하다보면 약간 복잡한 expression 혹은 statement의 결과를 변수로 치환한 다음에 아래에서는 그 변수를 사용하는 경우가 있다.  변수의 값 참조가 여러번 이뤄지면 문제가 아니지만 갸우뚱하게 되는 케이스는 한번만 사용하는 경우다.  대부분의 자동화 분석 도구는 이런 경우에 대해 고치라는 처방전을 준다.  하지만 나는 기계가 아니라 사람이고 사람이 보기에 복잡해보이는 걸 두는 것보다는 “변수가 의미를 설명해주는데 좀 더 낫지 않을까?” 라는 생각했다.

우연찮게 이 고민에 대한 글 하나를 봤는데 읽어보니 내 생각이 잘못된 것 같다는 생각이 빡! 한대 후려치고 간다.  글 내용을 간단히 정리하면.

  1. 간단한 expression or statement의 결과를 변수로 뽑는 엉뚱한 짓은 하지 마라. 걍 간단하니 직접 써라.
  2. 변수를 써서 의미를 명확하게 해야만 할 것 같은 충동을 일으키는 복잡한 경우에는 변수쓰지 말고 함수써라.

왜 변수 쓰는데 함수 쓸 생각을 못했을까? 재활용도 하고 테스트도 작성할 수 있는데 말이다. ㅠㅠ

아무래도 가장 간단한 Coding shortcut을 찾다보니까 그런게 아닐까 싶다.  반성하고 성실하게 살아야겠다.

 

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이 이어져야한다.  하지만 한번 돌고 나면 만족하게 마련이다.  인간이라는게 이기적인 동물이기 때문이다. 목표를 달성하면 그 이후의 뒤정리는 그 이기심을 넘어서는 열정이 있어야 하는데 말이다. 그런 사람이 대부분이라고 생각하지 않는다.

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

– 끝 –