열한번째 날: Coupon Code

저자

@keedi - Seoul.pm 리더, Perl덕후, 거침없이 배우는 펄의 공동 역자, keedi.k at gmail.com

시작하며

언젠가부터 우리는 쿠폰의 홍수 속에 살고 있습니다. 적은 금액의 물건 하나를 주문해도 박스 안에는 몇 백만원치의 쿠폰이 들어있죠. 물론 이런 대부분의 쿠폰은 무의미하다 못해 위험(?!?!)하기까지 하지만, 이런 쿠폰을 처음 본 어머니께서 10만원짜리 상품권이라고 뛸듯이 기뻐하셔서 피식 웃다가, 얼른 가입해서 수익을 실현하라(!!)는 말씀에 어찌 설명드려야 할지 난감해 당황스러웠던 기억이 납니다.

장난 나랑 지금 하냐? 그림 1. 장난 나랑 지금 하냐? (원본)

정상적인 쿠폰이라면 대부분 무의미해 보이지만 유의미한 일련의 복잡한 문자열 코드를 포함합니다. 이후 쿠폰에서 지시하는 온라인 사이트나 또는 오프라인 매장에서 이 코드를 입력하도록 유도하고 해당 코드가 의미하는 할인 또는 혜택을 제공하곤 합니다. 불특정 다수에게 한정적으로 제공하는 서비스일 경우 쿠폰 시스템은 아주 간단하면서도 적절한 해결책입니다. 이런 쿠폰 시스템에서 가장 핵심이 되는 부분은 결국 쿠폰 문자열입니다. Perl을 이용해 쿠폰 문자열을 생성하고 검증하는 방법을 알아보죠.

준비물

필요한 모듈은 다음과 같습니다.

직접 CPAN을 이용해서 설치한다면 다음 명령을 이용해서 모듈을 설치합니다.

$ sudo cpan Algorithm::CouponCode

사용자 계정으로 모듈을 설치하는 방법을 정확하게 알고 있거나 perlbrew를 이용해서 자신만의 Perl을 사용하고 있다면 다음 명령을 이용해서 모듈을 설치합니다.

$ cpan Algorithm::CouponCode

쿠폰 코드란?

쿠폰 코드불특정 다수에게 한정적으로 특정 서비스를 제공하기 위해 널리 쓰이는 시스템입니다. 해당 사용자가 특정 서비스를 이용할 자격이 있는지 확인하기 위한 용도로 쿠폰 코드를 사용하는 것이죠. 사실 흔히 볼 수 있는 누구나 사용할 수 있는 동일한 쿠폰 코드는 약간 변질(?)된 쿠폰이라고 생각합니다만, 대부분의 경우 쿠폰 코드는 이런 자격을 확인할 수 있을 정도로 유일해야 하며, 사용자가 입력하거나 읽을 수 있어야 하기 때문에 과도하게 복잡해서는 안됩니다. 네 자리의 숫자와 영문자로 조합한 그룹으로 구성된 코드는 가장 흔히 볼 수 있는 쿠폰 코드입니다. 필요에 따라 네 자리 그룹의 수를 1 ~ 5 개 또는 그 이상까지 늘려서 유일한 쿠폰 코드를 만들 수 있죠.

# 쿠폰 코드의 예
6GQN
HF1E-YUTV
6LT7-RCRR-0F0X
5F0M-GPXV-5077-THNW
UY8W-546C-UJ7E-5BHH-U3F1

이런 쿠폰은 다음과 같은 특징을 가집니다.

사실 어느정도 무작위의 문자열을 만들 수만 있다면 쿠폰 코드를 생성하는 것은 어렵지 않습니다. 하지만 CPAN의 Algorithm::CouponCode 모듈은 쿠폰 코드의 생성은 물론, 기본적인 체크썸 기능을 포함하고, 더불어 해당 체크썸을 활용해 쿠폰 자체의 기계적 유효성도 검증할 수 있는 방법을 제공합니다.

쿠폰 생성

쿠폰의 생성을 위해 cc_generate라는 함수를 제공하므로 특별한 인자 없이 바로 사용하면 됩니다. 코드는 다음과 같습니다.

use utf8;
use strict;
use warnings;
use feature qw( say );

use Algorithm::CouponCode qw( cc_generate );

say cc_generate() for 1 .. 3;

실행 결과는 다음과 같습니다.

$ coupon-gen.pl
QW0G-LD9M-0XT0
PUXK-F6B7-MLAV
3KXR-W46U-1T6H
$

