이 글은 Todd Motto의 글 Killing it with Angular Directives; Structure and MVVM를 번역한 글이다.

Angular에서 디렉티브를 어떤 방식으로 사용해야 하는지 예제를 통해 설명하고 있다. Angular에서 각각 코드 사이의 관계를 분리하는 방식은 Angular만의 방식이 아닌 MVVM 패턴을 활용하고 있다. 디렉티브의 구조를 어떤 방식으로 작성하는지, linkcontroller, 그리고 외부에서 서비스를 주입하는 것으로 각각 로직을 어떻게 분리하는지 이해하는데 도움되는 글이다.


Angular 디렉티브 때려잡기: 구조와 MVVM

이 포스트에서는 Angular 1.x에서 어떻게 디렉티브(Directives)를 작성하는가에 대한 원칙을 설명하려고 한다. 디렉티브를 어떻게, 왜, 어디에서 사용해야 하는지에 대한 혼란이 많다. 하지만 이 개념을 한번만 이해하고 구분하면 아주 간단한 기능이다. 이 포스트는 중첩된 디렉티브 또는 부모 스코프에서 흐르는 데이터 흐름과 같은 내용을 다루지 않는다. 대신 디렉티브를 만들고 구조화하고, 관계를 분리하는데 가장 이상적인 방법과 함께, controllerlink 프로퍼티를 어떻게 올바르게 사용하는가에 대해 다루려고 한다.

기초적인 디렉티브, 구조, 그리고 Angular에서 사용하기 가장 좋은 방식으로 구조화하는 방법에 대해 다룬다. 디렉티브 작성에 대한 접근 방식을 보여주기 위해 모조 "파일 업로드" 디렉티브를 만들어보자.

노트, 이 코드는 실제로 동작하지 않으며, 효과적으로 디렉티브를 구조화 하는 방법에 대해 설명하려는 의도로 작성했다.

구조 Structure

AngularJS 스타일 가이드를 따라 자연스럽게 기초적인 디렉티브 정의를 작성하고 Angular의 .directive() 메소드에 넣는다:

function fileUpload () {
  
  return {};

}
angular
  .module('app')
  .directive('fileUpload', fileUpload);

적당한 위치에 정의를 작성했으니 파일 업로드 컴포넌트를 위한 기본적인 프로퍼티를 추가한다:

function fileUpload () {
  
  return {
    restrict: 'E',
    scope: {},
    template: [
      '<div>',
      '</div>'
    ].join(''),
    controllerAs: 'vm',
    controller: function () {},
    link: function () {}
  };

}
angular
  .module('app')
  .directive('fileUpload', fileUpload);

이 코드가 내가 기본적으로 필요로 하는 모든 요소가 포함된 "디렉티브 보일러플레이트"로, 이 코드를 기초로 동작하는 디렉티브를 만든다.

컨트롤러 Controller (presentational layer)

컨트롤러를 controller: fn처럼 객체에 바인딩하는 대신 (link에도 동일), controller 프로퍼티를 fileUpload 함수 정의 내에서 바인딩을 먼저 한 후, 객체에 연결해 반환받는 형태로 작성한다. 이 방식으로 작성하면 함수를 객체에 직접 작성하는 방식보다 함수를 정의하는 공간이 있어서 연관된 함수를 더 쉽게 찾고 이해할 수 있고, 함수에 주석을 작성하는데 더 나은 구조가 된다. 이 형태는 "엄격해 보이는 API"에 묶여 있는 느낌보다 평범한 JavaScript처럼 표현된다.

함수를 맨 위에 작성하고 주석 몇 개를 작성한다.

/**
 * @name fileUpload
 * @desc <file-upload> Directive
 */
function fileUpload () {

  /**
   * @name fileUploadCtrl
   * @desc File Upload Controller
   * @type {Function}
   */
  function fileUploadCtrl() {

  }

  /**
   * @name link
   * @desc File Upload Link
   * @type {Function}
   */
  function link() {

  }

  return {
    restrict: 'E',
    scope: {},
    template: [
      '<div>',
      '</div>'
    ].join(''),
    controllerAs: 'vm',
    controller: fileUploadCtrl
    link: link
  };

}
angular
  .module('app')
  .directive('fileUpload', fileUpload);

멋진가? 당연하다. controllerAs: 'vm'에서 볼 수 있듯 컨트롤러를 vm이란 별칭으로 지정했다. (뷰모델 ViewModel을 뜻한다.) 이렇게 컨트롤러를 뷰모델로 다루는 것은 "프리젠테이션 모델 Presentation Model" 디자인 패턴에 해당한다. 이 문법에 익숙하지 않다면 ControllerAs(번역)에 대해 먼저 읽어보자. $scope를 필수적으로 주입하는 방식 대신 컨트롤러 자체를 $scopevm 별칭에 바인딩하는 방식, 즉 $scope.vm을 생성하게 된다. $scope 대신 this 키워드를 사용하는 것으로 이 뷰-모델을 마치 컨트롤러 "클래스"인 것처럼 작성할 수 있다.

/**
 * @name fileUploadCtrl
 * @desc File Upload Controller
 * @type {Function}
 */
function fileUploadCtrl() {
  this.files = [];
  this.uploadFiles = function () {

  };
}

this를 사용하는 것이 $scope 쓰는 것보다 훨씬 낫게 보인다. $scope$on 이벤트나 $watch를 사용하는 경우에나 필요하다. 이런 방식으로 컨트롤러 클래스인 "뷰모델"을 조금 다르게 작성할 수 있다.

기본적인 함수 작성은 다 끝났다. 하지만 나는 "exports" 스타일을 더 선호하고, 모든 함수와 변수가 바인딩되고, 어떤 적절한 주석이든 작성할 수 있는 형태가 좋다. 이 모든 것을 염두해서 작성하면 다음과 같다:

/**
 * @name fileUpload
 * @desc <file-upload> Directive
 */
function fileUpload () {

  /**
   * @name fileUploadCtrl
   * @desc File Upload Controller
   * @type {Function}
   */
  function fileUploadCtrl() {

    /**
     * @name files
     * @desc Contains all files passed in by the user
     * @type {Array}
     */
    var files = [];

    /**
     * @name uploadFiles
     * @desc Uploads our files
     * @type {Array}
     */
    function uploadFiles() {

    }

    // exports
    this.files = files;
    this.uploadFiles = uploadFiles;

  }

  /**
   * @name link
   * @desc File Upload Link
   * @type {Function}
   */
  function link() {

  }

  return {
    restrict: 'E',
    scope: {},
    template: [
      '<div>',
      '</div>'
    ].join(''),
    controllerAs: 'vm',
    controller: fileUploadCtrl
    link: link
  };

}
angular
  .module('app')
  .directive('fileUpload', fileUpload);

템플릿 융화 Template integration

다음 순서로 <input type=file> 같이 파일 업로드를 위한 엘리먼트를 작성하고 모델에 연결해야 한다. 앞서 작성한 디렉티브에 ng-model 어트리뷰트와 값을 추가하자. 또한 ng-change와 함께 "업로드" 버튼을 추가하자. (그렇다. form이라면 ng-submit을 사용해야 하겠지만 간단하게 작성하기로 한다.)

// ...
template: [
  '<div>',
    '<input type="file" ng-model="vm.files">',
    '<button type="button" ng-change="vm.uploadFiles(vm.files);">',
  '</div>'
].join('')
// ...

이 컨트롤러에 어떻게 업로드를 다뤄야 하는지에 대한 주석을 작성하자. UploadService을 추가했고 (좋은 이름이다) 이 서비스의 의존성이 컨트롤러에 주입될 수 있도록 fileUploadCtrl에 매개변수로 추가했다.

/**
 * @name fileUploadCtrl
 * @desc File Upload Controller
 * @type {Function}
 */
function fileUploadCtrl(UploadService) {

  /**
   * @name files
   * @desc Contains all files passed in by the user
   * @type {Array}
   */
  var files = [];

  /**
   * @name uploadFiles
   * @desc Uploads our files
   * @type {Array}
   */
  function uploadFiles(files) {
    // hand off our files to a Service
    UploadService
    .uploadFiles(files)
    .then(function (response) {
      // success, we could get our file Object back
      // and render it in the View for the user
      // maybe some ng-repeat with a list of files inside
    }, function (reason) {
      // error stuff if not handled globally
    })
  }

  // exports
  this.files = files;
  this.uploadFiles = uploadFiles;

}

잠깐, 별로 많이 변경되지 않았다. 왜지? 왜 그런지 이유를 보자.

서비스 Services (business logic layer)

백엔드와 연결해서 파일을 업로드 하는 작업과 같이 API와 소통하는 무엇이든 절대, 절대 컨트롤러에 작성하지 않는다. 왜냐고? 관계를 분리하는 것이다. 물론 컨트롤러에 작성할 수도 있다. 하지만 컨트롤러를 뷰모델비지니스로직어쩌고가 아닌 뷰모델처럼 사용한다면 우리 삶을 너무나도 힘들게 만든다.

여기서 서비스를 위한 모조 코드를 작성하진 않을 것이지만 왜 추상화된 비지니스 로직을 컨트롤러에 넘겨야 하는가에 대해서 이해하는 것은 아주 중요하다. 디렉티브의 구조와 의존성을 관리에 용이하고 확장 가능하게 구축하기 위해서는 처음부터 고려를 해야한다.

서비스는 컨트롤러(뷰모델)가 사용자에게 데이터를 표현할 수 있도록 필요한 모델 데이터를 복제해서 제공할 수 있어야 한다.

디렉티브는 표현 로직 레이어(컨트롤러)나 비지니스 로직 레이어(서비스)가 다루지 못하는 환상적인 통로를 제공하는데 그건 바로 DOM 문서 객체 모델(Document Object Model)이다. 종종 DOM이 필요한데 Angular는 우리를 위해 준비를 해두었다.

여기서 작성한 파일 업로드 디렉티브는 흑마법 같은 드래그 드랍 없이는 완성되지 않은 것이나 마찬가지니 dragover, drop 같은 DOM 이벤트를 활용하자. 먼저 <div class="drop-zone">을 디렉티브에 추가하고 이 영역을 "드래그 드랍" 영역으로 제공한다.

// ...
template: [
  '<div>',
    '<input type="file" ng-model="vm.files">',
    '<button type="button" ng-change="vm.uploadFiles(vm.files);">',
    '<div class="drop-zone">Drop your files here!</div>',
  '</div>'
].join('')
// ...

이제 디렉티브와 묶어야 한다. link 함수는 여기서 유용하다. 이 함수에 $scope, $element, $attrs를 주입한다. (미안하지만 달러 표시로 프리픽스를 붙이는 것을 좋아한다. iAttrs을 보면 눈물이 앞을 가린다.)

이제 .drop-zone엘리먼트에 특별한 이벤트 리스너를 연결해야 한다. link 함수를 최대한 가볍게 만든다는 점을 명심하자. 여기서 $scope 인자는 정말 드물게 사용하는데 여러분도 그래야 한다.

엘리먼트에 이벤트 리스너를 추가한다:

/**
 * @name link
 * @desc File Upload Link
 * @type {Function}
 */
function link($scope, $element, $attrs) {
  var drop = $element.find('.drop-zone')[0];
  drop.addEventListener('dragenter', function(e) {
    // "dragenter"에 무언가 동작
  }, false);
  drop.addEventListener("dragleave", function(e) {
    // "dragleave"에 무언가 동작
  }, false);
  drop.addEventListener("dragover", function(e) {
    // "dragover"에 무언가 동작
  }, false);
  drop.addEventListener('drop', function(e) {
    // "drop"에 무언가 동작
  }, false);
}

다시 말하지만 나는 깔끔하게 보이는 것을 좋아하니까 주석과 추상성을 좀 더 다듬었다. dragenter, dragleave, dragover는 이 데모에서 필요 없으니 지운다.:

/**
 * @name link
 * @desc File Upload Link
 * @type {Function}
 */
function link($scope, $element, $attrs) {

  /**
   * @name drop
   * @desc Drop zone element
   * @type {Element}
   */
  var drop = $element.find('.drop-zone')[0];

  /**
   * @name onDrop
   * @desc Callback on "drop" event
   * @type {Function}
   * @param {Event} e Event passed in to grab files from
   */
  function onDrop(e) {
    
  }
  
  // events
  drop.addEventListener('drop', onDrop, false);

}

이벤트 리스너를 설정했고 e.dataTransfer.files에서 파일을 집어 업로드 API로 넘겨줄 수 있다. 하지만 같은 함수를 컨트롤러에 있는 uploadFiles 메소드를 사용하고 싶다.

디렉티브 안으로 컨트롤러를 넘겨줄 수 있는데, $ctrl이라는 짧고 귀요미인 별칭을 사용해서 디렉티브에서 컨트롤러에 접근할 수 있도록 만든다. (역주. link가 호출될 때 4번째 인자로 컨트롤러가 제공됨.)

/**
 * @name link
 * @desc File Upload Link
 * @type {Function}
 */
function link($scope, $element, $attrs, $ctrl) {

  /**
   * @name drop
   * @desc Drop zone element
   * @type {Element}
   */
  var drop = $element.find('.drop-zone')[0];

  /**
   * @name onDrop
   * @desc Callback on "drop" event
   * @type {Function}
   * @param {Event} e Event passed in to grab files from
   */
  function onDrop(e) {
    if (e.dataTransfer && e.dataTransfer.files) {
      $ctrl.uploadFiles(e.dataTransfer.files);
    }
  }
  
  // events
  drop.addEventListener('drop', onDrop, false);

}

대박! 컨트롤러의 uploadFiles 메소드를 사용해서 API로 파일을 넘기는데 코드를 다시 사용했다! 이런 방식으로 표현 로직에서의 변경을 그대로 반영할 수 있게 되었다. 앞서 언급한 것처럼 업로드된 파일을 사용자에게 보여줄 때에도 컨트롤러에서 모든 코드를 다시 사용하고 활용할 수 있을 것이다.

