블로그가 털렸네

회사에서 개발을 하면서 항상 고려할 최우선 순위 가운데 하나가 보안이다. 특히나 서비스내에서 제공되는 정보 가운데 개인과 관련된 민감한 정보가 있다면 보안은 최우선 순위 고려 사항이다. 회사에서 개발을 할 때는 이걸 항상 가장 먼저 생각하는데, 최근에 정말 어이없는 일을 겪었다.

뭐냐하면… 짜잔~

그렇다. 이 홈페이지가 털렸다. 친절하신 해커분께서 WordPress DB로 사용하는 MySQL 서버에 접속하셔서, 데이터를 몽땅 다운로드 받으신 다음에 모든 테이블을 싹~ 정리해주셨다. 그리고 친절하게 READ.me 라는 테이블에 비트코인을 보내주면 데이터를 보내주겠다는 친절한 멘트를 남겨주셨네?

처음에는 이상하게 홈페이지 방문자 카운트가 나오지 않았다. 단순 플로그인 문제로 생각했다. 몇 달동안 플러그인 버전업이 됐다는 메시지를 봐왔으니까. 홈페이지의 방문자 수를 보는게 일상의 소소한 재미였는데, 몇 일이 지나도 숫자가 그대로니 고쳐야겠다는 생각이 들었다. 그런데 이게 왠걸? 업데이트 하라는 플러그인들을 죄다 업데이트했는데도 여전히 카운트가 나오질 않네? 뭐지???

Frontend 단에서 JS 문제가 있는가 싶어서 크롬 개발자 모드로 들어가서, Refresh를 했다. 음! DATABASE CONNECTION ERROR 라는 아주 불친절한 문구가 떡 하니 나온다. 그 사이에 암호도 변경한게 없고, AWS에 돈도 따박따박 내고 있었는데 이게 뭐지? 내가 실수로 Security Group 설정을 변경했나? 장비에 들어가서 telnet 으로 접속되는지 확인해봤지만 정상이다.

$ telnet db.rds.domain.name 3306

데이터베이스 서버로 TCP 접속은 되는데, 데이터베이스 오류면 암호가 안먹는거라는 것 같은데? Mysql Workbench로 데이터베이스에 접속해봤더니 덜렁 READ.me 라는 테이블만 하나 덜렁있다 순간 내가 RDS Instance의 Security Group의 설정을 0.0.0.0/0 으로 설정했던 기억이 머리를 커다란 망치로 때린다. 왜 EC2 Instance는 Security Group을 나름 생각한다고 잡아두고, RDS는 이렇게 바보처럼 해놨을까?

망!!

네이버 퇴사하면서 그 이후에 작성한 글들은 죄다 여기에서 작성해왔는데 다 날라간건가? 글의 절대적인 가치는 보잘것 없겠지만 그래도 하나 쓰더라도 나름 신경을 썼던 글들이었다. 무엇보다도 지금까지 라이엇에서 한 작업들 가운데 까먹지 말자라는 차원에서 기록해둔 것들이었는데. 이대로 날려버린건가?

좌절모드에서 헤매고 있었는데, AWS Console에서 RDS 설정을 보니 Snapshot 백업을 설정해 둔게 기억났다. 부랴부랴 들어가서 확인해보니 최근 7일내의 데이터가 일 단위로 백업되고 있었다. 언제 해킹을 당했는지 WordPress 사이트에서 일단위 Visiting count 이력을 보니 아직 하루쯤 여유 시간이 남았네!


기쁘다 구주오셨다!

서둘러 Snapshot을 가지고 RDS Instance를 생성시켰다. 일단 생성시킨 RDS 접속해서 테이블의 상태를 확인해보니 모든 테이블과 데이터들이 온전히 살아있다. 다행히 최근에 이런 저런 일들이 있어서 글을 거의 안쓰고 있었는데 빠진 데이터없이 온전히 글들이 살아있다. 휴~ 일단은 다행이다.

  • 먼저 Security group의 설정부터 잡는다. EC2 Instance에서만 접근 가능하도록 했다. 물론 Workbench와 같은 쿼리 도구들을 바로 접속할 수 없긴 하지만 필요한 경우에는 AWS Console에서 필요할 때마다 Security Group의 설정을 변경하는게 아주 많이 안전하다.
  • 다음으로 정말 단순했던 데이터베이스 접근 암호를 수정한다. 기억력의 한계를 절감하는 나이라 그런지 사이트마다 암호를 달리 설정하는데 한계가 있다. 5 ~ 6 가지 정도의 암호를 몇가지 룰을 가지고 변형해서 사용해왔다. 털린 암호는 2개의 영문 단어를 조합한 형태였는데 Dictionary를 가지고 대입 공격을 하면 쉽게 뚫릴 수 있는 구조라는 생각이 들었다. 안타깝게도 MySQL에서는 암호에 특수 문자를 사용할 수 없다. 그럼에도 암호에 대한 복잡도를 높일 필요성이 있고, 3개의 단어와 숫자들을 조합해서 암호를 변경했다.
  • 가장 중요한 포인트인 것 같은데 암호를 변경할 날짜를 캘린더에 적어뒀다. 암호는 자주 바꿔주는게 최선인 것 같다. 바꾼 암호를 까먹지 않는다는 전제하에.

이렇게 설정을 마치고, WordPress의 연결 정보를 업데이트했다. 그리고 이렇게 글을 쓴다.

이번 일을 겪으면서 돌이켜 생각이 든다. 과연 나는 제대로 된 보안 정책을 가지고 개발을 하고 있는가? 귀찮기 때문에 혹은 설마라는 생각 때문에 어느 포인트에서 보안과 관련된 허점을 만들어두지는 않았을까? 이번 일을 겪으면서 보니 모를 일이라는 생각이 불현듯 스친다. 신규로 개발한 서비스들에 대해서는 모두 보안 리뷰를 받았다. 전문가 팀이 리뷰를 해준 사항이기 때문에 일정 수준은 안전하다라는 생각이 들기도 하지만 이 생각 자체도 만약의 사고에 스스로를 위한 면피가 아닐까 싶다.

개발할 때 자만하지 말고 개발하자. 그리고 잘하자. 쉬운길이 아니라 제대로 된 길로 가자.

– 끝 –

Git 기반 효율적인 이벤트 페이지 배포 환경 만들기

고객과 소통을 많이 할려다보면 이것 저것 알릴 내용들이 많다. 이건 게임 회사이기 때문이 아니라 소통에 대한 의지를 가진 회사라면 당연히 그래야한다.

