[번역] PHP 5.6에서부터 타이밍 공격에 대한 대응을 지원합니다.

원본글 : http://blog.ohgaki.net/php-5-6-become-timing-safe

PHP 5.6부터는 ‘타이밍 공격’에 대한 대책이 도입되었습니다. 메이저 어플리케이션에는 타이밍 공격에 대한 대책이 도입되어 있습니다만, PHP 5.6에서부터는 간단하게 공격에 대응할 수 있게 되었습니다.

타이밍 공격이란 컴퓨터가 동작하는 시간의 차이를 측정하여 공격에 이용하는 수법입니다. 알고리즘 자체의 취약성이 아닌 컴퓨터의 동작 시간, 온도, 소리, 전자적 노이즈, 전력 사용량 등 부가적인 현상을 공격의 도구로 활용하는 ‘사이드 채널 공격’의 일종입니다. 예를 들면 블라인드 SQL 인잭션 기법에서는 타이밍 공격이 대단히 유용하다는 것이 이전부터 알려져 왔습니다.

다음과 같은 코드가 타이밍 공격에 취약한 코드 입니다.

<?php

if( $screct_password === $_GET['password'] )
{
  $authenticated = TRUE;
}

이 코드는 평문 패스워드($secret_password)를 유저가 송신한 패스워드($_GET[‘password’)와 일치하는지 확인하고 일치하는 경우 인증값($authenticated)를 TRUE로 설정합니다.

패스워드가 해싱되지 않는 평문인 것을 제외하면 특별히 이상할 것이 없는 코드 입니다만, 이 코드는 타이밍 공격에 취약합니다. 이유를 알기 위해서는 PHP내부에서 이 코드가 어떻게 동작하는지를 알 필요가 있습니다.

문자열 === 문자열 or 문자열 == 문자열

위와 같이 비교할 때 문자열의 길이가 다른 경우 PHP는 곧바로 false를 반환합니다. 문자열의 길이가 같은 경우에만 표준 C 라이브러리의 strncmp를 이용하여 문자열을 비교합니다.

길이가 다른 경우 strncmp가 호출되지 않기 때문에 길이의 일치 여부에 따라 미묘하게 응답의 시간차가 발생합니다. 응답시간의 차이를 통계적으로 처리하면 패스워드의 길이를 파악할 수 있습니다.

평문 패스워드의 길이를 확인하면 이번에는 strncmp의 동작시간의 차이를 계측합니다. strncmp는 비교하는 문자열이 다르면 곧바로 false를 반환합니다. 즉, 최초 첫문자가 일치하는 경우와 그렇지 않은 경우 역시 미묘한 응답시간의 차이가 발생합니다. 이런 작업을 반복하여 1문자씩 패스워드를 해독해갑니다. 패스워드가 랜덤하게 생성된 문자열이라고 해도 난이도는 크게 변하지 않습니다.

알기쉬운 예를 들면 패스워드가 숫자로만 이루어져있고 최대 길이가 6자리인 경우 경우의 수는

10^6 = 1,000,000

입니다. 하지만 타이밍 공격을 이용할 경우

10*6 = 60

의 경우의 수로 해석이 가능합니다. 물론 타이밍 공격을 실시하는 경우 처리 시간의 차이를 통계적으로 처리해야 하기 때문에 60번 이상의 엑세스를 시도해야 할 필요가 있습니다. 가령 1문자당 1,000회의 엑세스를 한다고 한다면 6만회의 시도가 필요합니다. 모든 경우의 수를 고려하는 것과 비교해 압도적으로 적은 횟수로 해석이 가능합니다.

해싱된 값 또한 타이밍 공격에 비슷한 취약점을 갖습니다. 이러한 미묘한 시행시간의 차이를 이용하여 실제로 Web 어플리케이션에 대한 공격이 가능할까? 라는 의문을 갖는 분도 계실지 모르겠습니다만 이미 가능하다는 것이 실제로 증명되었습니다. 네트워크 지연 등 우연적으로 발생하는 부분에 대해서 통계적으로 처리해야할 부분이 늘어나긴 합니다만 최근 네트워크 기술의 발달로 인해 이런 우연의 요소가 비교적 많이 감소하여 타이밍 공격이 더욱 용이하게 되었습니다.

로그인 패스워드 등은 연속으로 틀리는 경우에 대한 제한을 둘 수 있으나 API키의 대량의 엑세스를 전제로하는 서비스의 경우에는 엑세스 제한을 두는 것은 현실적으로 불가능 합니다. 가령 1일 최대 엑세스 한도를 설정하더라도 몇일의 시간을 두고 시도한다면 해석이 가능합니다.

OS 및 다른 언어에서의 대책

예를들면 Python 3.4부터는 문자열 비교에 해쉬값을 이용합니다. OS에서도 유사한 라이브러리를 제공합니다. 예를들면 OpenBSD의 timingsafe_bcmp 등이 있습니다.

int
timingsafe_bcmp(const void *b1, const void *b2, size_t n)
{
  const unsigned char *p1 = b1, *p2 = b2;
  int ret = 0;

  for (; n &gt; 0; n--)
    ret |= *p1++ ^ *p2++;
  return (ret != 0);
}

C언어를 모르더라도 어떤 동작을 하는 코드인지 알 수 있으리라 생각합니다. 고정길이의 데이터에 대해 1바이트씩 모든 바이트를 비교합니다. 어떤 입력값이 들어오더라도 항상 같은 동작을 함으로 처리 시간의 차이를 이용한 타이밍 공격을 무효화 합니다.

Python 3.4에서는 SipHash라고 하는 해쉬를 생성하는 알고리즘을 이용하여 해쉬를 생성하여 64비트 정수로서 비교합니다. 해쉬 함수를 이용해 그 결과를 비교하면 타이밍 공격을 무효화합니다.

PHP에서의 대책

PHP 5.6에서 부터는 타이밍 공격에 대한 대책이 도입되었습니다.

http://kr1.php.net/manual/en/function.hash-equals.php

기타 대책

스스로 대책을 강구해 볼 수도 있습니다만 프레임워크에서 사양으로 제공하는 경우도 있습니다.

  • Symfony
  • ZendFramework
  • Joomla Framework

PHP의 hash_password 함수를 이용하는 경우 타이밍 공격으로부터 안전합니다. 패스워드는 통상 해쉬화한 패스워드를 비교하기 때문에 타이밍 공격을 이용할 수 없습니다. API키 등 문자열로서 보존하고 비교하는 경우에는 주의할 필요가 있습니다. 상기한 프레임워크가 제공하는 문자열 비교방법을 이용하거나 직접 해쉬값을 비교하셔야 합니다.

API키 등 열쇠가 되는 해쉬값이 특정하기 쉬운 조건은 다음과 같습니다.

  • 열쇠가 특정 유저의 ID와 연관되어 있다
  • 열쇠가 메모리에 캐쉬되어 있다
  • 열쇠를 1문자씩 비교한다.

이경우 문자열 비교의 미묘한 지연시간을 해석해서 공략할 수 있는 가능성이 높아집니다. 1문자씩 비교하는 문자열 비교를 배제하지 않으면 공략될 가능성이 남기때문에 문자열 비교시간을 일정하게 유지하는 것이 중요합니다. 앞서 말한 해쉬함수를 이용하여 유저가 송신한 문자열을 비교하는 것이 안전합니다.

열쇠가 특정 유저 ID와 연관되어 보존되어 있는 경우란 유저 ID와 열쇠가 같이 송신되는 경우를 말합니다. 예를들면 다음과 같은 요청입니다.

http://example.com/api.php?userid=1234&apikey=123456789

타이밍 공격에 안전한 비교 방법은 열쇠가 되는 문자열의 길이를 노출시키지 않는 것이지만 사실상 문자열의 길이를 안전하게 은폐하는 것은 불가능합니다. 열쇠의 길이를 알더라도 안전하도록 충분히 긴 값을 이용하는 것이 타이밍 공격의 리스크를 줄일 수 있습니다.

정리

타이밍 공격은 언어나 OS에 관계없이 적용가능한 공격방법 입니다. PHP 이외의 언어에서도 마찬가지입니다. 보안이 중요한 시스템에서는 타이밍 공격의 리스크를 무시해서는 안됩니다. 보호된 환경에 놓인 어플리케이션이라고 해서 안심해서는 안됩니다. 보호된 환경에서도 SSRF 공격의 가능성은 항상 존재하며, 보호된 시스템 일수록 일반에 공개된 시스템과 비교해 반응시간에 개입하는 우연적인 요소가 적기 때문에 타이밍 공격이 더 용이할 것으로 판단됩니다.

마지막으로 우연적 요소를 증가시키를 것을 대응방법으로 삼는 것은 추천하지 않습니다. 우연성을 높이는 것은 완화책으로서는 기능하지만 근본적인 해결책은 될 수 없습니다. 오직 근본적인 대책만이 보안수단은 아니지만 문자열 비교의 처리시간을 일정시간으로 유지하는 것이 보다 우수하고 현실적인 방법입니다.

Javascript의 Object를 다루는 몇가지 팁들

Node.js 의 등장으로 서버 사이드에서 프론트 엔드까지 Javascript로 코딩을 할 수 있는 시대가 왔습니다.  MongoDB라는 Javascript 형태의 데이터베이스를 포함하여 MEAN 스택(MongoDB + Express + AngularJS + Node.js)이라는 용어가 등장하기도 합니다.  Javascript로 클라이언트와 서버가 통신을 하다보니 Javascript의 Object를 얼마나 잘 다루느냐가 꽤나 중요한 문제가 되었습니다.

Javascript에서 Objcect와 Array 등을 다루는데 최고봉은 역시 Underscore입니다.  Node.js 에도 모듈 형태도 제공하고 있기 때문에 배워두면 여러모로 편리합니다(이걸 어디다 쓰지 싶은 기능들도 있는데 하다보면 신기하게도 쓸데가 있습니다…).  일단 Underscore에 관한 이야기는 나중에 하고 최근에 구글신과 스택오버플로우로부터 전해받은 은혜로운 복음(…)의 결과물을 정리해 봅니다.

1. Object의 key를 변수로 받기

 var person = {
  name : "neko",
  gender : "male"
};

console.log( person.name, person.gender );
// 결과 : neko male

var key1 = 'name', key2 = 'gender';

console.log( person[key1], person[key2] );
// 결과 : neko male
// 위와 동일한 결과를 얻을 수 있습니다.

 

2. key, value 값 얻어내기( = foreach )
애석하게도 Javascript에서 foreach가 없습니다.

var object = { ... };

for( var key in object ) {
  console.log( key + '=>' + object[key] );
}

 

3. 요소의 갯수 알아내기
배열의 경우에는 length로 쉽게 알아낼 수 있지만 Object에는 length가 없습니다.

var object = { ... };
var length = Object.keys( object ).length;

console.log( length );
// object의 요소 갯수가 반환됩니다.
// Object.keys()만 돌리게 되면 해당 Object의 value값만 배열로 반환이 됩니다.

 

4. 형 구분하기
JSON은 데이터를 주고받는데 매우 편리한 Data형식이지만 Node.js 서버에 JSON으로 데이터를 날리게되면 value가 전부 String으로 날아가는 경우가 있습니다. 이게 꽤나 난감한 것이 MongoDB에서는 $inc와 같이 value가 Number( Int, Float 등 )가 아니면 에러를 날리는 경우가 있기 때문에 최소한 수와 문자의 구분을 해주어야 합니다.

다음은 JSON 데이터의 value가 0으로 시작하지 않는 숫자들만의 조합으로 이루어 졌을때 정수형으로 변환한 후 이를 반환하는 함수입니다. 두번째 if문의 블럭의 조건과 구문을 조작하면 좀더 정교한 구분도 가능합니다(일단 편의상 정수 데이터만 날아온다고 가정했습니다. 전화번호 같이 0으로 시작하는 경우 이를 정수로 받으면 가장 앞 부분의 0이 생략되기 때문에 문자로 취급했습니다)

var typeCheck = function( jsonData ) {
  for( var key in jsonData ) {
    if( typeof jsonData[key] === 'object' || typeof jsonData[key] === 'array' ) {
      jsonData[key] = typeCheck( jsonData[key] );
    } else {
      if( /^(0|[1-9][0-9]*)$/.test( jsonData[key] ) ) {
        jsonData[key] = parseInt( jsonData[key] );
      }
    }
  }

  return jsonData;
}

[Angular] IE8 에서 Directive 사용하기

Angular JS에서 Directive를 통해서 임의로 생성한 Element의 경우 IE8 에서 사용할 경우 문제가 발생합니다.  구체적으로는 태그가 닫히지 않는다거나 Template가 제대로 적용이 안된다거나 이런저런 자잘한 버그가 발생하는데요, 해결 방법은 의외로 간단합니다.

위와 같이 컨디셔널 커멘트를 이용하여 IE8 이하에서 Javascript로 Element를 수동으로 생성하시면 됩니다.

MongoDB Error Note.

참조 : http://stackoverflow.com

Cent OS 6.4 서버에 설치되어 있는 PHP의 MongoDB 드라이버를 업데이트 한 후 다음과 같은 에러가 발생하며 Extension 파일을 읽어오지 않는 증상이 발생하였습니다.

mongo.so: > undefined symbol: php_json_encode in Unknown on line 0. After installation mongo driver for php

참고로 저는 Cent OS 6.4에 PHP 5.5를 사용중입니다. 사용하시는 환경에 따라 디렉토리 구조가 상이할 수 있으니 유의하시기 바랍니다. 기본적인 해결 방법은 상단에 스택오버플로우 링크에서 안내되어 있습니다.

먼저 php.ini에 extension=mongo.so라고 기술한 부분은 삭제합니다. 다음 /etc/php.d/ 디렉토리로 이동하여 mongo.ini 파일을 만듭니다. 작성하실 내용은 다음과 같습니다.

; configuration for php mongo module
; priority=30
extension=mongo.so

1행과 2행은 주석문이니 안쓰셔도 무관합니다. 다음 자신의 서버 환경설정이 담긴 디렉토리로 이동합니다. 저는 Nginx를 사용하고 있고 디렉토리는 /etc/ngnix/conf.d/ 였습니다. 여기에 아까 작성한 mongo.ini의 심볼릭 링크를 생성합니다.

sudo ln -s /etc/php.d/mongo.ini 30-mongo.ini

이제 서버와 MongoDB를 재시작 합니다.

[Scala Note. 1] 변수와 형

Scala에서 변수의 선언은 var과 val의 두 가지 방법을 사용한다.  var는 통상의 변수와 동일하나, val은 초기화 이후 값을 변경할 수 없으며 이는 Java에 final 변수·필드와 동일한 개념이다.

var num:Int = 10
val message:String = "Hello"

변수에 명시적으로 형을 선언하지 않으면 scala는 형을 추론하여 지정한다. 또한 초기화하지 않는 변수는 _ (underscore)로 지정해 둘 수 있다.

val message = "World!"
// message:java.lang.String = World!
val num:Long = _
// num: Long = 0

Scala에서 선언할 수 있는 형과 기본값은 다음과 같다.

설명 기본값
Byte 부호 있는 8비트 정수 0
Short 부호 있는 16비트 정수 0
Int 부호 있는 32비트 정수 0
Long 부호 있는 64비트 정수 0
Char 16비트의 부호없는 Unicode 문자 ‘\0′
String 문자열(Char 시퀀스) null(nil)
Float 32비트 IEEE754단정도 부동소수점 0.0
Double 64비트 IEEE754단정도 부동소수점 0.0
Boolean 참/거짓 false

 
문자열에서 메타문자 사용법은 기본적으로 다른 언어들과 유사하다. 단 더블쿼트(“)를 3개씩 사용해서 감쌀 경우 내부의 모든 문자를 그대로(Raw String) 표시한다.

val es = """\"\'\\"""
// es: java.lang.String = \"\'\\

 
싱글쿼트(‘) 하나로 Symbol 형을 선언할 수 있다.

val s = 'symbol
//s: Symbol = 'symbol
s.name
//res: String = symbol

Scala 설치하기

1. 윈도우 환경

Scala는 JVM 환경에서 돌아가기 때문에 JDK부터 설치합니다(JDK 설치과정은 생략)

Scala는 Scala 다운로드 페이지에서 다운로드 받으시고 설치하시면 끝입니다. 제대로 설치되었는지 확인해 보기 위해서는 윈도우의 커먼트 창을 열고 scala를 입력합니다.

c:\Users\username> scala
Welcome to Scala version 2.11.0 (Java HotSpot(TM) 64-Bit Server VM, Java 1.7.0_21).
Type in expressions to have them evaluated.
Type :help for more information.

scala>

이렇게 REPL 콘솔이 뜨게되면 정상적으로 설치된 것입니다. scala 콘솔에서 나가고 싶을 때는 :q 를 입력합니다.

2. OS X

역시 JDK를 설치해야 합니다.

Scala 홈페이지에서 다운로드 받은것 보다 homebrew를 이용하는 것이 훨씬 편합니다. Homebrew가 설치되어 있지 않으신 분들은 우선 Homebrew부터 설치하시면 되겠습니다. Homebrew 페이지에서 가장 아랫쪽에 설치 방법이 나와있습니다. Homebrew 페이지는 한글로 지원하기 때문에 편리합니다. Homebrew 설치가 끝나면 터미널창에서 다음을 입력합니다.

brew install scala

설치가 완료된 후 역시 scala를 입력하면 REPL 콘솔이 뜹니다. 사용법은 동일합니다.

3. 이클립스 플러그인

http://scala-ide.org/download/current.html에서 자신의 scala 버전과 이클립스 버전에 맞는 플러그인 경로을 복사해 옵니다. 현재 글 작성 시간을 기준으로 scala는 2.11.0이 최신입니다.

Help > Install New Software에 들어가서 위 페이지에서 복사해온 경로로 플러그인 목록을 불러온 다음 전부 설치합니다.

Let’s Scala!

버블링을 활용한Event 부여

동적으로 생성되는 DOM 요소에 이벤트를 부여해야 하는 경우가 있습니다.  먼저 Add Button 이라는 이름의 버튼이 하나 있고, 그 버튼을 누를때 마다 More 라는 버튼이 생성된다고 해봅니다. 그리고 More 버튼을 누를때 마다 console에 More! 라는 메세지를 띄웁니다. 먼저 HTML 문서는 다음과 같이 구성합니다.

<body>
  <button id="add-button">Add Button</button>
  <div id="button-area">
  </div>
</body>

그리고 Javascript(jQuery를 활용합니다)는 다음과 같이 작성합니다.

$('#add-button').on('click', function($event) {
  $event.preventDefault();
  var code = '&lt;button class="more"&gt;More&lt;/button&gt;';

  $('div#button-area').append( code );
  $('.more').on('click', function( $event ) { 
    $event.preventDefault();
    console.log( 'More!' ); 
  });
});

일단 겉보기에는 별다른 문제가 없이 작동합니다. 하지만 콘솔창을 띄워보면 문제가 발생한 것을 알 수 있습니다. More 버튼을 3개 생성한 후 More 버튼을 눌러보면 첫번째 버튼을 클릭했을 때 콘솔창에는 More! 메세지가 3번씩, 두 번째 버튼을 2번씩 생성됩니다.

버튼이 생성될 때마다 ‘more’ 라는 클래스를 가진 DOM 요소에 이벤트를 부여했기 때문에 먼저 생성된 more 버튼에는 버튼을 생성한 횟수만큼 이벤트가 쌓입니다. 이를 다음과 같이 수정해 보겠습니다.

$('#add-button').on('click', function($event) {
  $event.preventDefault();
  var code = '<button class="more">More</button>';

  $('div#button-area').append( code );
  $('.more').off('click').on('click', function( $event ) {  
    //.off('click') 이 추가되었습니다.
    $event.preventDefault();
    console.log( 'More!' ); 
  });
});

이제 정상적으로 작동합니다. 버튼이 생성될 때 일단 more 클래스를 가진 모든 DOM 요소의 click 이벤트를 제거한 다음 다시 click 이벤트를 부여하기 때문이죠. 하지만 뭔가 꺼림직합니다. 버튼을 누를 때마다 Javascript는 모든 more 버튼에 이벤트를 제거하고 생성하는 작업을 반복합니다. 불필요한 자원 낭비죠. 처음 한번만 이벤트를 생성할 수는 없을까요? 가능합니다.

$('div#button-area').on('click', '.more', function( $event ) {
  $event.preventDefault();
  console.log( 'More!' ); 
});

$('#add-button').on('click', function($event) {
  $event.preventDefault();
  var code = '<button class="more">More</button>';

  $('div#button-area').append( code );
});

Add button은 More 버튼을 생성하는 기능만을 수행하고 이벤트를 부여하는 코드를 밖으로 뺐습니다. 근데 이벤트가 More 버튼이 아니라 그 상위 요소인 div#button-area에 지정되어 있군요! 그리고 on() 함수의 두번째 매개 변수가 callback 함수가 아닌 ‘.more’로 변경되어 있습니다. 일단 이 코드는 정확하게 우리가 원하는데로 작동합니다.

이와 같은 현상(?)은 이벤트 버블링이라는 자바스크립트 기능때문에 발생합니다. div#button-area를 포함한 이 DOM 요소의 자식요소에서 클릭 이벤트가 발생하면 누가 이 이벤트를 발생시켰는지 확인합니다. 그리고 on() 함수의 두번째 매개변수로 지정한 ‘.more’ 가 클릭이벤트를 발생시킨 범인(?)으로 확인되면 callback 함수가 비로소 실행됩니다. 이렇게 되면 div#button-area가 존재하는 한 몇개의 More 버튼이 생성되더라도 이벤트는 항상 발생하고, 중복으로 이벤트가 발생하는 일도 없어집니다.

[vs 놀이] 무엇이 더 빠를까?

jQuery는 Javascript 를 사용하는데 있어서 없어서는 안 될 라이브러리가 되었습니다.  하지만 편리함에 비례해 대단히 무거운 녀석입니다.  간단한 사이트나 어플리케이션에서는 눈에 띄는 문제를 일으키지 않지만 프로젝트의 규모가 커지거나 기준이 되는 디바이스의 성능이 낮은 경우 높은 확률로 퍼포먼스 이슈가 발생합니다.

특히 퍼포먼스 문제가 항상 따라다니는 하이브리드 웹 앱의 경우는 특히 신경쓸 부분이 많습니다.  어떻게 코딩하는 것이 좀더 빠른 성능을 낼 수 있을까요. 그래서 남녀노소 누구나 좋아하는 vs 놀이를 해보기로 했습니다.  실험 출처는 http://jsperf.com/ 입니다. 심심할때 한번 들러서 둘러보면 재미있습니다(엉터리 실험도 많으니 소스를 주의 깊게 살피셔야 합니다)

 

Case 1. getElementById() vs querySelector() vs jquery() : ID Selector

var case1 = document.getElementById('target');
var case2 = document.querySelector('#target');
var case3 = $('#target');

가장 느린 결과를 보인 것은 jquery()입니다.
그 다음은 querySelector()로 jquery에 비해 무려 10배 가까운 성능차이를 보였습니다.
가장 빠른 결과는 getElementById()로 querySelector()보다 2배 가까이 빨랐습니다.

 

Case 2. getElementsByClassName() vs querySelectorAll() vs jquery() : Class Selector

var case1 = document.getElementsByClassName('target');
var case2 = document.querySelectorAll('target');
var case3 = $('.target');

가장 빠른 것은 예상데로 네이티브 코드인 getElementsByClassName() 이었습니다.
다소 의외였던 것은 querySelectorAll()과 jquery()의 성능차이가 거의 나지 않았다는 것입니다(참고로 getElementByClassName()은 2배 정도 빨랐습니다).  querySelecorAll이 전반적으로 약간 빠르긴 했습니다만 좋은 환경일 수록 성능은 비슷했고 일부 환경에서는 오히려 jquery가 더 빠른 케이스도 있었습니다.  다만, 구형 안드로이드 기기나 저사양 환경에서는 querySelecor가 우위에 있었습니다.

 

Case 3. className vs setAttribute vs prop() vs attr() vs addClass()

이 실험은 다소 설명이 필요합니다.  각 요소에 class가 하나씩 붙어있고 이 클래스를 지우고 새로운 클래스를 부여하는 작업입니다.  따라서 addClass()의 경우에는 기존 클래스를 삭제하는 removeClass() 라는 단계가 하나 더 존재하게 됩니다.

var elements = $('.class'), i;
var eLength = elements.length;
for( i = 0; i < eLength ; i++ ) {
  elements[i].className = 'change'; // case. 1
  elements[i].setAttribute( 'class', 'change' ); // case. 2
  elements[i].prop( 'class', 'change' ); // case. 3
  elements[i].attr( 'class', 'change' ); // case. 4
  elements[i].removeClass('class').addClass('change'); //case 5
}

반복문 안의 소스들은 케이스 별로 별도로 시행되었다고 가정하겠습니다.
우선 className과 setAttribute가 가장 빨랐으며 이 두 케이스의 성능 차이는 거의 나지 않았습니다.
그 다음은 prop()와 attr()로 위의 경우보다 1/2 정도의 성능을 냈습니다.
마지막으로 removeClass + addClass 가 가장 느렸으며 위 경우보다 1/2정도의 성능을 냈습니다.

 

결론 : 네이티브 Javascript 쪽이 성능면에서는 2배 이상의 우위를 보였습니다.  하지만 성능을 위해 jQuery를 포기하기에는 넘어야 할 산이 너무나 많습니다. 일단 jQuery가 압도적으로 구문의 길이기 짧습니다. 그리고 세번째 실험의 경우 하나의 요소에서 여러 클래스를 관리하는 경우 네이티브 Javascript만으로 해결하다가는 지옥(…)을 경험하실 수 있습니다.  그리고 성능 이전 문제로 event 처리나 $.ajax()는 jQuery가 압도적으로 편리합니다.

따라서 규칙을 정해서 네이티브 Javascript와 jQuery를 적절하게 섞어서 사용하는 것을 권장합니다.  물론 이 경우 가독성을 해치거나 스파게티 코드가 탄생할 가능성도 배제할 수 없습니다. 역시 생산성과 어플리케이션의 퍼포먼스라는 두 토끼를 모두 잡기란 쉽지 않은 듯 합니다.