하지만 이건 아직 동작하지 않는다... 마법의 코드 $scope.$apply()를 잊었다:

/**
 * @name onDrop
 * @desc Callback on "drop" event
 * @type {Function}
 * @param {Event} e Event passed in to grab files from
 */
function onDrop(e) {
  if (e.dataTransfer && e.dataTransfer.files) {
    $ctrl.uploadFiles(e.dataTransfer.files);
    // force a $digest cycle
    $scope.$apply();
  }
}

파일이 업로드 된 후, $digest 사이클을 실행하도록 $scope.apply()를 추가한다. 파일을 업로드한 과정을 거치고 데이터가 변경된 후에 어플리케이션 또한 변경되도록 한다. 이 과정이 필요한 이유는 Angular 생태계 외부에서 존재하는 drop 이벤트 리스너를 활용했기 때문이다. 외부에 있어서 그 이벤트가 동작하는지, 무슨 일이 어떻게 일어났는지 알려야 할 필요가 있는 것이다.

이제 모든 것이 갖춰졌다:

/**
 * @name fileUpload
 * @desc <file-upload> Directive
 */
function fileUpload () {

  /**
   * @name fileUploadCtrl
   * @desc File Upload Controller
   * @type {Function}
   */
  function fileUploadCtrl(UploadService) {

    /**
     * @name files
     * @desc Contains all files passed in by the user
     * @type {Array}
     */
    var files = [];

    /**
     * @name uploadFiles
     * @desc Uploads our files
     * @type {Array}
     */
    function uploadFiles(files) {
      // hand off our files to a Service
      UploadService
      .uploadFiles(files)
      .then(function (response) {
        // success, we could get our file Object back
        // and render it in the View for the user
        // maybe some ng-repeat with a list of files inside
      }, function (reason) {
        // error stuff if not handled globally
      })
    }

    // exports
    this.files = files;
    this.uploadFiles = uploadFiles;

  }

  /**
   * @name link
   * @desc File Upload Link
   * @type {Function}
   */
  function link($scope, $element, $attrs, $ctrl) {

    /**
     * @name drop
     * @desc Drop zone element
     * @type {Element}
     */
    var drop = $element.find('.drop-zone')[0];

    /**
     * @name onDrop
     * @desc Callback on "drop" event
     * @type {Function}
     * @param {Event} e Event passed in to grab files from
     */
    function onDrop(e) {
      if (e.dataTransfer && e.dataTransfer.files) {
        $ctrl.uploadFiles(e.dataTransfer.files);
        // force a $digest cycle
        $scope.$apply();
      }
    }
    
    // events
    drop.addEventListener('drop', onDrop, false);

  }

  return {
    restrict: 'E',
    scope: {},
    template: [
      '<div>',
        '<input type="file" ng-model="vm.files">',
        '<button type="button" ng-change="vm.uploadFiles(vm.files);">',
        '<div class="drop-zone">Drop your files here!</div>',
      '</div>'
    ].join(''),
    controllerAs: 'vm',
    controller: fileUploadCtrl
    link: link
  };

}
angular
  .module('app')
  .directive('fileUpload', fileUpload);

정리, MVVM (Model-View-ViewModel)

이 접근은 컨트롤러를 뷰 모델로 사용하는 방식이며 link 함수를 DOM 조작에 활용함과 동시에 컨트롤러에 간단한 일을 전달하는 역할을 하도록 처리하는 역할을 한다. 이 접근은 함수 내부에 객체를 제공하는 등의 방법으로 중첩된 여러 계층의 코드를 작성하는 것과 같이 복잡한 방법을 사용하지 않고, 마치 코드 자체가 별도로 구성된 것 같이, 함수를 분리하고 다시 할당하는 방식으로 서로 의존적인 관계를 분리하는 데 더 적합하다.

의견이나 개선점은 GitHub 이슈로 남겨주기 바란다. Enjoy!

Johnpapa의 Do You Like Your Angular Controllers with or without Sugar?를 번역한 글이다. 원본 포스트는 CC BY 2.5 라이센스로 작성되어 있다.

그냥 읽을 때는 괜찮게 느껴졌는데 옮기고 나니 핵심적인 부분이 없는 감상문 느낌이라 아쉬웠다. 덕분에 다른 글도 번역하게 된 좋은 원동력(?)이 되었다. 1.2 이후로 소개된 Controller As에 대해 전통적인 방법과 어떻게 다른지에 대해 설명하고 있다.


Angular 컨트롤러를 작성하는 두가지 방법

Angular 문서만 읽고 왔더라도 $scope를 MVC의 C(컨트롤러)에서 미친듯이 사용하는 모습은 이상하게 보였을 것이다. $scope는 컨트롤러와 뷰 사이를 연결하는 풀과 같은 존재로 데이터 연결이 필요한 모든 경우를 돕는다. 최근 Angular 팀은 컨트롤러에서 $scope를 사용하는 새로운 방식을 공개했다. 이제 $scope(이 단어를 쓰면 전통적인 방식의 컨트롤러에서 쓰는걸 의미함)와 함께 this(Angular 팀과 내가 Controller-As로 사용하는 방식을 의미함)을 사용할 수 있게 되었다. 이 두 가지 기술에 대한 질문을 아주 많이 받았다. 모두가 선택을 좋아하고 동시에 그 선택에서 얻을 수 있는 것이 무엇인지 명확하게 알고 싶어한다. 그래서 Angular에서 컨트롤러를 생성할 때 사용할 수 있는 이 두 가지 방식($scope와 Controller As)에 대해 이야기하고 활용해보자.

전통적인 컨트롤러와 Controller As 모두 $scope를 갖고 있다. 이 점이 이해하는데 가장 중요하다. 어느 한 방식을 선택한다고 다른 장점을 포기하는 것이 아니다. 정말. 이 두가지 방법은 모두 사용된다.

먼저 알아야 할 과거

$scope는 "전통적인" 기법으로 "controller as"는 아주 최근에 나온 기술이다. (공식적으로 1.2.0 pre릴리스에서 나타나지만 불완전했음.) 둘 다 완벽하게 동작하기에 내가 줄 수 있는 지침은 둘 중 하나를 골라 일관되게 사용하라는 것이다. 하나의 앱에서 둘 다 섞어서 사용할 수 있지만, 일관적으로 사용해야 하는 이유는 놀라울 정도로 명확하다. 그러므로 하나를 고르고 주사위를 던져라. 가장 중요한 점은 일관성이다. 어느 것을 골라야 하나? 그 선택은 개발자에게 달렸다. $scope를 이용한 예가 훨씬 많지만 "controller as"도 흐름에 따라 잘 골라야 한다. 둘 중 어느 것이 더 나은가? 논쟁할 만한 주제다. 그렇다면 어떻게 골라야 할까?

"controller as"를 선호하면 숨기기 편하다

중개하는 역할을 하는 객체인 $scope를 사용하면 컨트롤러에서 사용하는 모든 맴버를 뷰에 공개하게 된다. this.*를 설정하는 것으로 컨트롤러에서 뷰에 공개하고 싶은 부분에 대해서만 노출하는 것이 가능하다. 물론 $scope를 사용해도 동일하게 쓸 수 있지만 표준 자바스크립트의 this를 사용하는 것을 선호한다. 종합적으로 보면 개인적인 선호에 따라 Controller As 기법을 더 선호한다. 다음과 같이 코드를 작성한다:

var vm = this;

vm.title = 'some title';
vm.saveData = function() { ... };

이 방식이 더 보기 쉽고 어떤 부분이 뷰에 노출되는지 쉽게 확인할 수 있다. "vm" 변수는 뷰모델(viewmodel)을 의미한다. 이 명칭은 단순하게 내 컨벤션이다. $scope를 사용할 때도 같은 방법을 쓸 수 있지만 $scope를 사용할 때는 그렇게 작성하지 않았다.

$scope.title = 'some title';
$scope.saveData = function() { ... };

결국 이 부분은 작성자에게 달려있다.

주입이 필요한 경우

$scope는 컨트롤러에 $scope를 주입할 필요가 있을 때 사용한다. 이 부분은 controller as 기법을 사용할 때는 필요 없는 부분이지만 몇가지 다른 이유에 의해 필요할 때가 존재한다. (가령 $broadcast가 필요하거나, watch를 사용할 필요가 있는데 컨트롤러 내에서 하는 것을 피하고 싶을 때.) 이 부분은 사실 Controller As 기법을 더 좋아하는 이유 중 하나다. $scope가 데이터 바인딩 등을 위해 정말 필요한 상황일 때만 명시적으로 선언하기 때문이다. broadcast 메시지를 듣기 위한 것도 한 예제다. watch는 다른 경우지만 컨트롤러 내에서 watch하고 싶지 않은 경우에 사용할 수 있다.

유행은?

명시적으로 $scope가 선언된 코드가 더 오래 사용한 방식이기 때문에 예제가 많다. 하지만 최근 예제는 Controller As를 사용한 경우가 많다. 이 예제를 원한다면 Visual Studio 플러그인인 SideWaffle을 사용할 수 있다. 이 두가지 기법 컨트롤러 모두를 지원한다. 설탕이 싫다면 전통적인 $scope 컨트롤러를 선택하라. 설탕을 원한다면 controller as 를 선택하라. Angular 팀은 이 두가지 선택지를 제공하고 있고 이 선택지 모두 마음에 든다. 개인적으로는 Controller As 기법이 마음에 든다. 이 두가지 방법 모두 데이터 바인딩을 할 수 있다. Controller As는 $scope와 개발하는데 더 편리하게 한다고 생각한다. 그러니 둘 중 어느 것을 선택하는가는 온전히 당신의 몫이다.

Todd Motto의 글 Digging into Angular’s “Controller as” syntax를 번역했다. Angular의 Controller As 문법에 대해 설명하고 있는 글이다. $scope를 분리하는 것으로 더 사용성 높은 컨트롤러를 만들 수 있고 최근 ES6에서 클래스를 만드는데 좋은 호환성을 보장하고 있다는 얘기를 듣고 번역하게 되었다.


AnularJS 컨트롤러는 최근 몇가지 변화가 있었다. (정확하게는 버전 1.2부터.) 스코프, 컨트롤러와 Angular 개발에 있어서 이 의미는 꽤 희미하면서도 아주 강력한 변화다. 이 변화는 구조를 향상하고 더 깔끔한 스코프와 똑똑한 컨트롤러를 만드는데 일조한다.

우리가 알고 있는 컨트롤러는 클래스 같은(class-like) 객체로 Model과 View를 변경하는데 사용되지만, 이 모든 과정이 수수께끼 같은 $scope 객체에 의해 이뤄진다. 많은 개발자가 this 키워드를 $scope 대신 사용하는 것을 추천하고 있어 Angular 컨트롤러에서 $scope가 선언되어 있는 방식을 변경하도록 압박하고 있다.

v1.2.0 이전의 컨트롤러는 다음과 같이 생겼다:

// <div ng-controller="MainCtrl"></div>
app.controller('MainCtrl', function ($scope) {
  $scope.title = 'Some title';
});

늘 컨트롤러에 $scope를 주입했었지만, 다음은 컨트롤러를 $scope로부터 분리한 개념이다. 이 방식이 더 낫다고 논의되었다:

app.controller('MainCtrl', function () {
  this.title = 'Some title';
});

별로 한 일은 없지만 이 과정으로 좀 멋진 결과를 얻을 수 있게 되었다.

클래스로서 컨트롤러

자바스크립트에서 "class"를 인스턴스화(instantiation) 하면, 다음과 같을 것이다:

var myClass = function () {
  this.title = 'Class title';
}
var myInstance = new myClass();

이렇게 선언 후 myInstance 인스턴스를 사용해 myClass의 메소드와 프로퍼티에 접근할 수 있다. Angular에서는 이와 비슷한 방식으로 접근하는 방법으로 Controller as 문법을 제공하게 되었다. 다음은 어떻게 선언하고 바인딩 하는지에 대한 예제다:

// 선언은 평소같이 하지만 `$scope` 대신 `this`를 사용
app.controller('MainCtrl', function () {
  this.title = 'Some title';
});

이 방법은 더 클래스 기반 설정을 사용할 수 있게 되어, 이 컨트롤러를 DOM에서 인스턴스화 할 때 쉽게 변수에 할당할 수 있게 된다:

<div ng-controller="MainCtrl as main">
  // MainCtrl은 존재하지 않고, 대신 `main` 인스턴스를 얻을 수 있음
</div>

this.title을 DOM에 반영하기 위해서는 새 인스턴스를 사용하면 된다:

<div ng-controller="MainCtrl as main">
   {% raw %}{{ main.title }}{% endraw %}
</div>

스코프를 네임스페이스로 처리할 수 있는 것은 아주 좋은 접근이라고 생각하며 Angular를 엄청나게 깔끔하게 한다고 생각한다. 난 항상 {% raw %}{{ title }}{% endraw %} 같이 "떠있는 변수(모호한 변수)"를 싫어했는데, {% raw %}{{ main.title }}{% endraw %} 처럼 인스턴스와 함께 작성할 수 있는 방식은 훨씬 마음에 든다.

중첩된 스코프

중첩된 스코프도 Controller as 문법에서 얻을 수 있는 결과인데, 가끔 현재 스코프의 $parent 프로퍼티에 접근해 상위 스코프에서 필요한 부분을 받아와야 할 필요가 있었다.

다음 예제를 보자:

<div ng-controller="MainCtrl">
  {% raw %}{{ title }}{% endraw %}
  <div ng-controller="AnotherCtrl">
    {% raw %}{{ title }}{% endraw %}
    <div ng-controller="YetAnotherCtrl">
      {% raw %}{{ title }}{% endraw %}
    </div>
  </div>