SVN을 사용했었는데 무엇보다도 변경 사항에 대해 파악하는 것이 너무 힘들었다. 또한 매번 배포 때마다 브랜치를 머지하고 관리하는데 쉽지가 않다. 대부분의 프로젝트들은 모두 git을 사용하고, 전환했지만, 프로모션 영역은 7G라는 덩치의 Hell of Hell이었기 때문에 차일피일 미뤄지고 있었다.

기술 부채를 언제까지 끌고갈 수는 없다. 해야할 것을 미루기만 해서는 두고두고 골치거리가 된다.

이벤트/프로모션 페이지들은 배포되면, 이후의 코드 변경은 거의 발생하지 않는다. 하지만 다른 사이트등을 통한 참조가 발생할 수 있기 때문에 유지는 필요하다. 해당 페이지들을 통해 컨텐츠 혹은 정보들이기 때문에 그냥 404 오류가 발생하도록 놔둘 수는 없다. 따라서 기간이 지나면 관리해야하는 용량이 커질 수밖에 없다.  이렇게 커진 용량을 빌드/배포하는 건 전체 프로세스의 효율성을 확 떨어트린다. 특정 프로모션 영역(디렉토리)별로 배포하는 체계를 이미 갖췄기 망정이지, 그게 아니라면 7G 짜리를 매번 배포하는 최악의 배포 환경이 될 수 밖에는 없었을 것이다.

특정 영역별로 배포하는 방식에서 힌트를 얻었서 전체 코드들을 각 이벤트/프로모션 영역별로 쪼개서 각자 관리하기로 했다. 개별적인 성격의 프로모션 사이트로 볼 수 있기 때문에 각각의 디렉토리는 의존성이 없다. 때문에 개별 Repository로 나눠놓는 것이 완전 독립성 부여라는 관점에서 맞기 때문에 SVN repository를 git organization으로 만들고, 개별 디렉토리를 git repository로 만들었다. 이 방식의 문제점은 SVN 작업 이력을 git 환경으로 가져가지 못한다는 점이다.  하지만 “새 술은 새 부대에“라는 명언이 있지 않은가!!

맘을 정하고, Organization을 생성한 다음에 Repository를 Github을 통해 생성했다. 수련하는 마음으로 열심히 노가다를 하다보니 이내 모든 Repository를 만들긴 했는데… 이렇게 노가다한 결과 Repository를 세어보니 100개가 훌쩍 넘는다. 헐… 올리긴 해야하니까 스크립트의 도움을 받아 push했다.

쪼개놓는 건 일단 이쁘게 정리를 했는데 이제 배포 체계다. 일반적으로 개발 단게에서 master로 머지되는 코드는 자동으로 배포한다. 그래야 과정의 결과물을 관련된 사람들이 즉시즉시 확인할 수 있다. Git을 사용하는 경우, 이를 위해 webhook을 이용한다. Polling을 이용하는 경우도 있긴 하지만 이건 SVN을 쓰때나 써먹는 방법이다. 현대적이지도 않고 아름답지도 않다.  그런데 100개 이상이나 되는 코드에 일일히 webhook을 걸려고 생각해보니 이건 장난이 아니다. 노가다도 개발자의 숙명이라고 이야기하는 사람이 있을지 모르겠다. 하지만 프로모션이 늘어날때마다 webhook을 한땀한땀 설정하는 것도 웃기다. 누가 이 과정을 까먹기라도 한다면 사수에게 괴롭힘을 당할 수도 있기 마련이기도 하고. (안타깝지만 정말 이런게 어느 분야를 막론하고 흔하게 있다. 적폐에 타성으로 물든다고나 할까?)

자동화다. 개발자의 숙명은 적폐를 청산하고 사람의 개입없이도 돌아가는 시스템을 만들어내는 것이다. 다행이도 git의 경우에는 개별 repository에서 발생한 push 이벤트를 repository가 소속된 organization에 전달하는 기능이 있고, wehbook을 oragnization에 설정하는 것을 허용한다. 이 기능을 활용하면 신규 프로모션 작업을 위해 새로운 repository를 만들더라도 별도로 webhook을 설정할 필요가 없다.

(Jenkins는 application/json content-type만을 받아들인다. 괜히 urlencoded 형식으로 해서 안된다고 좌절하지 말자)

이제 배포를 위해 Jenkins에 해당 webhook을 이용해 정보를 전달하면 된다. 근데 어케 webhook payload를 jenkins가 이해하지? 그렇다. 여기서 다시 큰 문제점에 봉착한다. Jenkins에서 활용할 수 있는 git plugin은 이름이 지정된 특정 repository의 webhook을 인식할 수 있지만, 이 경우를 상대할려면 jenkins쪽에 각 repository들에 대응하는 jenkins job을 만들어줘야 한다. 이게 뭔 황당한 시츄에이션인가? 간신히 한 고비를 넘겼다고 생각했는데 앞에 비슷한 역대급 장애물이 기다리고 있다.

하지만 갈구하면 고속도로는 아니지만 길이 나타난다.  Jenkins에서 아래와 같은 두가지 아름다운 기능을 제공한다.

  • Parameterized build – 비드를 할 때 값을 파라미터로 정의할 수 있도록 하고, 이 파라미터 값을 빌드 과정에서 참조할 수 있도록 해준다.
  • Remote build trigger – Job에서 지정한 Token값이 HTTP authorization header를 통해 Jenkins에 전달되면 해당 Job이 실행된다. 와중에 Parameter 값을 별도로 설정도 할 수 있다.

이 두가지 기능을 활용하면, Job 하나만 만들어도 앞서 정의한 100개 이상의 repository의 빌드/배포를 실행할 수 있게 된다. 환경 설정을 위해 아래와 같이 Jenkins Job에 Repository 맵핑을 위해 String parameter를 정의하고, git repository 설정에서 이를 참조하도록 한다.

Jenkins Job을 선택하기 위한 Token은 아래 방식으로 설정한다. Jenkins는 해당 토큰값으로 어느 Job을 실행한지 선택하기 때문에 중복된 값을 사용해서 낭패보지 말길 바란다.

설정이 마무리됐다면 아래와 같이 테스트를 해보자.

 

curl -X POST "http://trigger:jenkins-trigger-user-credential@jenkins.sample.io/job/deploy-promo-dev/buildWithParameters?token=TOKEN&delay=0&PROMOTION=promotion”

Jenkins Host 이름 앞에 들어가는 건 Jenkins 접근을 위한 사용자 정보이다. 일반 사용자의 아이디 및 Credential을 바로 사용하지 말고, API 용도의 별도 계정을 생성해서 사용할 것을 권한다.