모듈을 적재할 때 cc_generate() 함수를 내보내기(export)하면 Algorithm::CouponCode::cc_generate()와 같이 전체 함수명을 적지 않고도, 사용할 수 있습니다. 더불어 특별한 인자 없이 호출할 경우 3 그룹으로 구성된 12 자리의 쿠폰 코드를 생성합니다. 유닉스 계열의 경우 /dev/urandom 장치 파일을 참조하거나, 기본 펄의 rand() 함수와 시간 및 프로세스 아이디의 조합으로 무작위 바이트를 생성하고 이를 이용하죠. 좀 더 짧은 쿠폰 코드가 필요하거나 긴 쿠폰 코드가 필요하다면 cc_generate() 함수에 인자로 parts 값을 지정합니다. 공식 문서 상으로 parts 값은 1에서 6 사이의 값임을 가정하는군요.

#
# Generates: W2DK
#

my $coupon_1_group = cc_generate( parts => 1 );

#
# Generates: W3TL-LKLN-M76R-GP3H-U6ET-DDJF
#

my $coupon_6_group = cc_generate( parts => 6 );

매 번 호출할 때마다 새로운 쿠폰 코드를 생성하므로 미리 발급해야 한다면 필요한 횟수만큼 호출하거나 또는 필요할 때 호출해서 발급하면 됩니다.

쿠폰 검증

Algorithm::CouponCode 모듈이 생성하는 쿠폰 코드는 자체적으로 체크썸을 관리하기 때문에 코드의 의미적인 유효성이나 유일성은 차치하더라도 코드 스스로의 기계적인 유효성을 검증하는 것이 가능합니다. 기본적으로 그룹(파트, part) 단위로 생성하는데 이 하나의 그룹은 네 자리의 문자열을 가집니다. 이 중 세 자리는 2 ** 15 개의 난수 값 중 하나를 의미하며, 마지막 한 자리는 앞선 난수 값과 더불어, 해당 그룹이 몇 번째 그룹인지까지 고려해 체크썸을 만듭니다. 따라서 네 자리마다도 유효한지는 물론 해당 네 자리의 수가 적절한 그룹에 속한 것인지도 판별할 수 있기 때문에 유효하지 않은 코드의 경우 사전에 걸러내기 좋습니다. 검증을 위해서는 cc_validate() 함수를 사용합니다.

use utf8;
use strict;
use warnings;
use feature qw( say );

use Algorithm::CouponCode qw( cc_validate );

my @coupon_codes = qw(
    6KB4-9DLW-566U
    HX0K
    394T-E1X4
    7WQA-9PN4-LNRX-MGP4
    M6L4-OF9O-FLYN
    6PFN-3DCR-T9IL
    LSGPAHAN6RML
);

for my $cc (@coupon_codes) {
    my $normalized_cc = cc_validate(
        code  => $cc,
        parts => 3,
    );
    say "$cc : " . ( $normalized_cc ? "valid($normalized_cc)" : "invalid" );
}

cc_validate() 함수는 codeparts 두 개의 인자를 입력 받습니다. code의 경우 검증하려는 코드를 입력하면 됩니다. parts의 경우 검증할 쿠폰 코드의 그룹 개수를 의미하며 특별히 언급하지 않으면 기본 값은 3입니다. 실행 결과를 살펴보죠.

$ ./coupon-validate.pl
6KB4-9DLW-566U : valid(6KB4-9DLW-566U)
HX0K : invalid
394T-E1X4 : invalid
7WQA-9PN4-LNRX-MGP4 : invalid
M6L4-OF9O-FLYN : valid(M6L4-0F90-FLYN)
6PFN-3DCR-T9IL : invalid
LSGPAHAN6RML : valid(L5GP-AHAN-6RML)
$

몇가지 흥미로운 결과를 확인할 수 있습니다. M6L4-OF9O-FLYN 쿠폰 코드의 경우 유효하지만 M6L4-0F90-FLYN로 변경되었습니다. 이는 사람들이 흔히 착각하기 쉬운 문자인 영문자 O, I, Z, S를 각각 0, 1, 2, 5로 변환해서 인지하기 때문입니다. 따라서 cc_validate() 함수는 입력 받은 쿠폰 코드 중 특정 문자를 치환한다음 그 코드의 유효성을 검사합니다. 이 때 원천적으로 O, I, Z, S와 같은 문자는 애초에 치환할 목적으로 간주하고 사용하지 않기 때문에 중복과 관련한 문제는 없습니다. 더불어 LSGPAHAN6RML의 경우 역시 유효하며 L5GP-AHAN-6RML로 표시하는데, 마찬가지로 S5로 변환했으며, 최초 입력에는 없는 - 기호도 첨가해준다는 점을 알 수 있습니다. 유효하지 않은 쿠폰 코드의 경우 undef를 반환하므로 간단히 참, 거짓 논리 점검으로 분기할 수 있습니다.