</div>

먼저 {% raw %}{{ title }}{% endraw %} 를 반복적으로 사용하는데다 여러 스코프의 경계를 오가고 있기 때문에 이 값이 어디서 들어오는지 아주 모호하고 혼란스러운 인터폴레이션(interpolation) 이슈가 발생한다. 어느게 무엇이 될 지도 예측하기 어렵다. 스코프를 가로질러 변수에 접근하는 것은 이해하는데 훨씬 명확하다:

<div ng-controller="MainCtrl as main">
  {% raw %}{{ main.title }}{% endraw %}
  <div ng-controller="AnotherCtrl as another">
    {% raw %}{{ another.title }}{% endraw %}
    <div ng-controller="YetAnotherCtrl as yet">
      {% raw %}{{ yet.title }}{% endraw %}
    </div>
  </div>
</div>

또한 부모 스코프에 다음과 같이 작성하지 않고도 접근할 수 있다:

<div ng-controller="MainCtrl">
  {% raw %}{{ title }}{% endraw %}
  <div ng-controller="AnotherCtrl">
    Scope title: {% raw %}{{ title }}{% endraw %}
    Parent title: {% raw %}{{ $parent.title }}{% endraw %}
    <div ng-controller="YetAnotherCtrl">
      {% raw %}{{ title }}{% endraw %}
      Parent title: {% raw %}{{ $parent.title }}{% endraw %}
      Parent parent title: {% raw %}{{ $parent.$parent.title }}{% endraw %}
    </div>
  </div>
</div>

그리고 더욱 논리적이다:

<div ng-controller="MainCtrl as main">
  {% raw %}{{ main.title }}{% endraw %}
  <div ng-controller="AnotherCtrl as another">
    Scope title: {% raw %}{{ another.title }}{% endraw %}
    Parent title: {% raw %}{{ main.title }}{% endraw %}
    <div ng-controller="YetAnotherCtrl as yet">
      Scope title: {% raw %}{{ yet.title }}{% endraw %}
      Parent title: {% raw %}{{ another.title }}{% endraw %}
      Parent parent title: {% raw %}{{ main.title }}{% endraw %}
    </div>
  </div>
</div>

깔끔하지 않은 $parent 호출을 더이상 안해도 된다. 만약 컨트롤러의 위치가 DOM 또는 스택 내에서 변경된다면, $parent.$parent.$parent.$parent를 연쇄적으로 변경해야만 한다! 어휘적으로 스코프에 접근할 수 있는 것이 훨씬 편리하다.

$watchers/$scope 메소드

Controller as 문법을 맨 처음 사용하고서 "오, 대박!" 이랬지만, 스코프 관찰자(watchers)나 메소드를 사용하기 위해서는 $scope의 의존성을 주입할 필요가 있다. (예를 들면 $watch, $broadcast, $on 같은 것을 사용해야 할 때.) 웩, 이 부분을 얼마나 피하려고 노력했는데 말이다. 하지만 이조차도 대박인 것을 알게 되었다.

Controller as 문법이 동작하는 방식은 $scope 같은 클래스 같은 객체가 되는 것이 아니라, 컨트롤러가 현재 $scope바인딩 하도록 하는 방식이다. 나에게는 클래스와 Angular의 특별한 기능을 분리하는 핵심적인 방식이 되었다.

이 의미는 다음 같이 클래스 같은 컨트롤러를 갖고 있다는 뜻이다:

app.controller('MainCtrl', function () {
  this.title = 'Some title';
});

이 기능 이전에 또는 일반적인 바인딩 이상의 기능이 필요할 때, $scope를 의존성으로 넣어, 그냥 컨트롤러보다 훨씬 강력하고 특별한 기능을 활용할 수 있게 되었다.

이 특별한 기능은 $scope의 메소드로 모두 포함되어 있다. 다음 예제를 보자:

app.controller('MainCtrl', function ($scope) {
  this.title = 'Some title';
  $scope.$on('someEventFiredFromElsewhere', function (event, data) {
    // do something!
  });
});

꼬인 문제 다리미질 하기

이 코드는 $scope.$watch() 예제를 작성하는 동안 나타난 흥미로운 문제다. 아주 단순한 예제지만 Controller as 문법에서는 예상한대로 동작하지 않는다:

app.controller('MainCtrl', function ($scope) {
  this.title = 'Some title';
  // doesn't work!
  $scope.$watch('title', function (newVal, oldVal) {});
  // doesn't work!
  $scope.$watch('this.title', function (newVal, oldVal) {});
});

헤헤, 그래서 여기서 뭘 할 수 있나? 재밌게도 다른 날 이 코드를 읽었을 때, 이 부분에서 $watch()에게 첫 인자를 함수로 넘겨주면 해결할 수 있는 문제인 것을 알 수 있었다:

app.controller('MainCtrl', function ($scope) {
  this.title = 'Some title';
  // 음.. 함수로 쓰면,
  $scope.$watch(function () {}, function (newVal, oldVal) {});
});

그 의미는 여기서 작성한 this.title을 참조로 넘길 수 있다는 뜻이다:

app.controller('MainCtrl', function ($scope) {
  this.title = 'Some title';
  // 이러면 되겠군...
  $scope.$watch(function () {
    return this.title; // `this`가 위에서 말한 `this`가 아니네!!
  }, function (newVal, oldVal) {});
});

컨텍스트를 angular.bind()를 사용해 변경하자:

app.controller('MainCtrl', function ($scope) {
  this.title = 'Some title';
  // 짠
  $scope.$watch(angular.bind(this, function () {
    return this.title; // 이 `this`가 위 `this`와 같음
  }), function (newVal, oldVal) {
    // 이제 newVal과 oldVal의 변화를 잡을 수 있음
  });
});

역주. IE9 이상을 지원한다면 angular.bind 대신 Function#bind를 사용해도 되고, John Papa의 방식대로 var vm = this; 식으로 작성해 회피해도 된다.

$routeProvider/디렉티브/그 외 아무곳에나 선언하기

컨트롤러는 동적으로 배정될 수 있으므로 항상 어트리뷰트로 연결해둘 필요가 없다. 디렉티브 내에서 controllerAs: 프로퍼티를 사용할 수 있고, 이 프로퍼티는 쉽게 배정할 수 있다:

app.directive('myDirective', function () {
  return {
    restrict: 'EA',
    replace: true,
    scope: true,
    template: [].join(''),
    controllerAs: '', // 쉽고 편하다!
    controller: function () {}, // 이 컨트롤러를 위 controllerAs 의 이름으로 인스턴트화 할 것임
    link: function () {}
  };
});

$routeProvider 내에서도 동일하다:

app.config(function ($routeProvider) {
  $routeProvider
  .when('/', {
    templateUrl: 'views/main.html',
    controllerAs: '',
    controller: ''
  })
  .otherwise({
    redirectTo: '/'
  });
});

controllerAs 문법 테스트하기

controllerAs를 테스트하는데 미묘하게 다른데 고맙게도 $scope를 주입할 필요가 없다. 이 의미는 컨트롤러를 테스트할 때 참조하는 프로퍼티를 넣을 필요가 없다는 뜻이다. (vm.prop 같은 부분.) 이제 간단하게 $controller에 변수명을 지정하는 것만으로 테스트할 수 있다.

// controller
angular
  .module('myModule')
  .controller('MainCtrl', MainCtrl);

function MainCtrl() {
  this.title = 'Some title';
};

// tests
describe('MainCtrl', function() {
  var MainController;

  beforeEarch(function(){
    module('myModule');

    inject(function($controller) {
      MainController = $controller('MainCtrl');
    });
  });

  it('should expose title', function() {
    expect(MainController.title).equal('Some title');
  });
});

controllerAs 문법을 사용했을 때 $controller 함수로 인스턴스화 하는 것 대신에 $scope를 주입해야 할 필요가 있는 경우에는 $controller에 다음과 같이 객체로 넘겨주면 된다. (scope.main 인스턴스에서 사용될) 컨트롤러를 위한 이 alias는 $scope를 (실제 Angular 앱처럼) 추가하게 된다. 하지만 그다지 아름다운 해법은 아니다.

// Same test becomes
describe('MainCtrl', function() {
  var scope;

  beforeEarch(function(){
    module('myModule');

    inject(function($controller, $rootScope) {
      scope = $rootScope.$new();
      var localInjections = {
        $scope: scope,
      };
      $controller('MainCtrl as main', localInjections);
    });
  });

  it('should expose title', function() {
    expect(scope.main.title).equal('Some title');
  });
});

이 글은 Todd Motto의 글 A better way to $scope, angular.extend, no more “vm = this”를 번역한 글이다.

Angular에서 Controller As 문법을 사용하면 var vm = this;와 같이 this의 스코프 문제를 회피하는 방식으로 작성하는데 이 방법을 회피하기 위해 angular.extend를 활용하는 방식을 제안하고 있다. 댓글에도 많은 반론이 있는 글이라서 댓글도 따라 번역했다. 반론에는 문법이 익숙하지 않다고 하지만 이 문법이 더 명시적인 느낌이 드는 편이다. 다만 반론과 같이, angular에 대한 의존성을 높이는 방식이고 ES6 클래스와는 어울리지 않는 문법이란 부분에서 단점도 크게 느껴진다.


$scope를 사용하는 더 나은 방법, angular.extend, 더이상 "vm = this"는 없다

Angular 컨트롤러는 지난 1년간 발전해왔다. 이제는 많은 사람들이 가장 최근에 추가된 "컨트롤러" 문법인 controllerAs 스타일을 활용하고 있다. ($scope를 직접 바인딩하는 방식을 멀리 하고서 말이다.)

스타일에 대한 다양한 의견 중 내가 수용하고 있는 방식은, 컨트롤러 가장 상위에 var vm = this;를 우선적으로 선언하는 것이다. 최근에는 vm을 실제 자바스크립트 컨트롤러 내에서 사용하는 것을 멀리하고 있다. 대신 평범한 자바스크립트 변수와 함수로 작성한 후, 필요로 하는 부분을 "exports"와 같은 방식으로 외부에서 바인딩 할 수 있게 작성하고 있다.

내 이전 작업과 어떻게 다른지 살펴보기 위해 var vm = this; 부터 시작하자.

var vm = this;

이 방법은 컨트롤러를 변수와 바인딩하기 위한, 아주 유명한 방법이다. (결국 $scope 와 연결된다.) 단순한 예제를 살펴보자:

function MainCtrl () {

  var vm = this;

  function doSomething() {

  }

  // exports
  vm.doSomething = doSomething;

}

angular
  .module('app')
  .controller('MainCtrl', MainCtrl);

이 패턴은 굉장하며 Angular로 개발하는데 아주 유용하다. (여기에는 함수를 직접 선언하지 않고 vm.doSomething = function () {}와 같이 바로 바인딩하는 변형도 있다.) vm을 생성하는 이유는 다른 함수 내에서 올바른 문맥을 참조하기 위해서인데 this는 다른 변수와는 달리 어휘 스코핑(lexical scoping)를 따르지 않기 때문이다. 그래서 thisvm에 "참조"로 배정해놓고 사용하는 것이다.

많은 내용은 바인딩해야 할 때, vm을 엄청 많이 반복해서 사용하고 끝내 vm.* 참조가 코드 전반에 생기게 된다. 사실 잘 따져보면 모든 코드를 this에 직접 바인딩할 필요가 없고, JavaScript는 그 자체 인스턴스에 포함된 변수로도 충분히 동작할 수 있다. (예를 들면, 콜백에서 vm.foo를 사용하는 방식보다 var foo = {};와 같이 업데이트를 지역적으로 수행하는 방식이 낫다.) 다음은 vm.* 바인딩을 많이 사용한 경우에 대한 예시다:

function MainCtrl () {

  var vm = this;

  function doSomething1() {}
  function doSomething2() {}
  function doSomething3() {}
  function doSomething4() {}
  function doSomething5() {}
  function doSomething6() {}

  // exports
  vm.doSomething1 = doSomething1;
  vm.doSomething2 = doSomething2;
  vm.doSomething3 = doSomething3;
  vm.doSomething4 = doSomething4;
  vm.doSomething5 = doSomething5;
  vm.doSomething6 = doSomething6;
}

angular
  .module('app')
  .controller('MainCtrl', MainCtrl);

angular.extend 사용하기

angular.extend라고 알고 있는 이 방식은 새로운 아이디어는 아니지만, Modus Create의 글 AngularJS: Tricks with angular.extend()에서 아이디어를 얻게 되었고, 내 angular 컨트롤러 전략/패턴에서 vm 참조를 완전히 제거하게 되었다. 이 글에서는 angular.extend($scope, {...});를 사용하고 있지만, 내 예제에서는 controllerAs 문법으로 차용하고 있다.

다음은 vm을 버리고 this에 간단히 바인딩하는 간단한 예제다:

function MainCtrl () {
    this.someVar = {
      name: 'Todd'
    };
    this.anotherVar = [];
    this.doSomething = function doSomething() {
    };
}

angular
  .module('app')
  .controller('MainCtrl', MainCtrl);

angular.extend를 사용하면 깔끔하고 더욱 객체 주도적인 코드를 얻을 수 있고, 아이템 목록을 넘겨주는 대신 단순한 exports 객체를 넘겨줄 수 있다:

function MainCtrl () {
  angular.extend(this, {
    someVar: {
      name: 'Todd'
    },
    anotherVar: [],
    doSomething: function doSomething() {

    }
  });
}

angular
  .module('app')
  .controller('MainCtrl', MainCtrl);

이 방식이 this 키워드를 반복하지 않아도 되게 한다. (또는 $scope를 여전히 사용하고 있다면 $scope 또한 반복하지 않아도 된다.)