하지만 Build trigger를 누가 호출해주지? 누구긴, 당신이 짠 코드가 해야지! 이제 본격적인 코딩의 시간이다.

Git org에 설정한 webhook의 payload로부터 개발 작업이 이뤄진 repository와 branch를 확인하고, 이를 build trigger의 query parameter로 전송하면 된다. 일반적인 웹 어플리케이션처럼 상시적인 트래픽을 받는 시스템이 아니기 때문에 운영을 위해 별도의 어플리케이션 서버를 구축하는 건 비용 낭비다. 이를 경우에 딱 맞는 플랫폼이 AWS Lambda이다.  복잡한 코딩이 필요한 것도 아니기 때문에 Node.js를 활용해서 간단히 어플리케이션을 만들고, S3를 통해 이 어플리케이션이 Lambda에 적용될 수 있도록 했다. 실제 호출이 이뤄지도록 API Gateway를 앞단에 배치하면 끝!

Node.js를 이용한 Lambda 코드는 아래와 같이 작성해주면 된다.

var http = require('http');
var btoa = require('btoa');
exports.handler = (event, context, callback) => {
  var repository = event.repository.name;
  var options = {
    host: 'jenkins.sample.io',
    port: 80,
    headers: {
     'Accept': 'application/json', 
     'Authorization': 'Basic ' + btoa('trigger:jenkins-trigger-user-credential') 
    },
    path: '/job/deploy-promo-dev/buildWithParameters?token=TOKEN&delay=0&PROMOTION=' + repository,
    method: 'POST'
  };

  var refElements = event.ref.split('/');
  var branch = refElements[2];
  if (branch === 'master') {
    http.request(options, function(res) {
      console.log('STATUS: ' + res.statusCode);
      res.on('data', function(chunk) {
        console.log(chunk);
      })
    }).on('error', function(e) {
      console.loge('error',e);
    }).end();
    callback(null, 'Build requested');
  } else {
    callback(null, 'Build ignored for ' + branch + ' pushing');
  }
};

 

이제 개발하시는 분들이 개발을 막~~~ 해주시면 그 내용이 프로모션 웹 영역에 떡하니 표시되고, 프로모션 담당자들이 확인해주면 된다. 그리고 최종적으로 완료되면 라이브 환경에 배포를 해주면 된다.

근데 배포를 누가 해주지?? 라이브 배포는 자동으로 할 수 없으니까 개발 환경과 유사한 라이브용 Jenkins Job으로 개발자가 돌려야 하는거 아니가? 맞다. 걍 개발자가 하면 된다. 흠… 개발자가… 하지만 이 단계에서 개발자가 하는건 배포 버튼을 눌러주는게 다 아닌가? 개발과 라이브의 환경 차이가 물론 있긴하지만 프로모션이라는 특성상 그닥 크지 않다. 이미 개발 환경에서 프로모션을 담당자들이 깔끔하게 확인한 걸 개발자가 한번 더 확인할 필요도 없고 말이다.

게으른 개발자가 더 열열하게 게으르고 싶다. 어떻게 하지?? 뭘 어떻게 하긴, 열 코딩하는거지.

Git이란 환경은 정말 개발자에게 많은 것들을 아낌없이 나눠준다. 그 가운데 하나가 바로 API. 내가 사용하는 Git의 경우에는 Enterprise(Private) Git이기 때문에 적절하게 Credential만 맞춰주면 API를 호출할 수 있다. 보통은 이걸 위해 API 전용 Secrete을 생성해서 사용하는게 안전하다. (어느 바보가 자기 아이디 암호를 API 호출하는데 사용하지는 않겠지??)

Git API를 이용하면 Git org에 속한 모든 repository들을 모두 가져올 수 있다. 그럼 이 가운데 배포 대상을 하나 선택해서 프로모션 담당자가 Jenkins job을 trigger할 수 있도록 해주면 되는거 아닌가!! 쓰는 사람을 위해 배려 하나를 더해 준다면 가장 최근 작업 repository가 배포 대상이 될 것이라 업데이트 시간을 기준으로 최근 repository가 앞에 오게 하자. 아름다운 이야기다.

복잡하지 않다. jQuery를 이용한 간단한 웹 어플리케이션이면 족하다. 100줄 미만으로 구현된다. 물론 미적 추구를 더한다면 더 길어질 수도 있겠지만 개인적으로 절제된 공백의 아름다움이 최고라고 생각하는 1인이기 때문에. 물론 아무나 들어와서 마구 배포 버튼을 누르지 못하도록 적절한 예방 장치들을 마련되야 한다.

전체를 그림 하나로 그려면 대강 아래와 같다.

 

– 끝 –

참고한 것들

Spring batch를 Parallel로 돌려보자

Monolithic 아키텍처 환경에서 가장 잘 돌아가는 어플리케이션 가운데 하나가 배치 작업이다. 모든 데이터와 처리 로직들이 한군데에 모여있기 때문에 최소한의 비용으로 빠르게 기능을 돌릴 수 있다. 데이터 존재하는 Big Database에 접근하거나 Super Application Server에 해당 기능의 수행을 요청하면 된다. 끝!!!

하지만 요즘의 우리가 개발하는 어플리케이션들은 R&R이 끝없이 분리된 Microservices 아키텍처의 세상에서 숨쉬고 있다. 배치가 실행될려면 이 서비스, 저 서비스에 접근해서 데이터를 얻어야 하고, 얻은 데이터를 다른 서비스의 api endpoint를 호출해서 최종적인 뭔가가 만들어지도록 해야한다.  문제는 시간이다!

마이크로서비스 환경에서 시간이 문제가 되는 요인은 여러가지가 있을 수 있다. 배치는 태생적으로 대용량의 데이터를 가지고 실행한다. 따라서 필요한 데이터를 획득하는게 관건이다. 이 데이터를 빠르게 획득할 수 없다면 배치의 실행 속도는 느려지게 된다. 다들 아는 바와 같이 마이크로서비스 환경이 일이 돌아가는 방식은 Big Logic의 실행이 아니라 여러 시스템으로 나뉘어진 Logic간의 Collaboration이다. 그리고 이 연동은 대부분 RESTful을 기반으로 이뤄진다.

