지그재그에서는 현재 하루에 수천만개의 사용자 로그가 쌓이고 있습니다. 그리고 이 로그를 분석해 사용자가 얼마나 쇼핑몰에 가입을 하는지, 주문을 얼마나 하는지 살피고 있습니다.

그런데 지그재그가 지원하는 수많은 쇼핑몰은 다양한 솔루션을 사용하고 있고, 그에 따라 패턴도 전부 제각각입니다. 따라서 어떤 로그가 가입 페이지인지, 주문 페이지인지 확인하기 위해서 모든 솔루션의 패턴과 대조를 해야 합니다.

오늘은 이러한 대조를 어떻게 하고 있는지 살펴보겠습니다. 분석은 여러가지 언어로 하고 있지만, 여기서는 JavaScript를 살펴보겠습니다.

JavaScript에서 어떤 문자열이 패턴을 포함하고 있는지 검사하는 방법은 다음과 같은 것들이 있습니다. String.prototype.indexOf, String.prototype.includes, String.prototype.match, RegExp.prototype.test.

우선 10만개짜리 로그들을 놓고 간단한 문자열 포함 여부 검사를 각 방법으로 해서 시간을 비교해봤습니다.

fs = require 'fs'
logs = fs.readFileSync('logs', 'utf-8').split('\n')

run = (name, fn) ->
  start = Date.now()
  for i in [0...1000]
    fn()
  console.log "#{name} - #{Date.now()-start}ms"

run 'indexOf', ->
  count = 0
  for log in logs
    if log.indexOf('o') > 0
      count++

run 'includes', ->
  count = 0
  for log in logs
    if log.includes 'o'
      count++

run 'test', ->
  count = 0
  for log in logs
    if /o/.test log
      count++

run 'match', ->
  count = 0
  for log in logs
    if log.match /o/
      count++

다음은 그 결과입니다.

indexOf - 6517ms
includes - 7247ms
test - 13539ms
match - 14369ms

속도 차이가 많아보입니다.

이번에는 검색 패턴을 좀더 실제에 가깝게 해봤습니다.

run 'indexOf', ->
  count = 0
  for log in logs
    if log.indexOf('join.html') > 0
      count++

run 'includes', ->
  count = 0
  for log in logs
    if log.includes 'join.html'
      count++

run 'test', ->
  count = 0
  for log in logs
    if /join\.html/.test log
      count++

run 'match', ->
  count = 0
  for log in logs
    if log.match /join\.html/
      count++

다음은 그 결과입니다.

indexOf - 6812ms
includes - 7947ms
test - 10420ms
match - 10943ms

indexOf, includes에 대해서 시간이 증가하는 것은 예상된 것이지만, 정규식을 쓰는 경우 오히려 시간이 감소했습니다. 혹시 이유를 아시는 분은 알려주시면 감사하겠습니다.

그래도 indexOf가 가장 빠르지만, 우리가 원하는 패턴은 하나가 아닙니다. 패턴을 두개로 늘려봤습니다.

run 'indexOf', ->
  count = 0
  for log in logs
    if log.indexOf('join.html') > 0 or log.indexOf('join_contract.html') > 0
      count++

run 'includes', ->
  count = 0
  for log in logs
    if log.includes('join.html') or log.includes('join_contract.html')
      count++

run 'test', ->
  count = 0
  for log in logs
    if /join\.html|join_contract\.html/.test log
      count++

run 'match', ->
  count = 0
  for log in logs
    if log.match /join\.html|join_contract\.html/
      count++

다음은 그 결과입니다.

indexOf - 12686ms
includes - 13864ms
test - 11690ms
match - 12089ms

패턴이 두개만 되도 정규식이 빠릅니다. 심지어 저희는 패턴이 일단 10개는 넘습니다. 그래서 정규식의 test를 쓰는 것으로 결정을 했습니다. 그리고 알려진 패턴을 추가했습니다.

PATTERN = /join\.html|join_contract\.html|member\/register|register_form\.php|..../

10개미만일때는 그래도 괜찮은데 길어지니까 어디까지가 하나의 패턴인지 눈에 안 들어옵니다. 거기에 일일이 기억하기 어려워서 각 패턴이 어떤 솔루션의 것인지 주석을 달고 싶어졌습니다. 그래서 정규식을 여러 줄로 나눠서 쓸 수 있는지 찾아봤습니다. 몇몇언어(예. Perl)는 정규식 자체를 여러 줄로 나눌 수 있지만 JavaScript는 그런 문법은 없는 것 같습니다.

방법을 찾아보니 regex - How to split a long regular expression into multiple lines in JavaScript? - Stack Overflow를 찾을 수 있었습니다. 그 중에서도 단순히 문자열을 합친 후 RegExp 생성자를 이용하는 것은 문자열 escape에 신경을 써야 해서, 최종적으로는 두번째 답변의 방법을 이용하기로 했습니다.

PATTERNS = [
    /join\.html/, # 솔루션 A
    /join_contract\.html/, # 솔루션 B
    /member\/register/, # 솔루션 C
    /register_form\.php/, # 솔루션 D
    ...
]
PATTERN_RE = new RegExp PATTERNS.map((p) -> p.source).join('|')

이상 패턴 일치 여부 검사 방법이였습니다. (원래는 JavaScript에서 정규식을 여러 줄로 쓰는 방법에 대해서 쓰려던 건데 사족이 붙어서 길어졌네요)