이 방식을 사용하면 "private" 메소드를 사용하는데도 좀 더 편리하고 명확하게 작성할 수 있다:

function MainCtrl () {
  
  // private
  function someMethod() {

  }

  // public
  var someVar = { name: 'Todd' };
  var anotherVar = [];
  function doSomething() {
    someMethod();
  }
  
  // exports
  angular.extend(this, {
    someVar: someVar,
    anotherVar: anotherVar,
    doSomething: doSomething
  });
}

angular
  .module('app')
  .controller('MainCtrl', MainCtrl);

이 방식에 대한 다른 생각이나 좋은 예제가 있는지 궁금하다.


댓글에 달린 다른 의견 1, 2에서 공감가는 부분을 옮겨보자면, 이 방식에는 다음과 같은 단점이 존재한다.

  1. 코드 복잡도가 증가하는 것처럼 보인다. (vm에 비해 복잡하게 보인다.)
  2. angular.extend() 을 사용하는 것으로 angular에 대한 강결합이 발생한다. ES6 클래스 등을 사용할 때 불편하다.
  3. doSomething에서 someVar를 참조하는 과정이 복잡해진다.
  4. 느리다.

전공 종사자는 물론 비전공으로 이 분야에 일하게 된 사람이라면 논문을 읽어야 하는가에 대한 고민을 해봤을 것이다. 이 포스트는 Michael Robert BernsteinShould I read papers?를 번역한 글이다. 짧은 글이지만 논문을 읽어야 하는가에 대한 고민을 하고 있다면 조금이나마 자극이 되지 않을까 싶어 번역했다.


제가 논문을 읽어야 하나요?

한 줄 요약: 네.

논문, 논문, 논문. 근래 들어 논문은 아주 많은 주목을 받고 있다. 트위터, 세계 모든 밋업에서, 깃헙 리포지터리에서, 그리고 엄청 많은 사람들이 논문을 읽는 것에 관해 얘기하고 있다. 이 논문에 대한 모든 대화에서 사람들은 이 질문을 빼놓지 않는다.

"제가 논문을 읽어야 하나요?"

내 생각엔 그래야 한다. 난 공식적으로 좀 자주 얘기하는 편이고 다양한 이유로 계속 주장하는데, 논문을 읽는 것은 어떤 분야든 상관없이 모든 사람에게 좋은 생각이기 때문이다.

"제가 논문을 읽어야 하나요?"

이 세 단어에, "해야" 한다는 가장 도전적인 일일 것이다. 그러니까 논문을 "읽어야" 하냐니 무슨 의미인가? 혹시 내가 그렇게 말한 사람인가? 정말 내가 당신의 행동을 독재하길 원하는 것처럼 보이나?

아니다. 난 정말 그런 뜻이 아니다. 논문을 읽고 싶지 않다면 읽지 마라. 안 읽어도 괜찮다. 하지만 당신이 해야 한다고 생각하는 이유는 다음과 같다.

여기서 읽어야라는 표현은 건강한 음식을 먹어야 한다, 가능한 한 깨끗한 공기를 마셔야한다 같은 느낌으로 사용한 것이다. 자기 일에서 동기를 가져야 한다면, 그 분야를 매일 조금씩이나마 이해하고 자신을 향상하기 위해서 무엇이든 해야 한다고 생각한다. 나는 그런 의미에서 사용했다.

"제가 논문을 읽어야 하나요?"

여기에서 "제가"는 누구를 의미하는가? 현재 내 환경에서는 컴퓨터와 어떻게든 연관을 맺고 있는 사람들이지만, 먼저 소프트웨어 개발자를 지칭하고자 한다.

나는 운 좋게도 컴퓨터로 뭔가 만드는 방법을 알려주는 멋진 사람들과 함께 공부할 수 있었고 그 사람들이 어떻게 일하는지 알아야 할 필요가 있었다. 컴퓨터가 제공하는 예술적, 창의적 도구를 현재 상태 그대로 받아들이지 않고서, 속을 살피고, 모방하고, 그리고 자기 자신만의 도구를 만들었다.

만약 자신이 컴퓨터 앞에 매일 앉아 자신 또는 다른 사람들을 위해 무언가를 만드는 사람 중 하나라면, 당신은 "제가"에 해당한다.

"제가 논문을 읽어야 하나요?"

기가 막힌 인류 언어의 탄력성 덕분에 여기서 "읽다"의 문맥은 아주 큰 의미를 지닌다. 읽다가 하나의 의미만 갖는가? 만약 하나의 의미만 있다면 다음처럼 얘기해야 한다:

"만약 처음부터 끝까지 모든 내용을 파고들었다면 그건 무언가를 읽은 것이다."

하지만 "제가 논문을 읽어야 하나요?"에서 "읽다"의 의미는 아주 멋지게 좁은 정의로 사용되었다. 논문을 읽는 것은 논문과 가장 표면적으로 소통하는 방법이다:

  • 인용된 연구를 훑어본다.
  • 저자의 이름을 구글에서 검색하고 무슨 일을 했는지 확인한다.
  • 초록에 있는 내용을 살펴보고 그 분야의 지식을 얻는다.

논문을 "읽는" 것은 논문과 어떻게 관계하는가에 해당한다. 물론 장기간에 걸쳐 읽을 수 있다. 자리 펴고 앉아 "읽기 좋은 글이군" 하며 오랜 시간을 써 읽을 수도 있다. 하지만 빠르게 읽고서 그 글에 읽을 가치가 있다면, 그때 가서 다시 읽고 또 읽으면 된다.

다시 말해서, "너무 멍청하게" 논문을 읽을 필요는 없다. 누구든, 어떤 논문이든 원하는 시간에 읽을 수 있다. 겁내지 말고 용기를 가져라. 무언가를 배우게 될 것이다.

대신 "논문을 읽는 것은 블로그 글이나 소설을 읽는 것과 다르다"는 점을 기억하고 당신은 혼자가 아니라는 점을 잊지 말자.

질문이 "제가 논문을 읽어야 하나요?"지만 정말 혼자인 경우는 드물다. 어딘가에는 분명 당신과 비슷한 길로 가는 사람이 존재한다. 단지 그 사람을 아는 것 뿐만 아니라 거의 서로 만날 수도 있다.

"제가 논문을 읽어야 할까요?"

드디어 이 부분에 도달했다. (이 부분이 맨 처음 나왔어야 했나?) 무엇이 "논문"인가? 논문은 양식에 맞춰 작성해서 구체화한 개념이다. 논문은 작을 수 있다. 하지만 그 크기에 비해 엄청난 내용일 수도 있다.

이 논문은 누군가 찾은 아이디어 일 수도 있고, 인생 내내 연구한 결과거나, 또는 그사이 어떤 것이든 될 수 있다.

앞서 읽는 것에 대한 정의와 같이 유연하게 접근하면 논문에 대해 어떻게 생각해야 하는지 알 수 있다. 논문을 작성했다고 그 작성한 사람 또는 사람들이 당신보다 "똑똑할" 필요는 없다. 그들은 분명 우리와 다른 경험을 갖고 살고 있을 것이다. 그들이 어디서 오고 무엇을 하려고 하는지 누가 알겠는가?

결론

"제가 논문을 읽어야 하나요?"에 대한 내 답은 완전히 yes다. 내 진짜 의미로는, 부담스러울 정도로 읽어야 한다.

자기 자신에게 도전하라.

상상할 수 있는 것보다 훨씬 많은 부분이 표면 아래 숨어있다.

이 글을 작성할 수 있게 도와준 Zeeshan Lakhani, Tom Santero, James Golick, Alex Kahn, Ken Keiter에게 감사의 말을 전한다.

Jennifer Raff블로그는 최근에야 알게 되었는데 자기 연구 분야에 대해 공유하는 포스트가 인상적인 내용이 많았다. 또한, 연구 분야 외에도 비전문가를 위해 논문을 읽고 분석하는 방법을 제시하면서 실제로 그 과정을 보여주는 포스트도 인상 깊었다. 이 포스트는 Jennifer의 How to read and understand a scientific paper: a guide for non-scientists를 번역한 글이다. 글에서 언급했듯 모든 내용을 동의하지 않을지도 모르지만 논문을 읽어본 경험이 없다면 좋은 지침이 되리라 생각한다. 그리고 앞서 번역한 글과는 또 다른 접근 방식을 채택하고 있다. 모든 일이 그렇듯 한가지 방법만 있는 것이 아니므로 자기 상황에 맞는 방법을 전략적으로 참고하면 좋겠다.


지난주 포스트(예방 접종에 대한 진실: 당신의 의사가 구글 대학보다 더 많이 알고 있습니다)가 열띤 토론에 불을 지폈고, 댓글을 작성한 몇명은 이 글로 인해 자신이 작성한 논문에 대한 오류가 입증되기 때문에 나(그리고 다른 독자)를 설득하려 했다. 그 댓글을 읽고 자기 생각을 덧붙이라고 권장하기 전에, 토론을 해볼 만 한 더 큰 이슈에 초점을 맞춰보고 싶다. 무엇이 과학적 권위를 구성하는가?

이 이슈는 단순하게 즐거운 학술적 문제에 그치지 않는다. 과학이 잘못된 방향으로 가는 것은 실제로 매우 중대한 문제다. 예를 들면, "독소"에 대한 두려움과 기도로(또는 식이요법, 운동, 그리고 "청결") 질병의 감염을 예방할 수 있다는 생각으로 커뮤니티가 아이들에게 예방 접종을 하지 않았을 때, 문제는 폭발적으로 나타나게 된다.

"회의적으로 접근하라. 하지만 증거를 찾는다면, 증거를 수용해라." – Michael Specter

충분한 증거를 구성하는 것은 무엇인가? 이 질문에 대해서는 모두 다른 답을 갖고 있다는 것은 명백하다. 하지만 과학적 주제에 대해 제대로 된 교육을 받은 의견이 형태를 갖추기 위해서는, 해당 영역에서 진행되는 현재 연구와 친숙해질 필요가 있다. 그러기 위해서 "주요 연구 논문(primary research article)" (종종 "문헌" 이라 불리는) 을 읽어야 한다. 이전에 과학 논문을 읽어봤다면 난해하고 허풍스런 문체와 익숙지 않은 용어에 좌절한 경험이 있을 것이다. 나도 같은 경험에 대한 기억이 있다. 연구 논문을 읽고 이해하는 것은 모든 박사와 과학자가 학부 시절에 습득하는 기술이다. 어떤 기술이든 인내와 연습으로 얻을 수 있는 것처럼 당신도 이 기술을 배울 수 있다.

나는 사람들이 더 많이 과학적으로 문헌에 기반을 둔 의견을 낼 수 있도록 돕고 싶다. 그래서 비전문가라도 어떻게 과학적 연구 논문을 읽고 이해하는데 어떻게 접근해야 하는지에 대해 안내하기 위해 이 글을 작성했다. 이 글은 과학이나 의학에 배경이 전혀 없는 사람에게 적합하다. 또한 독자가 논문에 대한 기초적인 이해와 그 연구가 제대로 된 연구인지 아닌지에 대한 결정을 내릴 수 있는 것으로 가정하는 것을 기본으로 한다.

여기서 이야기하는데 인용하고자 하는 과학 논문은 주요 연구 논문이다. 이 논문은 특정 질문(또는 질문들)에 대한 새로운 연구로, 상호 비평(peer-reviewed)을 받은 보고서에 해당한다. 또 다른 유용한 형태의 출간문은 **비평 논문(review article)**이다. 비평 논문 또한 교차 평가를 받았고 새로운 정보를 제시하기보다는 여러 주요연구문헌의 결과를 요약해 어떤 합의와 논쟁, 결론 없는 질문이 이 분야에 있는지 요약한다. (여기서 이 내용에 대해 더 많이 작성하진 않지만 비평 논문을 읽을 때는 주의해야 한다. 이런 논문은 출간 당시의 상황을 기준으로 하기 때문이다. 2003년에 출간되어 2001년에 연구된 연구를 비평한 내용을 2013년에 읽는다면 아주 유용하지는 않을 것이다. 대부분의 연구는 비평 논문이 발표된 그해에 이미 그 연구 내용을 대부분 변경한다.)

시작하기 전에: 몇 가지 일반적인 조언

과학적 논문을 읽는 것은 과학에 대한 블로그나 신문기사를 읽는 것과는 완전히 다른 과정이다. 단순히 다른 순서로 정리된 섹션을 읽는 것뿐만 아니라 읽으며 필기하고, 여러 번 반복해서 읽고, 더 자세한 내용을 보기 위해 다른 논문도 아마 살펴봐야 할 것이다. 처음에는 논문 하나 읽는데도 시간이 오래 걸린다. 인내심을 가져라. 경험이 늘어날 때마다 이 과정은 더욱 빨라진다.

대다수의 주요 연구 논문은 초록(Abstract), 서론(Introduction), 절차, 결과, 그리고 결론/해석/토론으로 구성된다. 이 순서는 어느 저널에서 출간하느냐에 따라 달라진다. 어떤 저널에서는 연구에 대한 중요 내용이 포함된 추가 파일을 요구한다. (온라인 정보 부록 Supplementary Online Information 이라 부른다.) 만약 이런 저널에 온라인으로 출간할 때는 논문에 포함되어야 한다. (이 파일을 건너뛰지 않도록 주의해야 한다.)

논문 읽기를 시작하기 전에 저자와 협회를 기록한다. 협회에 따라 제출자를 잘 존중하기도 하지만 (e.g. University of Texas) 어떤 협회는 법적으로는 연구 협회지만 실제로는 의제 중심인 경우도 있다. (e.g. Discovery Institute) 팁: "Discovery Institute"를 구글에서 검색해보면 여기에서 나온 논문이 진화론과 관련해 과학적 권위를 갖기 어렵다는 사실을 알 수 있다.