RESTful이란 뭔가? HTTP(S) over TCP를 기반으로 한 웹 통신이다. 웹 통신의 특징은 Connectionless이다. (경우에 따라 Connection oriented) 방식이 있긴 하지만, 이건 아주 특수한 경우에나 해당한다. TCP 통신에서 가장 비용이 많이 들어가는 과정은 Connection setup 비용인데, RESTful api를 이용하는 과정에서는 API Call이 매번 발생할 때마다 계속 연결을 새로 맺어야 한다. (HTTP 헤더를 적절히 제어하면 이를 극복할 수도 있을 것 같지만 개발할 때 이를 일반적으로 적용하지는 않기 때문에 일단 스킵. 하지만 언제고 따로 공부해서 적용해봐야할 아젠다인 것 같기는 하다.)

따라서 Monolithic 환경과 같이 특정 데이터베이스들에 연결을 맺고, 이를 읽어들여 처리하는 방식과는 확연하게 대량 데이터를 처리할 때 명확하게 속도 저하가 발생한다. 그것도 아주 심각하게.

다시 말하지만 배치에서 속도는 생명이다. 그러나 개발자는 마이크로서비스를 사랑한다. 이 괴리를 맞출려면…

  1. 병렬처리를 극대화한다.
  2. 로직을 고쳐서 아예 데이터의 수를 줄인다.

근본적인 처방은 두번째 방법이지만, 시간이 별로 없다면 어쩔 수 없다. 병렬 처리로 실행하는 방법을 쓰는 수밖에…
병렬로 실해시키는 가장 간단한 방법은 ThreadPool이다. Springbatch에서 사용 가능한 TaskExecutor 가운데 병렬 처리를 가능하게 해주는 클래스들이 있다.

  • SimpleAsyncTaskExecutor – 필요에 따라 쓰레드를 생성해서 사용하는 방식이다. 연습용이다. 대규모 병렬 처리에는 비추다.
  • ThreadPoolTaskExecutor – 쓰레드 제어를 위한 몇 가지 설정들을 제공한다. 대표적으로 풀을 구성하는 쓰레드의 개수를 정할 수 있다!!! 이외에도 실행되는 작업이 일정 시간 이상 걸렸을 때 이를 종료시킬 수 있는 기능들도 지원하지만… 이런 속성들의 경우에는 크게 쓸일은 없을 것 같다.

제대로 할려면 ThreadPoolTaskExecutor를 사용하는게 좋을 것 같다. 병렬 처리 가능한 TaskExecutor들은 AsyncTaskExecutor 인터페이스 페이지를 읽어보면 알 수 있다.

@Bean(name = "candidateTaskPool")
public TaskExecutor executor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setCorePoolSize(CORE_TASK_POOL_SIZE);
    executor.setMaxPoolSize(MAX_TASK_POOL_SIZE);
    return executor;
}

이 메소드 정의를 Job Configuration 객체에 반영하면 된다. ThreadPool을 생성할 때 한가지 팁은 Pool을 @Bean annotation을 이용해서 잡아두는게 훨씬 어플리케이션 운영상에 좋다. 작업을 할 때마다 풀을 다시 생성시키는 것이 Cost가 상당하니 말이다. 어떤 Pool이든 매번 만드는 건 어플리케이션 건강에 해롭다.

전체 배치 코드에 이 부분이 어떻게 녹아들어가는지는 아래 코드에서 볼 수 있다.

@Configuration
@EnableBatchProcessing
    public class CandidateJobConfig {
    public static final int CORE_TASK_POOL_SIZE = 24;
    public static final int MAX_TASK_POOL_SIZE = 128;
    public static final int CHUNK_AND_PAGE_SIZE = 400;

    @Bean(name = "candidateTaskPool")
    public TaskExecutor executor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(CORE_TASK_POOL_SIZE);
        executor.setMaxPoolSize(MAX_TASK_POOL_SIZE);
        return executor;
    }

    @Bean(name = "candidateStep")
    public Step step(StepBuilderFactory stepBuilderFactory,
                     ItemReader<User> reader,
                     ItemProcessor<User, Candidate> processor,
                     ItemWriter<Candidate> writer) {
        return stepBuilderFactory.get("candidateStep")
                                 .<User, Candidate>chunk(CHUNK_AND_PAGE_SIZE)
                                 .reader(candidateReader)
                                 .processor(candidateProcessor)
                                 .writer(candidateWriter)
                                 .taskExecutor(executor())
                                 .build();
    }

이렇게 하면 간단하다.

하지만 이게 다는 아니다. 이 코드는 다중 쓰레드를 가지고 작업을 병렬로 돌린다. 하지만 이 코드에는 한가지 문제점이 있다. 살펴보면 Chunk라는 단위로 작업이 실행된다는 것을 알 수 있다. Chunk는 데이터를 한개씩 읽는게 아니라 한꺼번에 여러 개(이 예제에서는 CHUNK_AND_PAGE_SIZE)씩 읽어 processor를 통해 실행한다. 배치의 실제 구현에 대한 이해나 고려가 필요하다.

Chunk를 사용해서 IO의 효율성을 높이는 방법은 흔하게 사용되는 방법이다. 하지만 입력 데이터를 Serialized된 형태로 읽어들여야 하는 경우라면 좀 더 고려가 필요하다. MultiThread 방식으로 배치가 실행되면 각 쓰레드들은 자신의 Chunk를 채우기 위해서 Reader를 호출한다. 만약 한번에 해당 Chunk가 채워지지 않으면 추가적인 데이터를 Reader에게 요청한다. 이 과정에서 쓰레드간 Race condition이 발생하고, 결국 읽는 과정에서 오류가 발생될 수 있다. 예를 들어 입력으로 단순 Stream Reader 혹은 RDBMS의 Cursor를 이용하는 경우에는.

문제가 된 케이스에서는 JDBCCursorItemReader를 써서 Reader를 구현하였다. 당연히 멀티 쓰레드 환경에 걸맞는 Synchronization이 없었기 때문에 Cursor의 내부 상태가 뒤죽박죽되어 Exception을 유발시켰다.

가장 간단한 해결 방법은 한번 읽어들일 때 Chunk의 크기와 동일한 크기의 데이터를 읽어들이도록 하는 방법이다. Tricky하지만 상당히 효율적인 방법이다.  Cursor가 DBMS Connection에 의존적이기 때문에 개별 쓰레드가 연결을 따로 맺어서 Cursor를 관리하는 것도 다른 방법일 수 있겠지만, 이러면 좀 많이 복잡해진다. ㅠㅠ 동일한 크기의 데이터를 읽어들이기 위해 Paging 방식으로 데이터를 읽어들일 수 있는 JDBCPagingItemReader 를 사용한다. 관련된 샘플은 다음 두개의 링크를 참고하면 쉽게 적용할 수 있다.