C
const char *str = "2016-12-05";
regex_t rx;
if (regcomp(&rx, "([[:digit:]]{4})-([[:digit:]]{2})-([[:digit:]]{2})", REG_EXTENDED)==0) {
  regmatch_t m[4];
  if (regexec(&rx, str, 4, m, 0)==0) {
    int year = str2int(str+m[1].rm_so, m[1].rm_eo - m[1].rm_so);
    int month = str2int(str+m[2].rm_so, m[2].rm_eo - m[2].rm_so);
    int day = str2int(str+m[3].rm_so, m[3].rm_eo - m[3].rm_so);
  }
  regfree(&rx);
}
C++
string str("2016-12-05");
regex rx("(\\d{4})-(\\d{2})-(\\d{2})");
smatch m;
if (regex_match(str, m, rx)) {
  int year = stoi(m.str(1));
  int month = stoi(m.str(2));
  int day = stoi(m.str(3));
}
CoffeeScript
str = '2016-12-05'
if /(\d{4})-(\d{2})-(\d{2})/.test str
  year = Number RegExp.$1
  month = Number RegExp.$2
  day = Number RegExp.$3
Java
String str = "2016-12-05";
Pattern rx = Pattern.compile("(\\d{4})-(\\d{2})-(\\d{2})");
Matcher m = rx.matcher(str);
if (m.matches()) {
  int year = Integer.parseInt(m.group(1));
  int month = Integer.parseInt(m.group(2));
  int day = Integer.parseInt(m.group(3));
}
JavaScript
var str = '2016-12-05';
if (/(\d{4})-(\d{2})-(\d{2})/.test(str)) {
  var year = Number(RegExp.$1);
  var month = Number(RegExp.$2);
  var day = Number(RegExp.$3);
}
Kotlin
val str = "2016-12-05"
val rx = "(\\d{4})-(\\d{2})-(\\d{2})".toRegex()
val m = rx.matchEntire(str)
if (m!=null) {
  val year = m.groups[1]!!.value.toInt()
  val month = m.groups[2]!!.value.toInt()
  val day = m.groups[3]!!.value.toInt()
}
Lua
str = '2016-12-05'
rx = '(%d%d%d%d)-(%d%d)-(%d%d)'
year, month, day = string.match(str, rx)
year = tonumber(year)
month = tonumber(month)
day = tonumber(day)
Objective-C
NSString *str = @"2016-12-05";
NSRegularExpression *rx = [NSRegularExpression regularExpressionWithPattern:@"(\\d{4})-(\\d{2})-(\\d{2})" options:0 error:nil];
NSTextCheckingResult *m = [rx firstMatchInString:str options:0 range:NSMakeRange(0, [str length])];
if (m!=nil) {
  NSInteger year = [[str substringWithRange:[m rangeAtIndex:1]] integerValue];
  NSInteger month = [[str substringWithRange:[m rangeAtIndex:2]] integerValue];
  NSInteger day = [[str substringWithRange:[m rangeAtIndex:3]] integerValue];
}
Perl
$str = '2016-12-05';
$rx = qr/(\d{4})-(\d{2})-(\d{2})/;
if ($str =~ $rx) {
  ($year, $month, $day) = (int $1, int $2, int $3);
}
PHP
$str = '2016-12-05';
$rx = '/(\d{4})-(\d{2})-(\d{2})/';
if (preg_match($rx, $str, $m)) {
  $year = (int)$m[1];
  $month = (int)$m[2];
  $day = (int)$m[3];
  // if you don't need integers
  list($_, $year, $month, $day) = $m;
}
Python
str = '2016-12-05'
rx = '(\\d{4})-(\\d{2})-(\\d{2})'
m = re.search(rx, str)
if m:
  year = int(m.group(1))
  month = int(m.group(2))
  day = int(m.group(3))
  // if you don't need integers
  year, month, day = m.groups()
Ruby
// using method
str = '2016-12-05'
rx = /(\d{4})-(\d{2})-(\d{2})/
m = rx.match(str)
if m
  year = m[1].to_i
  month = m[2].to_i
  day = m[3].to_i
  // if you don't need integers
  year, month, day = m[1..3]
end

// using pattern-matching operator
str = '2016-12-05'
rx = /(\d{4})-(\d{2})-(\d{2})/
if rx =~ str
  year = $~[1].to_i
  month = $~[2].to_i
  day = $~[3].to_i
  // if you don't need integers
  year, month, day = $~[1..3]
end
Swift
let str = "2016-12-05"
let rx = try! NSRegularExpression(pattern"(\\d{4})-(\\d{2})-(\\d{2})"options: [])
if let m = rx.firstMatch(in: str, options: [], rangeNSRange(location0length: (str as NSString).length)) {
  let year = Int((str as NSString).substring(with: m.rangeAt(1)))
  let month = Int((str as NSString).substring(with: m.rangeAt(2)))
  let day = Int((str as NSString).substring(with: m.rangeAt(3)))
}
C
// basic
tmp = x;
x = y;
y = tmp;

// function
void swap(int *a, int *b) {
  int tmp;
  tmp = *b;
  *b = *a;
  *a = tmp;
}
swap(&x, &y);

// no temporary variable
x = x + y;
y = x - y;
x = x - y;

// no temporary variable 2
x = x ^ y;
y = x ^ y;
x = x ^ y;
C++
swap(x, y);
CoffeeScript
[x, y] = [y, x]
Java
tmp = x;
x = y;
y = tmp;
JavaScript
// basic
tmp = x;
x = y;
y = tmp;

// tricky one-line
x = [y, y = x][0];

// ES2015 (ES6)
[x, y] = [y, x]
Kotlin
tmp = x
x = y
y = tmp
Lua
x, y = y, x
Objective-C
tmp = x;
x = y;
y = tmp;
Perl
($x, $y) = ($y, $x);
PHP
// basic
list($x, $y) = array($y, $x);

// function
function swap(&$x, &$y) {
  $tmp = $x;
  $x = $y;
  $y = $tmp;
}
Python
x, y = y, x
Ruby
x, y = y, x
Swift
swap(&x, &y)