또한, 저널이 어디에서 출간되었는지도 기록한다. 좋은 평판의 (생물의학) 저널은 Pubmed와 같은 곳에 색인이 있다. 물론 생물의학 저널이 아니라면 Pubmed에 없다. 더 넓은 과학 영역에서의 저널 색인은 Web of Science에서 확인할 수 있다. 불확실한 저널은 주의한다.

읽는 동안, 이해할 수 없는 __모든 단어__를 적고 확인해봐야 한다. (그렇다, 모든 단어다. 이게 완전 고통스럽다는 것은 잘 알고 있다. 하지만 어휘를 이해하지 못하면 이 논문을 이해할 수 없다. 과학에서 쓰이는 어휘는 극단적으로 정확한 의미를 담고 있다.)

주요 연구 논문을 읽는 단계별 방법

초록이 아닌 서론부터 읽기 시작한다

초록은 밀도 높은 첫 단락으로 논문에서 가장 앞에 위치한다. 많은 비과학자가 논문에서 이 부분을 먼저 읽고 과학적인 논의를 시작한다고 하는 경우가 종종 있다. (최악의 관례다. 절대 하지 말아야 한다.) 나는 읽기 위한 논문을 고를 때, 제목과 초록의 조합에 기초해 내 흥미와 연관성이 있어 보이는 논문을 고른다. 하지만 깊게 읽기 위해 조합된 여러 논문 묶음이라면, 나는 항상 초록을 가장 __마지막__에 읽는다. 이런 방식을 사용하는 이유는 초록이 전체 논문의 내용을 간결하게 요약한 것으로 이것을 먼저 읽는 것으로 연구 결과에 대한 저자의 해석을 무의식중에 택하게 되는 경우가 발생할지도 모른다는 우려 때문이다.

큰 질문을 확인한다

"이 논문은 무엇에 대한 것인가" 식의 질문이 아니라 "이 전체 영역에서 해결하려고 하는 문제는 무엇인가"에 대해 질문해야 한다.

이 질문은 왜 이 연구가 수행되는가 하는 점에 초점을 맞출 수 있다. 주제에 대한 연구에 어떤 동기부여가 있었는지 그 증거를 자세히 확인한다.

다섯 문장 또는 그보다 적게 논문의 배경에 대해 요약한다

여기 도움이 될 만한 질문이 있다:

이 분야에서 큰 질문에 답변하기 이전까지 완료된 연구는 무엇인가? 그 연구에서의 제약은 무엇인가? 저자에 따르면 다음 완료되어야 할 연구는 무엇인가?

이 다섯 가지 질문에 대한 것은 좀 제멋대로인 경향이 있지만, 연구의 맥락에 대해 생각하게 하고 간결하게 접근할 수 있도록 강제성을 준다. 연구를 이해하기 위해 왜 이 연구가 완료되었는지에 대해 설명할 수 있어야 한다.

세부적인 질문을 확인한다

저자가 이 연구를 통해 정확히 답변하고자 하는 것은 무엇인가? 이 질문엔 여러 답이 있을 수도 있고 단 하나의 답이 있을 수도 있다. 그 질문을 적어본다. 만약 그 연구를 한번 또는 그 이상 점검해서 비어있는 가설(귀무가설)이 있는지 확인한다.

_비어있는 가설_이 어떤 의미인지 확신이 없다면 이 글을 읽어본다. 내가 앞서 작성한 포스트에서 언급한 글(이런 글)을 읽고 어떤 부분이 비어있는 가설인지 확인한다. 물론 모든 논문에 대해서 비어있는 가설을 확인할 필요는 없다는 사실을 기억하자.

접근 방식을 확인한다

저자가 세부적인 질문들에 대해 어떻게 답변하려고 하는가?

이제 절차(methods)를 읽는다. 각각의 실험에 대한 다이어그램을 그리고 저자가 정확히 무엇을 했는지 표현한다.

내 뜻은 말 그대로 그림을 그려야 한다. 그 연구에 대해 완전히 이해할 수 있을 만큼 세부적인 내용을 포함한다. 다음 그림은 내가 오늘 읽은 논문의 절차를 그림으로 그린 것이다. (Battaglia et al. 2013: “The first peopling of South America: New evidence from Y-chromosome haplogroup Q”) 이 그림은 물론 당신이 필요로 하는 것보다 훨씬 적은 내용을 담고 있는데 이 논문은 내 분야에 해당하고 나도 이런 절차를 항상 사용하기 때문이다. 하지만 만약 이 내용을 읽다가 "네트워크를 통해 감소-미디안 절차로 데이터를 처리한다"가 무슨 의미인지 모른다면 찾아봐야 한다.

이 연구의 세부적인 내용에서 반복적으로 사용되는 절차에 대해 이해할 필요는 없다. 그 일은 논문 리뷰어가 해야 하는 일이다. 하지만 기초적인 절차에 대해 다른 사람에게 설명할 정도가 되지 않는다면 결과를 읽을 준비가 되지 않은 것이다.

결과를 읽는다. 각각의 연구, 수치 그리고 표에서 나타나는 결과를 하나 또는 그 이상의 문단으로 요약한다. 아직 그 결과가 어떤 의미가 있는지 정하지 않아도 된다. 그 결과가 무엇인지에 대해서만 작성한다.

일반적으로 좋은 논문이라면 가장 결과의 중요한 부분은 수치와 표에 요약되어 있다는 점을 찾을 수 있을 것이다. 이 부분을 유의해서 보자. 몇 결과를 확인하기 위해서 온라인 정보 부록을 확인해야 할 수도 있다.

만약 이 논문에서 사용된 통계적 질의에 대해 이해할 수 있는 충분한 배경지식이 없다면 이 부분이 어려워질 수 있다. 이 포스트에서 통계에 대해 가르칠 수 없지만 다음 글(1, 2, 3)에서 기초적인 부분을 배울 수 있을 것이다. 이 내용에 대해 친숙해질 것을 강력하게 권한다.

결과에서 주의깊게 봐야 할 부분

  • "유효한 significant"나 "유효하지 않은 non-significant" 같은 표현은 언제든 사용할 수 있다. 이 단어는 명확한 통계적 의미를 담고 있다. 이 내용은 이 글을 참고한다.

  • 그래프가 있다면 거기에 오차 막대가 포함되어 있는가? 특정 분야 연구에서는 자신감 결여로 큰 차이가 발생하는데 주요한 적신호로 볼 수 있다.

  • 표본 규모. 이 연구의 대상이 10명인가, 10,000명인가? (연구 목적에 따라 10명 규모의 표본도 충분할 수 있지만, 대다수의 연구는 많을수록 좋다.)

결과를 활용해 세부적인 질문에 답할 수 있는가? 결과의 의미가 무엇이라고 생각하는가?

이 부분에 대해 생각하기 전까지 다음으로 넘어가지 말아야 한다. 저자의 해석에 따라 자기 생각을 바꿀 생각이라면 괜찮다. 이런 분석에 관해 초보자라면 이 고민 없이 넘어갈 것이다. 하지만 다른 사람의 견해를 읽기 전에 자기 자신의 해석을 형성하는 것은 정말 좋은 습관이다.

결론/토론/해석 섹션을 읽는다.

결론에 대해 저자는 어떻게 __생각__하는가? 저자의 주장에 동의하는가? 저자의 해석에 대해 어떤 __대안__이 있는가? 저자의 그 연구에서 확인할 수 있는 약점이 없는가? 저자가 놓친 부분은 없는가? (절대 틀릴 일 없다고 가정하지 않는다!) 다음 단계에 대해 어떤 제안을 하는가? 그 제안에 대해 동의하는가?

이제 처음으로 돌아가서 초록을 읽는다.

저자가 논문으로 얘기하는 부분과 초록이 일치하는가? 논문을 읽고 생각한 해석과 초록이 일치하는가?

마지막 단계: (이 단계를 등한시하지 말 것) 다른 연구자가 이 논문에 대해 어떻게 이야기 하는가?

(자칭이든 타칭이든) 이 분야에서 전문가는 누구인가? 그 전문가가 이 연구에 대해서 비평할 때 이런 식으로 생각해본 적이 없다고 하는가 아니면 전반적으로 지지하는가?

개인적으로 추천하는 방법은 구글을 활용하는 것이다. 하지만 이 과정은 마지막에 해야 한다. 그래서 다른 사람들이 어떻게 생각할지 비판적으로 생각하고 미리 준비할 수 있게 된다.

그리고 이 단계는 어떤 분야의 논문을 읽는가에 따라 다른 부분이다. 나에게는 아주 중요하다! 인용된 문헌을 확인해서 저자가 어떤 논문을 인용했는지 확인한다. 이 과정은 이 분야에서 중요한 논문을 알 수 있게 된다. 그리고 내 글이 인용되었는지 확인할 수 있... 이건 농담이다. 이 과정으로 유용한 아이디어, 기술의 원천을 찾는 데 도움이 된다.

이제 더 큰 일에 도전할 수 있게 되었다. 다음 주에는 쟁점이 많은 논문을 읽을 때 위 방법을 활용해보자. 어떤 식으로 읽고 싶은가? 지난주에 게시한 논문을 함께 읽고 비평해보는 것도 좋을 것 같다. 만약 예제를 보고 싶으면 예제: 어떻게 예방접종 안전에 대한 연구를 읽는가를 확인한다.


감사하게도 논문을 어떻게 비평적으로 읽고 분석하는가에 대한 방법을 José Bonner, Bill Saxton 교수님께 배울 수 있었다. 이 방법을 배울 기회가 있던 것은 영광이었다.

추가하고 싶은 조언, 전혀 다르지만, 더 낫다고 생각하는 방법이 있거나, 추가적인 질문, 다른 유용한 자료가 있다면 댓글을 남겨주기 바란다.

이 포스트는 CC BY 라이센스로 작성된 thoughtbot의 guides 중 Code review를 번역한 글이다. 짧은 만큼 상식적인 느낌도 많이 드는데 숙지하고 평소 습관으로 만들 수 있으면 좋겠다.

코드 리뷰

코드를 리뷰하고 내 코드를 리뷰 받는 방법에 대한 가이드.

모두에게 해당

  • 대다수 프로그래밍에서 내려진 결정은 각각의 견해에 따른다는 사실을 받아들인다. 어느 쪽이 더 좋은지 장단점을 의논하고 빠르게 결정을 내린다.
  • 질문한다. 대신 답을 강요하지 않는다. (“이 :user_id라는 네이밍에 대해 어떻게 생각하나요?”)
  • 명확하게 해달라고 묻는다. (“제가 이해하지 못했어요. 다시 설명해줄 수 있어요?”)
  • 코드의 소유권에 대해 특정하는 것을 피한다. (“내가 작성한”, “내 코드가 아닌”, “당신이 작성한”)
  • 개인적인 특징은 언급하는 것으로 보일 수 있는 단어를 피한다. (“바보”, “멍청한”). 모든 사람이 매력적이고 똑똑하며 선의가 있는 것으로 여겨야 한다.
  • 명시적으로 한다. 사람들은 온라인에서 당신의 속내를 항상 쉽게 이해할 수 있는 것은 아니란 점을 유념한다.
  • 겸손해야 한다. (“확신하기 어렵다. 한번 살펴보겠다.”)
  • 과장하지 않는다. (“항상”, “절대”, “끊임없이”, “아무것도”)
  • 빈정대지 않는다.
  • 실제처럼 행동해야 한다. 당신에게 해당하지 않는 이모지, 움짤 gif 또는 유머를 사용하더라도 그 사람들을 비꼬아서는 안된다. 만약 그들이 그런 행동을 한다면 침착하게 대응한다.
  • “이해할 수 없다” 또는 “대안:” 식의 코멘트를 너무 많이 사용한다면 대화가 필요하다. 오프라인에서 대화한 내용을 요약해 후속 코멘트로 남긴다.