이걸 바탕으로 구현한 예제 Reader 코드는 아래와 같다. 이전에 작성한 코드는 이런 모양이다.

Before

public JdbcCursorItemReader<User> itemReader(DataSource auditDataSource,
                                                @Value("#{jobExecutionContext['oldDate']}") final Date oldDate) {
    JdbcCursorItemReader<User> reader = new JdbcCursorItemReader();
    String sql = "SELECT user_name, last_login_date FROM user WHERE last_login_date < '%s'";
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    reader.setSql(String.format(sql, sdf.format(elevenMonthAgoDate)));
    reader.setDataSource(auditDataSource);
    ...
}

After

public JdbcPagingItemReader<User> itemReader(DataSource auditDataSource,
                                             @Value("#{jobExecutionContext['oldDate']}") final Date oldDate) {
    JdbcPagingItemReader<User> reader = new JdbcPagingItemReader();

    SqlPagingQueryProviderFactoryBean factory = new SqlPagingQueryProviderFactoryBean();
    factory.setDataSource(auditDataSource);
    factory.setSelectClause("SELECT user_name, last_login_date ");
    factory.setFromClause("FROM user ");
    factory.setWhereClause(String.format("WHERE last_login_date < '%s'", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(oldDate)));
    factory.setSortKey("user_name");

    reader.setQueryProvider(factory.getObject());
    reader.setDataSource(auditDataSource);
    reader.setPageSize(CHUNK_AND_PAGE_SIZE);

    ...
}

여기에서 가장 핵심은 CHUNK_AND_PAGE_SIZE라는 Constant다. 이름에서 풍기는 의미를 대강 짐작하겠지만, Chunk에서 읽어들이는 값과 한 페이지에서 읽어들이는 개수가 같아야 한다는 것이다. 이러면 단일 Cursor를 사용하더라도 실행 Thread간의 경합 문제없이 간단히 문제를 잡을 수 있다. 하지만 명심할 건 이것도 뽀록이라는 사실.
이렇게 문제는 해결했지만 과연 마이크로서비스 환경의 배치로서 올바른 모습인가에 대해서는 의구심이 든다. 기존의 배치는 Monolithic 환경에서 개발되었고, 가급적 손을 덜 들이는 관점에서 접근하고 싶어서 쓰레드를 대량으로 투입해서 문제를 해결하긴 했다. 젠킨스를 활용하고, 별도의 어플리케이션 서버를 만들어서 구축한 시스템적인 접근 방법이 그닥 구린건 아니지만 태생적으로 다음의 문제점들이 있다는 생각이 작업중에 들었다.

  • SpringBatch가 기존의 주먹구구식 배치를 구조화시켜서 이쁘게 만든건 인정한다. 하지만 개별 서버의 한계를 넘어서지는 못했다.  한대의 장비라는 한계. 이게 문제다. 성능이 아무리 좋다고 하더라도 한대 장비에서 커버할 수 있는 동시 작업의 한계는 분명하다. 더구나 안쓰고 있는데도 장비가 물려있어야 하니 이것도 좀 낭비인 것 같기도 하고…
  • Microservice 환경의 Transaction cost는 기존 Monolithic에 비해 감내하기 힘든 Lagging effect를 유발시킨다. 그렇다고 개별 개별 서비스의 DB 혹은 Repository를 헤집으면서 데이터를 처리하는건 서비스간의 Indepedency를 유지해야한다는 철학과는 완전 상반된 짓이다. 구린 냄새를 풍기면서까지 이런 짓을 하고 싶지는 않다. Asynchronous하고, Parallelism을 충분히 지원할 수 있는 배치 구조가 필요하다.

한번에 다 할 수 없고, 일단 생각꺼리만 던져둔다. 화두를 던져두면 언젠가는 이야기할 수 있을거고, 재대로 된 방식을 직접 해보거나 대안제를 찾을 수 있겠지.

Frontend crossdomain issue in IE

최근에 서비스를 오픈하면서 겪은 경험담 하나 정리해볼려고 한다.

백엔드 개발자로써 격는 크로스도메인 이슈를 통칭해서 CORS와 관련된 문제라고 이야기한다. API에 대한 요청이 동일 도메인이 아닌 경우에 발생할 수 있는 이슈다. 대부분 정책적인 문제와 관련된 것이라 도메인에 대한 접근 제어 혹은 권한 제어를 통해 해결의 실마리를 찾는다.

이 비슷한 문제가 Frontend쪽에서도 발생할 수 있다는 걸 작업 과정에서 알게 됐다. 문제의 개요를 간단히 설명해보면…

  1. AA 도메인 및 BA 도메인은 A 도메인의 하위 도메인이다.
  2. AA 도메인에서 B 사이트에서 제공하는 기능을 이용해 공통 기능을 구현하였다.
  3. BA 도메인에서 AA 도메인의 기능을 이용하기 위해 AA 도메인에서 제공하는 웹 페이지를 팝업으로 실행한다.
  4. AA 도메인에서는 기본 설정을 잡고, B 도메인의 페이지를 호출한다.
  5. B 도메인에서는 기능을 모두 수행하고 그 결과를 AA 페이지에 반환한다.
  6. AA 도메인의 결과 페이지에서는 최종 결과를 팝업을 실행한 BA 도메인 페이지로 전달한다. 이 전달을 위해 일반적으로 widnow.opener를 사용한다. 보통은 window.opener.location.replace(…) 메소드를 활용해서 Opener 페이지를 다른 페이지로 redirection 시키는 방법이 적용된다.(물론 우리도 이런 방법을 사용했다.)

설명은 복잡하지만 아래 그림을 보면 좀 더 이해가 빠를 것이다.

일반적인 시나리오고, 대부분(?)의 경우에 이 방식은 정상적으로 잘 동작한다. 하지만 IE가 이 대부분의 경우에 포함되지 않는 것 같다.
IE는 마지막 5번 과정에서 팝업을 실행시킨 페이지가 지정된 페이지로 redirection되지 않는다. 기존 화면은 그대로 유지된 상태에서 새로운 탭 화면에서 replace() 메소드로 전달된 url이 나타난다. OMG

재미있는 건 이 현상은 크롬이나 파이어폭스에서는 나타나지 않는다. 오직 IE10, IE11등 IE 브라우저에서만 나타난다. IE에서 당췌 뭔 짓을 한거야???

