http://www.kumi-dongkwang.com 을 Django로 만들고 있는데, URL shortening(이하 URL줄이기)이 필요해서 직접 구현하기로 했다.
구글링한 결과 URL 줄이기의 기본 원리는 다음과 같다.
1. 긴 URL을 저장하고 incremental id를 생성한다. (ex. 0부터 증가하는 숫자)
2. 긴 URL의 아이디를 Base62로 인코딩하여 키 저장한다.
3. 2에서 저장한 인코딩 된 키가 우리가 보는 짧은 URL의 주소가 된다. (ex. http://tr.im/0aoi8 에서 0aoi8 이 키가된다.)
* Base62 : 0-9, a-z, A-Z 가 총 62자 이므로 62진수 표기를 하는게 Cool url 을 유지하는 최적의 방법이 된다.
효과는 다음과 같다.1
Digit Base 10 Base 64
2 100 4,096
3 1,000 262,144
4 10,000 16,777,216
5 100,000 1,073,741,824
6 1,000,000 68,719,476,736
보면 알겠지만 6자리로 표기할 때 기존 10진수 ID에 비해 68,719배 높은 표현상의 효율을 보인다.
Django에 익숙하다고 생각하고 디테일한 부분은 빼고, 따라해서 구현하는데 꼭 필요한 부분만 짚고 넘어간다.
적당한 프로젝트 이름(urlShortener)을 설정하고 프로젝트를 생성한뒤 적당한 이름의 앱(url_manager)를 만든다.
Base622 는 위에서 대충 설명했으니 넘어가고 표현방법을 코드로 보자
BASE62 = '01234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
nBASE62 = len(BASE62)
def encode_basen(id, n=nBASE62):
u'''아이디를 입력받아 인코딩한다'''
base = id
rests = []
while base!=0:
quotient, rest = divmod(base, n)
rests.append(BASE62[rest])
base = quotient
return ''.join(rests)
알고리즘은 보통 숫자 m을 n진수로 바꾸는 것과 동일한 방식으로 했다.
즉, 몫이 0이 아닌 동안 n으로 나누기해서 나머지를 값으로 취한다.
(직관적이 아니라면 굳이 비트를 쉬프트하고 이런거 별로 안좋아해서 그냥 제일 이해하기 쉬운 코드로)
자, 이제 인코딩을 했으니 디코딩을 해야하지 않을까? 그러나 우리는 굳이 디코딩을 할 필요는 없다. 다음에서 설명한다.
알고리즘에 따라 URL모델은 ShortURL과 LongURL 두개를 만든다.
이 때, LongURL은 자신의 id를 키 값으로 Base62 인코더에 의해 hashing 되어 ShortURL로 변환된다.
따라서 인코더는 필요하지만 디코더는 필요가 없다.
ShortURL에서 LongURL의 인스턴스와 인코딩된 키값을 가지고 있기 때문에 키를 통해 긴 URL에 바로 접근할 수 있기 때문이다.
LongURL은 실제 연결될 긴 URL을 저장하므로 PositiveIntegeriField인 id 와 URLField인 url 로 구성된다.
단, id는 지정안하면 Django가 알아서 설정해주므로 url필드만 설정한다.
ShortURL은 LongURL의 인스턴스와 id를 base62로 인코딩한 키를 가지고 있어야 한다.
위의 모델링에 따른 url_manager 앱의 models.py 는 다음과 같다.
#coding: utf-8
from django.db
import models
class LongURL(models.Model):
url = models.URLField(unique=True)
def __unicode__(self):
return self.url
class ShortURL(models.Model):
id = models.CharField(max_length=128, primary_key=True)
longUrl = models.OneToOneField(LongURL)
def __unicode__(self):
return self.id
class Meta:
ordering = ['-id']
사실 중요한 부분은 Model부분으로 View부분은 사용자 마음대로 하면 된다.
대부분의 서비스에 사용하는 데이터 디자인은 url을 값으로 넘겨받아 변환 후 저장하는 것이므로
여기서도 대부분의 서비스에서 하는 대로 만들어본다.
#coding: utf-8
import re
from models import WrongURL, LongURL, ShortURL
PATTERN = r'http://[^/^\s]+'
def get_short_id(url):
u'''긴 URL을 받아서 짧은 URL의 id를 반환'''
if not url:
return WrongURL(0)
elif not re.match(PATTERN, url):
return WrongURL(1)
longUrl, isnew = LongURL.objects.get_or_create(url=url)
if isnew:
id = encode_basen(longUrl.id)
try:
return HttpResponse(ShortURL.objects.create(id=id, longUrl=longUrl).id)
except:
raise WrongURL(2)
else:
try:
return HttpResponse(ShortURL.objects.get(longUrl=longUrl).id)
except ShortURL.DoesNotExist:
raise WrongURL(3)
def get_long_url(id):
u'''아이디를 입력받아 인코딩한다'''
try:
return ShortURL.objects.get(id=id).longUrl.url
except:
return '' return ''.join(rests)
코드가 간단하므로 크게 설명할 부분은 없는 것 같다.
WrongURL은 Exception을 상속받은 사용자 예외처리 클래스로 스트래티지 패턴을 적용했다.
ERROR_MSG = {
0: "data error",
1: "it is not a url",
2: "url creation error",
3: "there is no such a url",
}
class WrongURL(Exception):
def __init__(self, key):
self.error_message = ERROR_MSG[key]
def __str__(self):
return self.error_message
def __unicode__(self):
return self.error_message
get_short_id 함수에서 get_or_create 는 쿼리검색결과 매칭된 결과가 존재하면 해당 결과를 가져오고
매칭결과가 없다면 해당 레코드를 생성한다.
그리고 (생성/검색된 레코드 인스턴스, 생성된 인스턴스인지 참/거짓) 튜플을 반환한다.
urls.py 에 다음과 같이 접근 경로를 잡아주면 끝.
(r'^(?P\w+)/?$', 'urlShortener.url_manager.views.long_url'), (r'^get/short/url/?$', 'urlShortener.url_manager.views.short_url'),
python manage.py runserver 0.0.0.0:8080 으로 테스트서버를 작동시키고,
브라우저 주소창에서 http://localhost:8080/get/short/url/?url=줄일URL 을 입력하면 결과를 확인해 볼 수 있다.
실제로 구현되어 돌아가고 있는 사이트인 http://url.kumi-dongkwang.com 의 코드는
http://hg.monolith.pe.kr/urlShortener 에서 확인하고 코드를 다운받아 볼 수 있다.