내 코드를 리뷰 받을 때

  • 리뷰어의 추천에 감사해야 한다. (“리뷰 고맙다. 그렇게 변경하도록 하겠다.”)
  • 개인적인 부분으로 받아들이지 않는다. 리뷰의 대상은 코드지 당신이 아니다.
  • 왜 코드가 존재하는지 설명한다. (“이 부분을 이렇게 짠 이유는 이런 이유 때문이다. 내가 클래스/파일/메소드/함수명을 이렇게 바꾸면 더 명확해질 수 있을까?”)
  • 변경점과 개선점을 꺼내 미래의 티켓/스토리로 둔다.
  • 티켓/스토리에 코드 리뷰를 링크한다. (“리뷰를 위한 준비가 되었음: https://github.com/organization/project/pull/1&#8221;)
  • 초기 피드백을 받을 때는 독립적인 커밋으로 만들어 브랜치로 푸시한다. 브런치가 머지되기 전까지 커밋을 뭉쳐버리지 않는다. 리뷰어는 초기 피드백에 기반을 둬 작성한 각각의 업데이트를 읽는 것이 가능해야 한다.
  • 리뷰어의 관점에서 이해하도록 노력한다.
  • 모든 코멘트에 응답하도록 노력한다.
  • 지속적인 통합(TDDium, TravisCI 등)이 브랜치에서 모든 테스트가 통과되기 전까지 브랜치로 머지하지 않고 기다린다.
  • 코드를 머지하는 일은 프로젝트에 영향을 준다. 그러므로 자신의 코드에 확신이 있을 때 머지한다.

코드를 리뷰할 때

왜 이 코드가 필요한지 이해한다. (버그, 사용자 경험, 리팩토링.) 그러고 나서:

  • 어느 아이디어가 강점이라 생각되는지, 혹은 그 반대인지 소통한다.
  • 문제를 해결할 때까지 코드를 단순화하는 것으로 방향을 찾아라.
  • 논의가 철학적이거나 학문적으로 흐를 때는 그 주제를 금요일 오후 기술 토의처럼 오프라인으로 가져와서 논의한다. 그 후 코드 작성자가 대안적인 구현으로 최종 결정을 내리도록 한다.
  • 대안적인 구현을 제공할 때는 작성자가 이미 그 구현을 고려했을 것으로 가정한다. (“custom validator를 여기서 사용하는 것에 대해 어떻게 생각하나요?”)
  • 작성자의 관점을 이해하도록 노력한다.
  • :thumbsup: 또는 “머지 준비됨 Ready to merge”와 같은 코멘트와 함께 풀 리퀘스트를 수락한다.

코멘트 스타일

리뷰어는 스타일 가이드라인에 따라 코멘트를 남긴다. 예시는 다음과 같다:

[스타일](https://github.com/thoughtbot/guides/blob/master/style/README.md):

> 연관 라우팅을 이름 알파벳 순으로 정렬.

응답할 때는 다음과 같은 스타일로 코멘트를 작성한다:

앗, 좋은 지적이네요. 감사합니다. Fixed in a4994ec.

만약 이 가이드라인에 동의하지 않는다면 토론하기 전에 가이드 리포지터리에 이슈를 먼저 만들길 바란다. 합당하다면 가이드라인에 덧붙여질 것이다.


지난 읽을 거리

nodejs로 개발을 한다면 Express, Koa, Hapi 중 하나는 꼭 접하게 된다. 내 경우는 Express를 맨 처음 접해서 가장 익숙하지만 generator를 지원하는 koa에 대한 이야기도 들어봤고 hapi도 최근 react나 angular와 함께 사용하는 얘기를 자주 들을 수 있었다.

어떤 차이가 있는지 검색하다가 간단하게 정리된 Jonathan Glock의 글 Node.js Framework Comparison: Express vs. Koa vs. Hapi을 접하게 되었고 장단점 부분만 간단하게 번역했다. 원문에는 비교 코드도 포함되어 있어서 코드를 보고 싶다면 원문을 살펴보길 권한다.

Thank you Airpair for giving me the opportunity to translate this article. If you want to check the original, please visit Node.js Framework Comparison: Express vs. Koa vs. Hapi page.

각 프레임워크의 장단점

Express

Node.js 프레임워크 중 커뮤니티가 가장 크다. 거의 5년 가량 개발되어 가장 성숙했고 StrongLoop에 의해 관리되고 있다. 서버를 쉽게 실행/운영할 수 있다. 내장된 라우터로 코드를 쉽게 재사용 가능하다.

수작업으로 해줘야 하는 부분이 많다. 내장된 에러 핸들링이 없어서 미들웨어를 잃어버릴 수 있다. 한 문제를 해결하기 위해 여러 방법으로 접근할 수 있다. Express는 스스로를 완고하다고 표현하는데 이 부분은 양날의 검이며 초보인 경우에는 단점으로 작용한다. 다른 프레임워크에 비해 메모리를 많이 차지한다.

Koa

메모리를 덜먹고 표현력이 좋다. 다른 프레임워크에 비해 미들웨어 작성이 쉽다. 기본적으로 뼈대 프레임워크라서 제공되는 미들웨어와 함께 사용해야만 하는 Express와 Hapi와 달리, 개발자가 필요한 미들웨어만 구성해 사용할 수 있다. ES6를 도입하고 있어 ES6 제너레이터를 사용할 수 있다.

여전히 불안정하고 많은 양의 개발이 진행중이다. ES6를 사용하기 위해 최신 버전의 node.js를 사용해야 한다. (주, 이 문제는 지금도 해당하는지 모르겠음.) 미들웨어를 직접 작성할 수 있는게 장점일 수 있지만 단점일 수도 있다. 예제서 살펴본 라우터는 훨씬 다양한 옵션을 다뤄야 한다.

Hapi

코드보다 설정을 더 많이 해야 해서 정말 좋은 프레임워크인지 말이 많다. 견고함과 재사용성을 요구하는 큰 규모 팀에서는 흔하게 사용한다. 월마트랩에서 만들고 이름있는 회사에서 많이 쓰고 있어서 검증되었다고 보는 편이다. 좋은 프레임워크로 계속 성장할 것으로 보인다.

Hapi는 크고 복잡한 어플리케이션에 특성화 되어 있다. 보일러플레이트로 작성해야 할 코드가 많아서 작은 웹앱에서는 쓰기 불편하고, 예제 및 hapi로 작성된 오픈소스 앱도 적다. 이 프레임워크를 선택하면 서드파티 미들웨어에 기대는 쪽보다 개발자가 직접 작성해야 할 부분이 더 많을 것이다.


위 프레임워크 중 Express만 경험해봐서 각각 예제 코드가 살펴보는데 도움되었다. Koa는 tj가 노드를 떠난다는 글 쓴 이후로 시들할줄 알았는데 (그 핑계로 Koa를 딱히 살펴보지 않았는데) 여전히 잘 관리되고 있었다. 다양한 라이브러리가 매일같이 쏟아져 나와 봐야할 것도 많긴 하지만 잘 정착하는 프레임워크도 늘어나고 있어 커뮤니티가 잘 성숙하고 있다는 인상을 준다.

각 프레임워크 웹사이트

이 글은 Peter G. Klein가 작성한 How to Read an Academic Article을 번역한 포스트로, 학술 논문을 어떻게 읽어야 하는가에 대한 전략을 제시하고 있다. 내용에서 언급되는 것처럼 당연한 이야기를 적은 목록이라고 느껴질 수 있지만, 학술 논문을 읽는 것 이외에도 정보를 수용하고 생산하는 데 있어 좋은 관점을 갖고 있어 번역하게 되었다.


이번 가을에 학부 1학년 학생을 대상으로 "기관 및 조직의 경제학"을 가르치고 있다. 이 강의에서 읽어야 하는 목록은 대다수의 학부 코스, 그리고 1학년 대상의 미시경제학이나, 계량경제학 등에 비해서 좀 많은 편이다. 학생들에게 단순하게 열정적인 독자뿐 아니라 능률적인 독자가 돼야 할 필요가 있다고 설명한다. 그래야 학술적 기사에서 최소한의 노력을 통해 최대한의 정보를 얻을 수 있기 때문이다. 다시 말해 훑어보기(skim)의 예술을 배울 필요가 있다.

지금까지 학생들에게 훑어보는 것에 관해 설명했을 때, 대부분 어떻게 훑어봐야 하는지 모르겠다고 반응했다. 그래서 몇 년 전에 그 방법에 대해 작은 안내지 "어떻게 학술 논문을 읽어야 하는가"를 작성해 몇가지 팁과 방법을 적었다. 이 안내지는 거만하게 내 방식에 대해 알려주고자 하는 의미가 아니란 점을 강조하고 싶다. 그리고 이 안내지의 내용이 너무 당연한 이야기라고 느껴진다면 무시해도 문제 없다. 대다수 학생은 이 안내지로 도움을 받았다며 고마움을 표현했다. 그래서 이 안내지를 아래 첨부했다. 내용을 향상하기 위한 댓글이나 추천은 언제나 환영한다.

어떻게 학술 논문을 읽어야 하는가

  1. 경고: 모든 경우에 통하는 한가지 방법은 없다!
  2. Klein의 기본적인 훑어보기, 탐색하기, 처리하기...
    1. 초록(abstract)을 읽는다. (만약 있다면)
    2. 서론(introduction)을 읽는다.
    3. 결론(conclusion)을 읽는다.
    4. 중간 내용을 훑어본다. 섹션의 주제, 표, 수치 등 - 논문의 스타일, 흐름을 따라가 느낄 수 있도록.
      1. 이 논문은 방법론적인가, 개념적인가, 이론적인가(구술에 기반을 뒀나, 수학적인 방법에 기반을 뒀나), 실증적인가, 또는 그 외의 접근 방법인가.
      2. 이 논문은 설문조사에 기초하는가, 새 이론적 기여에 기초하는가, 기존에 존재하던 이론이나 기술에 대한 실증적 타당성을 확인했나, 비평인가, 또는 그 외의 방법인가.
    5. 다시 처음으로 돌아가 등식, 대부분 표와 수치를 건너뛰고 모든 내용을 빠르게 읽는다.
    6. 다시 처음으로 돌아가 전체 내용을 주의 깊게 읽는다. 가장 중요하게 보이는 섹션 또는 영역에 집중해 읽는다.
  3. 이 논문에서 저자가 하려는 일에 관해 주장의 근거를 이해했다면 그 내용을 비평하라.
    1. 만약 논거가 타당하다면 물어봐라. 그 내용이 내부적으로 일치하는가? 주장과 그에 대한 근거가 타당한가? (이 방법은 경험을 키우는 데 도움이 된다.)
    2. 이전에 읽은 같은 주제 또는 비슷한 주제의 논문과 비교하라. (만약 이 논문이 이 영역에서 첫 번째로 읽은 논문이라면, 비슷한 논문을 더 찾아 훑어 본다. 서론과 결론이 열쇠다.) 비교하고 대조해본다. 논거에 일관성이 있는가, 모순되는 내용이 있는가, 직교하는(orthogonal) 부분이 있는가?
    3. Google 학술 검색, Social Sciences Citation Index, 각 출판사 웹 페이지, 학술기사를 찾을 수 있는 곳에서 읽은 학술 논문에서 인용된 자료를 찾는다. 그 자료에서는 어떻게 언급되었는지 살펴본다. 블로그나 그룹 등에서 언급된 내용도 살펴본다.
    4. 인용된 자료를 확인한다. Journal of Economic Literature에서 진행한 설문 자료, 핸드북, 백과사전 또는 이와 유사한 출처의 자료 등을 확인하고 논문에서의 주제와 얼마만큼 해당 영역에서 접점을 가졌는지 검증한다.

원문: Microservice Trade-Offs By martin FowlerMartin Fowler (July 1, 2015)


많은 개발팀이 모노리스(monolithic) 아키텍처에 비해 마이크로서비스 아키텍처 스타일이 낫다는 점을 발견했다. 몇몇 팀에서는 오히려 생산성 저하를 만드는 부담이 된다는 점도 찾을 수 있었다. 다른 아키텍처 스타일처럼 마이크로 서비스도 비용과 이점을 동시에 갖고 있다. 상황에 맞게 선택할 수 있도록 다음 내용을 이해할 필요가 있다.

마이크로서비스가 제공하는 이득

마이크로서비스에서 발생하는 비용

  • 분산 Distribution: 원격 호출은 느리기 때문에 분산 시스템 개발을 더 어렵게 한다. 느린 속도에는 항상 실패의 위험성이 도사린다.
  • 최후 정합성 Eventual Consistency: 분산된 시스템에서는 강한 정합성을 유지하기 지극히 어렵다. 즉, 모두가 최후 정합성을 관리해야 한다.
  • 운영 복잡성 Operational Complexity: 재배포(redeployed)가 정기적으로 이뤄지는 많은 서비스를 운영하기 위해서는 성숙한 운영팀이 필요하다.

명확한 모듈 경계 Strong Module Boundaries

마이크로서비스의 가장 큰 이득은 명확한 모듈 경계를 갖는다는 점이다. 특이하게도 정말 중요한 이점 중 하나다. 왜 특이하냐면 이론적으로 모노리스(monolith)에 비해 마이크로서비스가 더 명확한 모듈 경계를 갖을 이유가 없기 때문이다.

그렇다면 모듈 경계가 명확하다는 것은 무슨 의미일까? 소프트웨어를 모듈로 분리해 서로 잘라두는 것(decoupled)이 좋다는 점에 모두 동의할 것이다. 모듈형 시스템으로 운영된다면 시스템의 한 부분을 변경할 필요가 있을 때, 그 작은 부분을 쉽게 찾을 수 있고, 변경해야 할 작은 범위에 대해서만 이해하면 되기 때문이다. 모듈로 구성된 좋은 구조는 어떤 프로그램이든 유용하다. 게다가 이 구조는 소프트웨어의 규모가 양적으로 팽창할 때 그 중요도가 기하급수적으로 증가한다. 특히 개발하는 팀이 양적으로 증가할 때 더욱 중요하다.

마이크로서비스를 옹호하기 위해 Conways 법칙에 대해 짧게 언급하면, “소프트웨어의 시스템 구조는 조직의 의사소통 구조를 답습하게 된다”고 한다. 대규모팀, 특히 다른 지역을 기반으로 한 여러 팀을 운영하는 경우에는, 단일 팀으로 운영하는 것에 비해 팀 간 소통 빈도가 낮아지며 더 공적인 형태로 소통을 하게 된다. 이런 소통 구조를 고려해 소프트웨어의 구조를 구축하는 것은 매우 중요하다. 마이크로서비스는 각각의 팀이 독립적인 단위로 의사소통을 할 수 있는 패턴을 구축하는 것이 가능하게 만든다.

앞서 말한 것처럼, 모노리스 시스템이 좋은 모듈형 구조를 갖지 못할 이유가 하나도 없다. 하지만 사람들 대부분이 모노리스에서 좋은 모듈형 구조를 갖고 있는 경우를 본 경험은 흔치 않다.1 실제로 볼 수 있는 가장 일반적인 아키텍처 패턴은 대형 진흙 덩어리 Big Ball of Mud다. 이 패턴은 모노리스의 일반적인 운명과도 같다. 팀이 이 문제로 어려움을 겪으면 마이크로서비스로 전환하는 원동력이 되기도 한다. 모듈을 분리(decoupling)하는 것으로 각각의 모듈이 서로 참조하는 관계에서 모듈 간의 벽이 생긴다. 이런 벽을 쉽게 우회할 수 있다는 점이 모노리스 시스템의 문제점이다. 각각의 기능을 사용하기 위해서 전략적으로 유용한 지름길을 만들어서 빠르게 사용할 수 있다. 하지만 이 방식으로는 모듈화된 구조를 망치고 팀의 생산성을 쓰레기로 만든다. 모듈을 분리된 서비스로 두는 것은 경계를 더 단단하게 만들고, 나쁜 코드를 작성하는 것을 더욱 어렵게 제한한다.

마이크로서비스의 연결 방식에서 중요한 부분은 영속적인 데이터(persistent data)다. 마이크로서비스의 주요 특징 중 하나는 탈중앙적 데이터 관리 Decentralized Data Management다. 각각의 서비스가 각자의 데이터베이스를 갖고 있기 때문에, 필요한 데이터를 얻기 위해서는 해당 서비스의 API를 통해야만 가져올 수 있다. 이 방식은 대형 시스템에서 주요 소스가 지저분하게 연결되어 있을 때 흔하게 볼 수 있는 통합 데이터베이스를 제거하는데 도움된다.

모노리스에서도 강한 모듈 경계를 만드는 것은 충분히 가능한 일이란 점도 중요하지만 그러기엔 소양이 필요하다. 같은 접근으로 대형 진흙 마이크로서비스 덩어리를 만들 수 있다. 물론 마이크로서비스에서 잘못된 방식으로 만들기 위해서는 모노리스보다 더 많은 노력을 필요로 하지만 말이다. 이런 관점에서 볼 때, 마이크로서비스를 사용하면 더 나은 모듈화를 얻게 될 가능성이 높아진다. 팀이 갖고 있는 소양에 대해 자신 있다면 마이크로서비스의 이점을 모노리스에서도 충분히 구현할 수 있을 것이다. 하지만 소양을 유지하기 어려울 정도로 팀이 급격하게 성장하고 있다면, 그만큼 모듈 경계를 유지하는 것은 더욱 중요한 일이 된다.

모노리스에서 제대로 된 경계를 갖지 못하게 되었을 때, 모듈의 분리는 장점이 아닌 핸디캡으로 변하게 된다. 도메인을 잘 이해해야 하는 이유로 모노리스 우선 Monolith First 전략이 필요한 것과 같은 맥락이며 이미 도메인을 잘 이해하고 있다면 마이크로서비스로 더 빠르게 전향하지 않았는가에 대한 스트레스만 있을 뿐이다.

이 아이디어에 대해서 더 얘기해야 할 부분이 있다. 시스템이 잘 모듈화되어 관리된다는 점은 시간이 흐른 뒤에 알 수 있다. 그래서 마이크로서비스가 더 개선된 모듈화를 제공한다는 사실을 알기 위해서는 적어도 몇년이 흘러야 제대로 평가할 수 있다. 게다가 이 아키텍처를 빠르게 적용한 경우에는 더 재능있는 팀일 경우가 높기 때문에, 모듈화의 장점이 있는 마이크로서비스를 평균적인 팀이 적용하기까지 더 많은 시간이 필요할지도 모른다. 그렇게 평균적인 팀이 마이크로서비스를 적용해 평균적인 소프트웨어 작성에 사용한 다음에야 이 시스템이 모노리스 아키텍처와 비교해서 더 나은 모듈화를 제공하는지 그 결과를 비교할 수 있게 된다. 이게 실질적인 평가에 있어 까다로운 점이다.

지금 이 순간 얘기할 수 있는 증거는 내 지인 중 이 스타일을 적용하고 있는 사람에게서 들은 이야기가 전부다. 그 사람들의 판단으로는 마이크로서비스에서 모듈을 관리하는 것이 훨씬 편하다고 이야기한다.

특히 이 케이스 스터디는 흥미롭다. 이 팀은 마이크로서비스의 혜택 Microservice Premium을 얻을 만큼 복잡하지 않은 시스템이라 생각하고서 잘못된 선택을 했다. 그 프로젝트에 문제가 생겼고 문제를 해결하기 위해 더 많은 사람이 투입되었다. 이런 시점에서는 마이크로서비스 아키텍처는 아주 유용하다. 이 아키텍처에서는 급격하게 증가하는 개발자를 흡수할 수 있고 전형적인 모노리스에 비해 더 큰 팀의 숫자를 감당할 수 있기 때문이다. 그 결과로 모노리스에서 기대되는 생산성보다 더 큰 효과를 얻을 수 있고 팀이 목적을 달성할 수 있게 된다. 이 프로젝트에서 내린 잘못된 선택으로, 모노리스 아키텍처에서 목표를 달성하기 위해 더 많은 시간을 사용하고 그로 인해 더 큰 소프트웨어 비용을 지출하게 되었다. 마이크로서비스 아키텍처가 이미 편안하게 지원하고 있는 부분인데 말이다.

분산 Distribution

마이크로서비스는 모듈화를 향상하기 위해 분산 시스템을 사용한다. 하지만 분산 시스템은 바로 분산되어 있다는 사실 자체가 주된 단점이다. 분산이라는 카드를 꺼내면 모든 호스트의 복잡성이 증대된다. 마이크로서비스 커뮤니티가 분산된 객체를 사용하며 발생하는 비용에 대해 순진하게 대응할 수 있을 것이라고는 생각하지 않지만 이 복잡성은 여전히 존재한다.

먼저 성능 문제가 있다. 요즘 세상에서 프로세스 내 함수 호출에 성능 문제가 있다는 점은 말도 안되는 일이겠지만 여전히 원격 호출은 느리다. 서비스가 6개의 원격 서비스를 호출하고, 그 서비스 각각 또 다른 6개의 원격 서비스를 호출한다고 가정하면 응답 시간이 증가해 끔찍하게 지연되는 특성이 있다.

물론 이 문제를 완화할 방법이 있다. 먼저 호출을 좀 더 덩어리로 만들어서 호출하는 횟수를 줄일 수 있다. 이 방식으로 이뤄지는 연산은 프로그래밍 모델을 복잡하게 만들기 때문에 내부 서비스 간의 소통을 어떻게 관리해야 할 지 고려해야 한다. 이 방식을 활용하더라도 각각 공용으로 필요한 서비스에 대해서 적어도 한 번 이상은 호출해야만 한다.

두번째는 비동기성(asynchrony)을 사용하는 것이다. 6번의 비동기 호출이 병렬로 실행되면 지연 시간은 가장 느린 호출 하나의 길이 만큼만 걸린다. 이 방식을 사용하면 성능은 엄청 향상되지만 또 다른 인지 비용이 발생한다. 비동기 프로그래밍은 어렵다. 올바르게 하는 것도 어렵고 디버그 하는 것은 훨씬 어렵다. 하지만 대부분 마이크로서비스 이야기에서는 납득할 만한 성능을 위해서 비동기를 필요로 했다는 점을 들을 수 있었다.

속도 다음으로 오는 점이 신뢰성(reliability)이다. 프로세스 내 함수를 호출하면 동작하는 것을 기대하지만 원격 호출은 언제든 실패할 수 있다. 대다수의 마이크로서비스에서 가장 실패하기 쉬운 부분이다. 똑똑한 개발자는 이 사실을 알고 실패를 위한 디자인 Design for failure을 한다. 이러한 전략은 비동기를 활용할 때도 필요하며 실패를 다루는 것과 문제가 생긴 결과에 대한 회복에도 도움이 된다. 하지만 이 방식이 모든 문제를 보정하진 못하며, 모든 원격 호출 중 발생할 수 있는 실패를 해결하기 위해서 별도의 복잡한 문제를 해결해야만 한다.

이 문제는 단지 분산 컴퓨팅에 대한 착오 Fallacies of Distributed Computing에서 언급된 문제 중 두가지 일 뿐이다.

이 문제에 대한 몇가지 주의점이 있다. 먼저 모노리스의 규모가 커졌을 때도 동일한 문제가 발생한다. 규모가 커진 모노리스는 정말로 독립적(self-contained)인데, 대개 각각 다른 시스템이며, 종종 레거시 시스템과 함께 동작하기도 한다. 이 모노리스 시스템 간에서 네트워크를 통해 이뤄지는 상호작용에서도 앞에서 이야기한, 마이크로서비스에서 발생하는 문제가 동일하게 나타난다. 이러한 점으로 인해 많은 사람들이 빠르게 마이크로서비스로 넘어가 원격 시스템으로 구축하는 것으로 상호작용을 처리하려 하는 이유다. 또한 이 문제는 경험이 해결할 수 있는 영역이며, 마이크로서비스에서는 분산 문제에 대해 쉽게 접근할 수 있어 기술력이 뛰어난 팀으로 해결하는데 용이하다.

하지만 분산은 항상 비용이 따른다. 난 여전히 분산이라는 카드를 사용하는데 꺼리는 편이다. 많은 사람들이 앞서 언급한 문제를 과소평가하고서 너무 쉽게 분산으로 넘어가고 있는 것은 아닌가 생각한다.

최후 정합성 Eventual Consistency

웹사이트는 작은 인내심을 필요로 한다. 무언가 업데이트 한 후 스크린을 새로고침 하면 업데이트된 내용이 포함되어 있지 않다. 1~2분 지난 후 새로고침을 누르면 나타난다.

이런 부분은 분명 사용성에 있어 짜증나는 문제다. 이런 문제는 거의 대부분 최후 정합성의 위험에서 나타난다. 업데이트는 적색 노드에서 처리하는데 새로고침으로 보낸 요청은 녹색 노드에서 처리된다. 녹색 노드가 적색 노드에서 업데이트 되었다는 사실을 받기 전까지는 페이지에서 새로고침을 눌러도 업데이트 되지 않은 화면을 봐야만 한다. 언젠가 일치되긴 하겠지만 업데이트 되지 않은 화면을 본 사람은 여전히 어딘가 잘못된 것은 아닌가 고민하게 된다.

이런 불일치의 문제는 충분히 짜증나는 일이지만 단순히 짜증나는 일에 그치는 것이 아니라 심각한 문제가 될 수 있다. 비지니스 로직에서 불일치된 정보로 의사를 결정하게 될 가능성이 있고 이런 일이 발생했을 때에는 문제를 분석하는 것이 지극히 어렵다. 대개 불일치된 정보로 인해 발생한 문제를 조사하는 것은 불일치된 화면을 닫아버린 이후에 시작되기 때문이다.

마이크로서비스는 탈중앙적인 데이터 관리라는 칭찬 받을 만한 구조를 갖고 있기 때문에 이 최후 정합성 문제에 대해 소개할 수 있는 것이다. 모노리스에서는 단일 트렌잭션에서 여러가지 업데이트를 갱신할 수 있다. 마이크로서비스에서 여러 리소스를 동시에 갱신해야 할 일이 있을 때 나타나는 분산된 트랜잭션은 눈살을 찌푸리게 한다. (좋은 이유에서 말이다.) 그래서 개발자는 정합성 문제에 대해 주의하고, 코드가 잘못된 결과를 만들기 전에 동기화 해야 할 부분은 없는지 감지하는 부분을 처리해야 한다.

모노리스 세계에서도 이런 문제에 자유롭지 않다. 시스템이 성장할 때, 성능을 향상하기 위해 데이터를 캐싱해야 할 때가 있다. 검증되지 않은 캐시(cache invalidation) 문제는 또 다른 어려운 문제다. 대부분의 어플리케이션은 동작 시간이 긴 데이터베이스 트랜잭션을 피하기 위한 오프라인 잠금이 필요하다. 외부 시스템은 트랜젝션 매니저 없이 데이터를 갱신할 수 없다. 비지니스 프로세스에서는 종종 생각보다 더 관용적일 때가 있는데 그게 더 상품 가치가 있기 때문이다. (비지니스 프로세스는 본능적으로 CAP 정리를 이해한다.)

모노리스, 특히 규모가 작은 경우에는, 그 외 분산 이슈에서도 불일치 문제에 대해 완벽하게 피할 수 있는 것은 아니지만 그래도 덜 고통스러운 편이다.

독립적 배포 Independent Deployment

모듈 경계와 분산 시스템의 복잡도 사이에서 균형을 잡는 일은 내 인생의 커리어 내내 따라다녔다. 하지만 지난 몇 년 사이 최종 제품으로 출시하는 역할이 눈에 띄게 달라졌다. 20세기의 제품 출시는 정말 고통스럽고 드문 이벤트였다. 그 일에는 소프트웨어 조각을 쓸모있게 만들기 위해 밤낮 주말 교대도 수반되었다. 하지만 최근엔 기술력 강한 팀이 빈번한 주기로 제품을 출시하고, 많은 조직이 지속적인 배포 Continuous Delivery를 활용해 하루에도 여러번 배포를 수행한다.

이 전환은 소프트웨어 산업에 깊은 영향을 줬으며 그 변화는 마이크로서비스 운동과 밀접한 영향을 갖고 있다. 대형 모노리스 시스템에서는 작은 변경에도 전체를 다시 배포해야 했고 배포 과정 중 개발 전체에 문제가 생길지도 모르는 상황이 바로 발단이 되어 마이크로서비스에 대한 논의가 시작되었다. 서비스는 컴포넌트라는 접근으로, 각각의 서비스를 독립적으로 배포 가능하다는 것이 마이크로서비스의 주요 원칙이다. 그래서 변경사항이 있다면 그 작은 서비스에 대해서만 테스트하고 배포하면 된다. 반영한 서비스에 문제가 있다고 하더라도 전체 시스템을 고장내지 않는다. 그 결과로 실패에 대한 설계가 당연해졌고, 컴포넌트가 실패하게 되더라도 동작하고 있는 시스템의 다른 부분을 멈추게 해서는 안되며, 최소한 우아하게 처리되는 형태를 보여야 한다.

이 관계는 왕복차선과도 같다. 많은 수의 마이크로서비스는 빈번한 배포를 요구하며 그 배포를 위한 여건을 함께 수행하는 것이 필수적이다. 마이크로서비스의 전제 조건으로 급진적인 어플리케이션 배포와 급진적인 인프라스트럭쳐 지원이 요구되는 이유다. 최소한 기본적으로 지속적인 배포(continuous delivery)는 해야 할 것이다.

마이크로서비스는 포스트 데브옵스 혁명을 이끄는 최초의 아키텍처다. — Neal Ford

지속적인 배포의 가장 큰 이득은 아이디어가 소프트웨어로 전환되는 사이에 발생하는 시간 주기를 줄여준다는 점이다. 조직은 시장의 변화에 대해 빠르게 대응할 수 있고 새 기능을 경쟁자보다 더 빠르게 소개할 수 있다.

많은 사람들이 마이크로서비스가 지속적인 배포를 사용하기 위한 이유라고 생각하지만 실제로는 어떤 환경에서든, 심지어 대형 모노리스더라도 지속적인 배포는 필수적이다. Facebook과 Etsy는 잘 알려진 케이스다. 마이크로서비스 아키텍처를 사용하고 있는 많은 경우에도 독립적 배포 중에 실패가 발생하는데 이 경우 다수의 서비스를 배포하는 상황에서 주의깊게 조율하는 것이 필요하다.2 많은 사람들은 마이크로서비스에서의 지속적인 배포가 훨씬 쉽다는 이유를 이야기하지만 내 생각에 그 부분은 부수적이며 모듈화에 대한 실질적 중요성에 더 주목하고 있다. 물론 모듈화에 집중하면 배포 속도에 강한 면모를 보인다는 자연스러운 연관성이 있다.

운영 복잡성 Operational Complexity

독립적인 단위로 재빠르게 배포가 가능하다는 점은 개발에 있어 큰 축복이지만 어플리케이션 6개를 운영하던 상황에서 수백개의 작은 마이크로서비스를 관리하게 되었다는 점은 부담이 될 수 밖에 없다. 대다수의 조직은 빠르게 바뀌는 도구의 사용은 금지해야 하는가 등의 문제를 어떻게 다뤄야 하는지 방법을 찾아야 한다.

운영 복잡성은 지속적인 배포의 중요성을 강화한다. 지속적인 배포가 모노리스에서는 대부분 노력하면 얻을 수 있는 정도에 가치있는 기술이란 점에 반해 진지한 마이크로서비스의 설정이라면 필수적인 기술로 변모했다. 자동화와 지속적인 배포 없이 여러 뭉치의 서비스를 운영하는 방법은 존재하지 않는다. 서비스를 관리하고 모니터링할 필요가 생기더라도 운영 복잡성은 증가한다. 마이크로서비스가 뒤섞이기 시작하면 모노리스 어플리케이션이 제공하는 성숙함을 다시 필요로 하게 될 것이다.

마이크로서비스에 찬성하는 사람은 서비스가 작아질수록 이해하기 쉽다고 이야기한다. 하지만 서비스의 상호 연결성이 산재해 있고, 그 복잡도가 제대로 제거되지 않은 상태는 위험하다. 가까이 있는 서비스 사이의 행동은 디버깅하기 어려워지는 등 컴포넌트 간 잘못된 상호 연결로 인해 운영 복잡성이 증가하게 된다. 서비스 경계에 대한 좋은 선택은 이 문제를 해소하는 편이지만 경계가 잘못 설정되어 있으면 더 나쁜 상황에 빠진다.

운영 복잡성을 다루기 위해서는 새로운 기술과 도구를 사용하는 것과 동시에 기술적으로 뛰어나야 한다. 툴을 사용하는 것은 여전히 서툴면서도 내 본능은 더 나은 도구를 사용할 수 있고, 낮은 장대를 넘는 것으로 충분하다고 생각하지만 마이크로서비스 환경은 그렇게 쉽지 않다.

발전된 기술과 도구 사용에 대한 요구가 운영 복잡도를 해소하는데 가장 어려운 부분이 아니다. 이 모든 문제를 효과적으로 해결하는 방법은 개발팀과 운영팀, 그리고 모두가 소프트웨어 배부에 참여하는 데브옵스 문화를 도입하는 것이다. 문화를 바꾸는 것은 특히 크고 오래된 조직일수록 어려운 일이다. 기술의 향상이나 문화의 변화를 만들 수 없다면 모노리스 어플리케이션은 방해가 되는 정도겠지만, 마이크로서비스 어플리케이션에서는 치명적일 것이다.

기술 다양성 Technology Diversity

각각의 마이크로서비스가 독립적으로 배포 가능한 단위가 된 이후로 기술 선택에 있어 자유롭게 고려할 수 있게 되었다. 마이크로서비스는 다른 언어, 다른 라이브러리, 다른 데이터 저장소를 사용해 작성할 수 있다. 이런 특징으로 팀은 작업에 대해 적절한 도구를 선택할 수 있게 되고, 특정 문제에 대해 더 적합한 언어와 라이브러리를 선택할 수 있게 된다.

기술 다양성에 대한 토론은 작업이 요구하는 일에 가장 적절한 도구를 선택할 수 있다는 사실이 주로 다뤄지지만 마이크로서비스의 가장 큰 이점은 버전 관리라는 더 평범한 문제에 있다. 모노리스에서는 라이브러리에 대해 단 하나의 버전만 사용할 수 있어 업그레이드로 인한 문제가 발생하는 경우가 간혹 있다. 새로운 기능을 사용하기 위해 시스템의 업그레이드가 필요한데 그 업그레이드가 시스템의 다른 부분을 망가뜨릴 수 있어 업그레이드를 못할 수 있다. 라이브러리 버전관리 문제는 코드의 규모가 커지면 커질수록 기하급수적으로 어려워진다.

물론 개발 조직이 압도당할 정도로 지나친 기술 다양성을 갖는 일은 위험하다. 내가 아는 대다수의 조직은 제한적인 기술 내에서 사용할 것을 권장하고 있다. 서비스를 쉽게 만들 수 있도록 돕는 모니터링과 같은 일반적인 도구를 제공하는 등 제약을 통해 서비스를 일반적인 환경에서의 작은 포트폴리오에서 유지할 수 있도록 지원한다.

실험적인 작업을 지원하는 가치를 저평가하지 말아야 한다. 모노리스 시스템에서는 초기에 결정한 언어와 프레임워크에 대해 되돌리기 어렵다. 10년이 흐르면 이런 결정이 팀을 이상한 기술에 묶어놓는 결과를 만들지도 모른다. 마이크로서비스는 팀이 새로운 도구로 실험하는데 적합하고 시스템을 점진적으로 한 서비스씩 변환해가면 그 때마다 최상의 기술을 적절하게 활용할 수 있을 것이다.

부차적인 요소

여기까지 트레이드오프의 주요 요소를 살펴봤다. 덜 중요하다고 생각하는 몇가지 더 있다.

마이크로서비스 지지자는 종종 서비스가 스케일하기 편하다고 이야기한다. 하나의 서비스가 많은 부담을 받을 때, 전체 어플리케이션을 확장하는 것보다 그 서비스에 대해서만 확장하면 된다는 것이다. 내 경험에 따르면 실제로 어플리케이션 전체를 복사하는 쿠키 자르기 확장에 비해 선택적 확장이 훨씬 효과적이였다.

마이크로서비스는 민감한 데이터를 분리할 수 있고 데이터에 대해 더 주의깊은 보안을 적용할 수 있다. 게다가 마이크로서비스 사이에서 발생하는 모든 트래픽은 안전하며 마이크로서비스 접근 방식은 동작을 멈추는 익스플로잇을 만들기 어렵다. 보안 문제의 중요성이 증대됨에 따라 이런 마이크로서비스의 특징을 주요 고려 대상으로 보는 경우도 늘고 있다. 마이크로서비스가 아니더라도 모노리스 시스템에서 민감한 데이터를 다루기 위해 별도의 서비스로 분리하는 것은 특별한 일이 아니다.

마이크로서비스에 대한 비평 중에서는 모노리스 환경에 비해 테스트가 어렵다는 점도 있다. 분산 시스템의 복잡성으로 인해 이는 실제로 어렵긴 하지만 마이크로서비스에서의 테스팅을 위한 좋은 접근 방식이 있다. 여기서 모노리스와 마이크로서비스에서의 테스트 차이점을 비교하는 것은 두번째 순위로 봐야 하는 부분이고, 테스트를 수행하는 것에 대해 진지하게 생각하도록 단련하는 것이 가장 중요하다.

마이크로서비스 리소스 가이드
마이크로서비스에 대한 추가적인 정보는 마이크로서비스 리소스 가이드를 살펴보자. 어떻게, 언제, 어떻게, 누가 사용해야 하는지에 관한 최고의 정보를 모은 책이다.

정리

아키텍처 스타일에 작성한 어떤 글이든 일반적인 조언의 한계를 갖고 있다. 그래서 이런 글에서는 결정을 대신 내려주진 않지만 다양한 요소에 대해 고려해볼 수있는 시각을 제공한다. 각각의 비용과 이점은 각각의 시스템에서 다른 무게를 갖고, 이점과 비용이 뒤바뀔 수도 있다. (강한 모듈 경계는 더 복잡한 시스템에서 좋지만 간단한 시스템에서는 불리한 조건이 될 수도 있다.) 어떤 결정이든 상황에 따라 달라진다. 각 요인으로 인한 문제를 어떻게 평가할 것인지, 자신만의 특정 맥락에서 어떤 영향을 주는지 말이다. 게다가, 마이크로서비스 아키텍처에 대한 경험은 상대적으로 제한적이다. 아키텍처에 관한 결정을 제대로 내렸는지 알기 위해서는 시스템이 충분히 성숙하고 나서야 가능하고 개발을 시작하고 몇년은 작업하고 나서야 배우게 된다. 오랜 기간 사용한 마이크로서비스 아키텍처에 대해서는 아직 많이 들어보지 못했다.

모노리스와 마이크로서비스는 단순한 양자택일의 문제가 아니다. 둘 다 흐릿한 정의인데, 그 의미는 많은 시스템이 흐릿한 경계로 두고 거짓말을 하고 있다는 뜻이다. 어떤 시스템은 이 두 카테고리 중 어디에도 맞지 않을 수도 있다. 내 자신을 포함한 대다수 사람들이 모노리스에 비해 마이크로서비스를 강조하는데 더 일반적인 상황에 적합하기 때문에 강조하는 것은 맞지만 세상 모든 시스템이 이 두가지 경우에 편안할 정도로 딱 맞을 수는 없다는 사실을 기억해야 한다. 모노리스와 마이크로서비스는 아키텍처 우주에서 두 지역이다. 이 두 아키텍처의 이름이 가치있는 이유는 유용함에 대해 논의하기에 흥미있는 특징을 갖고 있고 아키텍처 우주에서 부분으로 떼어내 사용하는데 큰 불편함이 없기 때문이다.

광범위하게 동의를 받고 있는, 한가지 결론으로 내릴 수 있는 일반적인 부분은 마이크로서비스 프리미엄이 있다는 점이다. 마이크로서비스는 더 복잡한 시스템을 만들기 위해 필요한 생산성을 비용으로 지불한다. 만약 시스템의 복잡도를 모노리스 아키텍처에서 감당할 수 있다면 마이크로서비스를 사용하지 않아야 한다.

하지만 마이크로서비스에 대한 대화에서 그냥 흘려 잊으면 안되는, 소프트웨어 프로젝트의 흥망을 결정하는 중요한 문제가 있다. 팀 구성원의 질이나 각자가 어떻게 협동할 것인가, 도메인 전문가가 커뮤니케이션 학위를 갖고 있는가와 같은 요소는 마이크로서비스 사용 여부에 비해 더 큰 영향이 있다. 순수하게 기술적 레벨에서 보면, 깔끔한 코드, 좋은 테스팅에 집중하는 것이 더 중요하고 진화하는 아키텍처에 대해 주목해야 한다.


더 읽을 거리

Sam Newman은 마이스로서비스의 장점 목록을 자신의 책 1장에서 다뤘다. (마이크로서비스 시스템을 구축하기 위한 세부 사항에서는 필수적인 자료다.)

Benjamin Wootton의 포스트 마이크로서비스는 무료 점심이 아니다!에서는 마이크로서비스를 사용하는 경우에 발생할 수 있는 어려움에 대한 이야기를 찾아볼 수 있다.

Acknowledgements

Brian Mason, Chris Ford, Rebecca Parsons, Rob Miles, Scott Robinson, Stefan Tilkov, Steven Lowe, and Unmesh Joshi discussed drafts of this article with me.


번역에 도움 준 Sinclebear님 감사 말씀 전합니다.

  • 어떤 사람은 “모노리스”를 빈곤한 모듈화 구조를 가졌다는 말로, 공격적으로 듣는 경향이 있다. 마이크로서비스를 사용하는 대다수의 사람들은 모노리스를 단순히 단일 단위의 어플리케이션으로 만들었다는 의미로 사용한다. 마이크로서비스를 얘기하는 많은 사람들은 대부분의 모노리스가 큰 진흙 덩어리인 것처럼 얘기하지만 아무도 잘 구조화된 모노리스가 절대 불가능하다고 토의하는 경우는 보지 못한 것 같다. 
  • 서비스를 독립적으로 배포할 수 있는 능력은 마이크로서비스의 정의 중 일부다. 그래서 서비스끼리 조율해서 배포를 해야 하는 경우는 마이크로서비스 아키텍처라고 부르지 않는게 합당하다. 또한 많은 팀이 마이크로서비스 아키텍처를 사용하며 발생하는 문제는 서비스 배포를 조율하는 것이 주된 원인이다. 
  • 색상을 바꿔요

    눈에 편한 색상을 골라보세요 :)

    Darkreader 플러그인으로 선택한 색상이 제대로 표시되지 않을 수 있습니다.