문제를 좀 파보다보니 다음 문제 사항을 찾게 됐다.
팝업 혹은 frame, iframe 환경에서 서로 다른 도메인간에 데이터를 주고 받는 건 위험하다.
따라서 이걸 위한 적절한 안전 장치가 필요하다.
각 브라우저에 따라 안정 장치를 구현하는 방법이 틀린데 그럼 IE는 이걸 어떤 방식으로 구현했을까?

팝업된 화면에서 페이지 navigation등이 발생해서 opener와 다른 도메인으로 callee 화면이 이동된 경우, window.opener에 대한 callee쪽에서 호출하는 것을 차단한다.

그럼 어떤 방식으로 해야 callee쪽에서 호출했을 때 caller쪽의 페이지를 접근할 수 있는걸까? 여러 방법을 찾아봤지만, 뾰족한 방법이 보이지 않았는데 찾다보니 내부 사이트에 다음과 같은 코드가 존재하는 걸 확인했다.

document.domain

즉, document.domain의 값을 caller와 callee 사이에 맞추면 된다.  실행시킨 화면과 팝업 화면을 나타내는 document의 domain이 동일한 값을 가지면 document.location의 값을 참조하거나 replace() 등의 함수를 사용해서 변경할 수 있다. 하지만 그렇다고 아무 도메인 값을 document.domain 객체 값에 할당할 수 있는 것은 아니다. 현재 도메인이 sub.a.b.c 라고 한다면 document.domain의 값이 될 수 있는 대상자는 a.b.c, b.c 등이 될 수 있다. 아예 다른 도메인으로의 변경은 데이터의 hijacking을 고려해서 허용되지 않는 것 같다.

이 방식을 적용해서 위 그림의 시나리오를 정리해보면 아래와 같은 그림으로 풀어볼 수 있다.

 

  • AA 화면에서 “Click here to popup” 링크가 눌렸을 때 현재 화면의 document.domain의 값을 A.com 으로 JS로 변경한다.
  • 팝업 화면은 Redirection 혹은 내부 페이지 이동을 통해 각 페이지의 hostname이 domain 값이 된다.
  • 팝업의 최종 화면인 end.B.A.com에서 화면을 닫기 전에 A.com 값으로 팝업 페이지의 document.domain 값을 변경한다.  end.B.A.com 도메인의 상위 도메인인 A.com으로 값을 변경하는 것이기 때문에 보안적으로 허용된다.
  • 팝업 화면과 팝업 화면을 실행시키는 화면의 도메인이 이제 A.com 으로 동일해졌다. 이 시점부터 팝업 화면에서 실행 화면의 페이지를 window.opener 객체를 이용해서 제어할 수 있는 길이 생겼다.
  • 이 상태에서 Opener 페이지를 이동시키고, 팝업 화면은 닫히면 된다!!!

보안이라는 관점에서 보면 되려 이런 고려는 오히려 크롬이나 파이어폭스보다 IE가 좀 더 나은거 아닌가 하는 생각도 든다. (하지만 이런 정책이 있다는게 어딘가에 알려졌다면 더 좋았겠지만, 당췌 이 정보를 찾기가 넘 어렵다는게 함정!!!)

암튼 이렇게 해서 문제를 해결해서 어색한 동작을 없앴다라는게 기쁠 뿐이다.

좋은 코드에 대한 생각 – 3: 작업에 대한 기록

개발이라는 건 기록의 작업이다. 코드 한줄을 작성하더라도 이유없는 코드가 없다. 이런 이유로 코드를 작성할 때 그 근거를 기록으로 남길려고 하고 권장한다.

당신은 어떤 방식으로 기록하고 있나? Jira와 같은 티켓 관리 시스템을 이용할 수도 있겠고, 혹은 Confluence에 일지를 쓸수도 있겠다. 하지만 당신이 개발자라면 이 문제를 개발자스럽게 풀고 있을 것이라고 생각한다.

코멘트?

가장 흔하게 생각할 수 있는 방법이고 실제로 많은 개발자들이 작업에 대한 이력을 코멘트로 남긴다. 그림에서 보이는 녹색 코멘트가 가장 대표적인 경우다. 코드를 작성한 이유가 무엇이며 동작이 이런 방식으로 움직인다고 설명한다.

하지만 이런 코멘트는 비추다. 첫번째 이유는 이 코멘트에 사람들이 집중하지 않는다. 개발자라면 코드를 읽을려고 하지 문제가 있지 않는한 코멘트를 읽지 않는다. (사실 정말 그런지도 의문이다. 문제가 있다면 디버깅을 하거나 해당 코드 영역에 대한 테스트 코드를 살펴보지 않을까?)

정말 심각한 문제는 코드를 작성한 본인(들)도 본인들의 코멘트를 지키지 않는다는 사실이다. 기록을 해두면 된다라고 생각하지만 수정 가능한 코드에 대한 기록은 관리가 필요하다.  즉 코드가 바뀌면 코멘트도 바뀌어야 하는데 거의 대부분 코드만 수정하지 코멘트는 수정하지 않는다. 여러 이유가 있지만 그만큼 코멘트는 최초 작성자를 포함해 제대로 읽히지 않는다는 것을 의미한다.

코멘트는 남기지 않는게 좋다. 당신이 남기는 코멘트는 100% 쓰레기가 된다. 이런 쓰레기를 남기는 활동은 정신 건강에 좋지 않다. 그럼에도 불구하고 굳이 코멘트를 남기겠다면 코드 중간에 남기지 말고 Docs 문서 생성이 가능한 메소드 상단에 붙혀준다.

코드 중간에 남기는 코멘트는 진심 백퍼 쓰레기다. 되려 상단에 남기는 코멘트는 메소드의 의미를 설명해주기 때문에 사람들의 눈에 보여질 가능성도 훨 높을 뿐만 아니라 수정 가능성도 덩달아 높아진다.

의미에 이름을 주자.

특정 코드 블럭이 의미나 목적을 가진 부분이라면 이걸 굳이 코멘트로 남길려고 하지 말자.

질척거리는 코멘트보다 함수 혹은 메소드로 의미에 이름을 부여하는 것이 백배 좋다. 메소드의 이름을 적어주고 따로 Javadoc를 목적으로 한 것인지는 모르겠지만 따로 코멘트를 남기는 경우가 있다. 그렇게 남기는 설명이 메소드의 의미를 부연하기 위한 것이라면 차라리 이름을 충분히 길게 작성하자. 코드를 읽는 사람이 두 번 읽게 하는 것보다는 한번 읽어서 의미를 파악할 수 있게 하는게 좋다.