더불어 Algorithm::CouponCode 모듈은 릴리스 타르볼 파일 안에 jQuery 플러그인을 제공하므로 클라이언트인 브라우저 단에서 코드의 문법적인 유효성 검사를 할 수 있습니다.

CouponCode jQuery 플러그인 그림 2. CouponCode jQuery 플러그인 (원본)

다만 해당 플러그인은 UI와 강하게 결합되어 있고, 자체적으로 값을 체크하기에 썩 적합하지는 않습니다. 배포되는 jQuery 플러그인 소스를 참고해 쿠폰 코드를 체크할 수 있도록 리팩터링 해보았습니다. 실제 로직은 펄 코드나, 제공되는 자바스크립트 파일과 대동소이합니다. 다음은 리팩터링한 자바스크립트 코드입니다.

function symbolSet() {
  return '0123456789ABCDEFGHJKLMNPQRTUVWXY';
}

function badSymbol() {
  return new RegExp('[^' + symbolSet() + ']');
}

function validate(code, parts) {
  var partCodes = code.split('-');
  if (partCodes.length != parts) {
    return false;
  }
  var newCodes = [];
  for (var i = 0; i < partCodes.length; i++) {
    var normalizedCode = validateOneField(partCodes[i], i + 1);
    if (normalizedCode === false) {
      return false;
    }
    else {
      newCodes.push(normalizedCode);
    }
  }
  return newCodes.join('-');
}

function validateOneField(val, i) {
  if (val == '') {
    return;
  }
  var code = cleanUp(val);
  if (code.length > 4 || badSymbol().test(code)) {
    return false;
  }
  if (code.length < 4) {
    return false;
  }
  if (code.charAt(3) != checkDigit(code, i)) {
    return false;
  }
  return code;
}

function cleanUp(code) {
  code
    = code.toUpperCase()
    .replace(/ /g, '')
    .replace(/O/g, '0')
    .replace(/I/g, '1')
    .replace(/S/g, '5')
    .replace(/Z/g, '2')
    ;
  return code;
}

function checkDigit(data, pos) {
  var check = pos;
  for (var i = 0; i < 3; i++) {
    var k = symbolSet().indexOf(data.charAt(i));
    check = check * 19 + k;
  }
  return symbolSet().charAt(check % 31);
}

실제로 자바스크립트 안에서는 다음과 같이 사용할 수 있습니다. :-)

console.log( validate("T13P-2LMP-E0B5", 3) );
console.log( validate("T13P-2LMP-E0B5", 2) );
console.log( validate("T13P-2LMP-E0B5", 4) );
console.log( validate("FLY6-TYPQ-72JJ", 3) );
console.log( validate("PM98-W5MX-1RD5", 3) );
console.log( validate("PM98-W5MX-1RDS", 3) );
console.log( validate("PM98-W5MX-2RDS", 3) );

정리하며

Algorithm::CouponCode 모듈의 경우 생성하는 코드는 그룹 단위로 각 그룹은 네 자리의 무작위 문자열로 구성됩니다. 이 문자열의 네 자리 중 앞 세 자리는 실제 무작위 코드로 15 비트의 무작위 문자열을 가지며 마지막 한 자리는 체크썸으로 코드의 기계적 유효성을 검증하는 용도로 사용합니다. 따라서 1 그룹의 쿠폰 코드(예. 6G75)는 32,768개를, 2 그룹의 쿠폰 코드(예. DKV6-8LFD)는 약 10억개를, 3 그룹의 쿠폰 코드(예. 6P29-XR39-LF62)는 2**45개, 약 35조개(!)의 유일한 코드를 가집니다. 생각보다 꽤 많죠? 쿠폰이 필요할 때 잊지말고 펄과 함께 Algorithm::CouponCode 모듈을 사용해보세요. 꽤 많은 수고를 덜 수 있답니다. 이런 시스템을 구축하거나 사용하지 않고 수작업으로 쿠폰을 만들고 일일이 확인하는 상황을 본적이 있긴한데, 솔직히 상상만해도 머리가 다 지끈하네요. ;-)

xkcd: Coupon Code 그림 3. xkcd: Coupon Code (원본 / 출처)

Enjoy Your Perl! ;-)

P.S.

참! 쿠폰 코드의 각각의 그룹 별 순서를 고려한 유효성은 관리되나 전체 쿠폰 코드에 대한 체크썸은 없으니 이 점도 유의하세요. 더불어 쿠폰 코드의 기계적 유효성과 별개로 실제 유효성과 쿠폰 코드의 유일함은 발급처에서 관리해야 함을 잊지마세요! 즉, 여러분의 몫이랍니다. ;-)

EOT

blog comments powered by Disqus