개발자가 싫어해야할 것은 같은 어떤 의미에서든 중복을 없애는 것이다. 그리고 이 규칙은 메소드 이름과 코멘트의 경우에도 마찬가지다. 그리고 이제 메소드의 코드를 최대한 간결하게 작성해서 술술 읽히게 만든다. 충실한 코멘트보다는 쉽게 읽히는 코드가 다른 개발자 혹은 동료를 위한 배려다.

소스 관리 도구들을 활용하자.

그럼에도 불구하고 다른 기록을 남기고 싶은 욕구가 생기는 경우가 있다. 개인 경험상 특정 버그 혹은 요청 티켓을 처리한 경우가 대표적이다. 지금 변경한 코드가 이 티켓에 해당하는 변경이라는 것으로 남기고 다른 사람도 그 티켓을 참조했으면 하고 바라기 때문이다. 앞서 이야기했지만 그렇다고 이걸 코멘트로 코드안에 남겨봐야 의미없다. 더 좋은 방법이 없을까?

코멘트보다 좀 더 효과적인 방법으로 추천할만한 내용은 커밋 로그(Commit log)를 활용하는 것이다.  커밋은 코드의 “특정 작업 단위가 마무리됐다는 것“을 말한다. 마무리된  작업의 내용을 설명하는 것이 커밋 로그이다. 또한 온라인 코드 리뷰 자체도 이 커밋 단위로 리뷰가 진행된다. 따라서 작업 설명은 충실하면 충실할수록 좋다. 작업 내용의 충분한 이해를 바탕으로, 리뷰어가 코드를 살펴보고 리뷰를 남겨준다면 리뷰를 받는 사람에게 더 도움이 된다.

결론은 충실하면 좋다긴 하지만 Source repository로 뭘 사용할 것인가에 따라 접근 방법이 틀려진다. 국내에서는 아직 SVN을 Repository로 사용하는 회사 혹은 개발팀이 많다. 하지만 알다시피 SVN은 중앙에서 소스를 관리한다. 그게 뭔 문제??? 관리 대상이 작다면 별 문제가 안되지만, 누구나 알듯이 많아지면 주구장창 느려지는 문제점이 있다. 더구나 SVN은 커밋을 하게되면 이를 서버에 전송하여 저장한다. 각 커밋 단위가 가지는 의미가 상당하다.

이런 이유로 SVN을 사용하는 대부분의 팀에서는 한 커밋 단위에서 자잘한 코드 변경을 커버하기 보다는 의미있는 단위로 작업하라고 권고한다. 한 커밋 변경에서 변경되는 작업 분량이 커지게 된다. 이 내용을 상세하기 적을려면 분량이 상당해진다. 바꿔 말하면 변경 내용을 알아보게 로그 형태로 적기 힘들다. 잘 적을까? 당연히 안적게 된다. 남길 수 있다면 티켓 번호 정도? 따라서 변경의 주요 내용을 커밋 로그에서 따라잡기 어렵다. 이 상황이 지속된다면 차라리 변경 주요 내용을 코멘트로 님기는게 더 나은 방법일 수도 있다.

그러니까 git을 사용해야 한다. Distributed source repository가 주는 장점이랄까? 혹은 로컬에서 자신의 Copy를 가지고 움직이기 때문에 갖는 장점일지는 모르겠다. 가볍고 빠른다. 그리고 Remote repository에 push를 통해 업로드하기 때문에 각 Commit 단위가 갖는 무게가 상대적으로 가볍다.

Git을 Source repository 사용한다면 다음의 규칙을 사용해서 로깅을 남겨보자.

» 서로 독립적인 변경이 있다면 각각에 대해 모두 커밋 로그를 남긴다. 예를 들어 불필요한 코멘트를 삭제했다면 그것도 커밋의 단위이고, 오타 한글자를 수정했었도 것도 따로 커밋한다.

» 코드 작성/변경 자체가 좁은 범위에 대해 이뤄진다. 로그에 쓸말도 많지 않아야 한다. 50 ~ 80자 이내로 코멘트를 작성하자. 바꿔 이야기하면 이 정도의 내용으로 커버될 내용이 한번의 커밋 대상이어야 한다.

» 다른 사람들과 공유해야하는 경우에만 Remote branch에 push한다. 모 회사의 광고 아닌 광고에 보면 불이 나도 push는 하고 가라는 말이 있긴 하지만 branch 방식으로 PR을 관리한다면 간간히 push를 하지 말고 자주 push를 하는게 좋다. 그렇게 모인걸 PR 날리면 땡이니 Remote에 얼마나 자주 push를 하던 문제가 안된다. (불날때 먼저 나가는게 중요하지 push는 하고 가라는 우스개 소리 아닌 소리를 하는 회사는 별로다.)

» 최종 PR의 타이틀은 간결해야한다. 내용에는 PR통해 반영될 기능들에 대한 설명과 리뷰를 해줬음 하는 대상자들의 이름을 적어둔다. 그리고 사람들 사이에 대화가 이어지면 된다.

 

Source repository를 쓴다면 이런 목적이어야 하지 않을까? 변경이 왜 일어났고, 언제 일어났으며, 누구에 의해 발생했는지를 한 눈에 명확하게 파악할 수 있다. 와중에 git 같은 툴을 사용하고 있다면, 코드가 발전되어 가는 형상을 관찰할 수도 있다. 그렇기 때문에 git을 써야한다.

개인적으로는 코드에 코멘트를 하나도 남기지 않는 것이 최선이라고 생각한다. 코드에 남기는 코멘트는 파일이 최초로 만들어졌을 때 자동 생성되는 작성자 이름과 작성일 정도면 충분할 것 같다. 그 이외에 자동으로 생성되는 나머지 코멘트들은 모두 지워버린다. 남겨두면 그게 득이 되는 경우를 본적이 없는 것 같다.

코딩 세상에서 미니멀리즘을 추구해야할 부분이 바로 이 포인트이지 않을까 싶다.

곱씹기 – 피터 드러커의 최고의 질문

리더쉽에 대한 조직장님의 추천이 있어서 읽게 된 책이다.

목차에 보면 위대한 질문들이 나온다.

» 미션: 왜 무엇을 위해 존재하는가?

» 고객: 반드시 만족시켜야 할 대상은 누구인가?

» 고객 가치: 그들은 무엇을 가치있게 생각하는가?

» 결과: 어떤 결과가 필요하며 그것은 무엇을 의미하는가?

» 계획 수립: 앞으로 무엇을 어떻게 할 것인가?

각 질문에서 이야기하는 것들은 책을 읽어보면 알 것 같고, 전체 문맥을 통해 저자가 이야기하고 싶은 것들을 정리하면 아래와 같은 그림이 되지 않을까 싶다. 그 최종 결과로 나오는 성과가 “고객의 일상을 어떻게 변화시키고 있는가?” 라는 질문에 똑 부르진 답을 내놓을 수 있다면 우리가 하는 일은 올바른 일을 하고 있다고 결론적으로 책에서 이야기한다.

 

개인적으로 간만에 읽은 한글 책이다. 소설책을 제외하고. 하지만 좀 번역이 아주 상당히 이상하다. 아무래도 직역을 한 부분들이 많은 것 같고, 그렇기 때문에 당췌 글이 머리에 잘 들어오지 않는다. 걍 원서로 읽을 걸 그랬다는 생각이 든다.

원서 링크는 여기에…

 

Slack as a slack – 슬랙을 슬랙답게 쓰자

일상 생활에서 여러 메신저 어플을 사용하지만, 업무용 메신저도 따로 있다. 다른 분들은 카톡이나 라인 같은 어플을 업무용으로도 사용할지도 모르겠다. 하지만 개인용 메신저 어플에게 회사의 정보를 믿고 맞긴다는건 말이 안된다. 항상 “사고”는 존재하기 때문이다.

업무용 메신저는 따로 사용해야한다. 큰 기업인 경우 자체적으로 사내 메신저를 만들어서 사용하는 케이스를 본다. 네이버의 경우에도 사내 메신저를 PC부터 모바일까지 죄다 자체적으로 만들어서 사용했었으니까. 자체적으로 만들어 사용하는 한마디로 비추다.

몇가지 메신저를 봤지만, 기업용 메신저의 끝판왕은 슬랙(slack)인 것 같다. 여러 이유가 있지만 그 가운데서 가장 독보적인 이유는 알림 서비스다. 슬랙의 알림 기능, 특히 모바일 어플의 알림 기능은 독보적이다. 가끔은 30분 정말 느리게 올 경우에는 메시지가 입력된 이후 2시간 이후에도 도착하는 경우가 있다.  심지어 이미 내가 읽었음에도 불구하도 친절하게 다시 한번 알려준다는… 하지만 PC버전은 칼같이 메시지 알림이 잘 온다.

이건 불평 대상이 아니냐구? 오~~ No! No!! 당신이 당신의 오피스 자리 혹은 PC를 떠났다는건 여러모로 이유가 있다. 그중 첫번째 이유는 퇴근이다. 그리고 휴식이다. 휴식을 취하는 시간에 오는 연락은 정말 짜증이다. 정권이 바뀐 다음에는 이걸 금지하고 있다. 마땅히 그래야한다. (내가 이런 말을 할 자격이 있는지는 모르겠지만…)

좌우당간 PC 버전을 설치해놓은 당신은 일하고 있음을 증명한 거다. 알람 칼같이 온다. 모바일 버전은??? 깔아는 둬야겠지? 하지만 왠만히 연락 안되면 문자한다. 것도 아니라면 전화를 하던지… 당장 급한 상황임에도 불구하고 굳이 슬랙 메시지를 보내고 연락이 안된다고 성화인 사람! 스마트 폰의 본연의 기능인 전화기 기능을 사용해라.

업무용 슬랙을 잘 사용하는 방법을 좀 적어보자.

  1. Public channel에서 이야기한다.
    업무용 메신저의 가장 큰 목적은 일하는데 필요한 정보를 나누는 것이다. 그리고 대부분의 정보는 일에 관심있는 모든 사람들에게 유익하다. 이 유익한 정보에 대한 접근에 대한 제한을 굳이 둬야할 이유는 없다. 재무나 경영상의 정보와 같이 민감한 정보가 아니라면 모든 걸 회사에 있는 사람들이 접근할 수 있도록 허용하는게 옳다.
  2. Private channel을 가급적 만들지 않는다.
    하지만 정말 아무나 들여다보면 신경이 쓰이는 정보들이 있다. 그런 정보들의 경우에는 제한된 사람들이 보는게 맞고 그런 사람들을 위해 Private channel이라는게 있다. 하지만 두번 생각해볼 필요가 있다. 정말 필요하지 말이다. 뭔가 숨길만한 꺼리가 아니라면 Public channel로 채널을 유지하는게 맞고, 정히 숨겨야할 이야기라면 따로 이야기한다.
  3. 따로 이야기하지 않는다.
    왠만하면 DM(Direct messaging)을 사용하지 말라는 이야기다. 회사에서 주고 받는 대화는 대부분 일에 관련된 것이다. 일에 관련된 게 아니라면 DM을 사용하거나 카톡을 사용하는게 맞겠다. 가쉽꺼리를 원한다면 카톡으로 가서 친구들이랑 이야기하는게 옳다. 그리고 그렇게 쫄리는 이야기라면 회사에서 안하는게 좋다.
  4. 하나의 주제로 모아져야 할 필요가 있을때는 쓰레드(Thread)로 글을 모은다.
    메신저라는 메인 대화 창구로 삼았을 안타깝게도 이미지를 첨부할 수 없다는… 언능 이 기능이 생겼으면 좋겠다!
  5. 어플들을 활용해라.
    개발자들이 사용하는 거의 모든 도구들이 연동된다. Jira, Git, Jenkins, Favro,… 그래서 개발자들이 슬랙을 좋아하는 것 같다.
    어플을 연동해둬야 하는 이유는 심플하다. 관련된 정보들을 슬랙 채널에 모으기 위해서다. 새로운 이슈가 생기던 진행되던 상태의 변화가 있던 모든 내용이 업무 채널에 존재한다. 그리고 그걸 화두로 이야기가 시작된다. 얼마나 멋진가?  그리고 이렇게 모아진 정보들에 대해 슬랙은 멋진 검색 기능을 제공한다. 적절한 검색 필터를 활용하면 효과적으로 자신의 정보를 찾을 수 있다.
  6. 슬랙은 업무용이다. 이점을 잊지 말자.

마이크로소프트가 업무용 메신저를 만든다고 대들었다고 한다. 아직도 이 프로젝트가 진행중인지 모르겠지만,… 슬랙이 짱!!이다.

만약 당신 회사에서 업무용 메신저로 깨톡을 사용한다면… 당장 슬랙으로 바꾸길 권한다.

다중 도메인을 위한 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을 찾다보니까 그런게 아닐까 싶다.  반성하고 성실하게 살아야겠다.