본문 바로가기
42

URL Shortener 개발기

by jay-choe 2021. 8. 6.

1. 개발 배경

 

 

 방문 예약 서비스를 개발 중, 사용자들이 방문 신청을 하면, 입장할 때, 사용할 수 있는 QRcode의 Url을 문자메시지로 보내주려고 했는데, 문자메시지를 보냈을 때, 문자메시지의 길이가 너무 길어서 3개의 문자로 분리되어서 전송이 되었습니다. 문자메시지 전송 플랫폼은  AWS SNS를 사용을 했었는데, AWS SNS의 문자메시지 전송 규칙 때문이었습니다. 전송 규칙은 문자메시지 길이가 140 bytes가 넘어간다면 다음 문자메시지로 넘어가고, 결국엔 140 bytes를 넘기는 긴 문자를 보내면 위 이미지처럼 문자가 나뉘어서 전송이 가며, 문자가 나뉘어서 전송이 될 때, 순서가 보장되지 않는다는 것이었습니다.

 또한, 만약 url 길어져서 3통의 문자로 가게 된다면, 사용자 입장에서 링크를 클릭할 수가 없는 문제와, 입장 시에 3개의 문자를 합쳐서 링크로 열어야 하는, 편의를 위한 개발이 오히려 불편을 만드는 문제가 있었습니다.
이러한 이유로 방문 예약 서비스에서는 해당 문제의 해결책으로 단축 Url을 만들어서, 1통의 문자만으로 해당 문제들을 해결하기로 했습니다.

 

2. 단축 Url이란?

 

 긴 원본 Url을 단축시켜서, 간결한 Url로 만드는 기술이며, 가장 대표적인 서비스로는 bitly가 있습니다.

인터넷을 하다 보면 혹은, 문자 메시지나 메신저 등을 통해서 다음과 같은 Url을 보셨을 겁니다. (https://bit.ly/3lsNzhu)

이는 bitly에서 단축된 Url이며 클릭 시 단축되기 전 Url로 이동합니다.

예를 들어, 친구들을 집에 초대했을 때, 오는 길을 알려주기 위해 오는 길을 나타내 주는 링크를 원본 그대로

https://map.naver.com/v5/directions/14137066.166526776,4501798.089970242,%EA%B2%BD%EA%B8%B0%EB%8F%84%20%EA%B3%BC%EC%B2%9C%EC%8B%9C%20%EA%B3%BC%EC%B2%9C%EB%8F%99,,/14143779.945204053,4506609.959716333,%EC%84%9C%EC%9A%B8%ED%8A%B9%EB%B3%84%EC%8B%9C%20%EA%B0%95%EB%82%A8%EA%B5%AC%20%EA%B0%9C%ED%8F%AC%EB%8F%99,,/-/transit?c=14138494.1284269,4504450.6978522,13,0,0,0,dh

이렇게 보내주는 것과 (물론 카카오톡 공유 같은 기능이 따로 있기는 하지만 긴 원본 url을 단축시키는 것을 확연하게 보여드리기 위해 예시를 들었습니다)

해당 url을 단축시켜서 https://bit.ly/3lsNzhu 이렇게 보내주는 것은 분명 간결함에 차이를 나타내 주며, 메신저나 , 문자메시지를 사용 시 단축 Url을 사용하는 것은 가독성에 상당한 장점이 있습니다.

 

3. 구현

먼저 구현 시 가장 핵심이 되는 것을 2가지로 나누면 다음과 같습니다.

 

1) Url을 충분히 간결하게 단축시켜야 합니다.

  bitly 서비스의 경우 위의 단축 Url(https://bit.ly/3lsNzhu)처럼 원본 도메인 + /{value}의 형태로 되어있는데, 이 값이 7글자인걸 감안했을 때, 이 정도 길이(6~7글자)가 적절하다고 생각을 했습니다.

 

그렇다면 원본 Url을 어떻게 단축시킬 수 있을까요?

상용서비스인 bitly를 벤치마킹해서, 데이터베이스에 추가 시 원본 Url의 중복을 허용하는 케이스와 허용하지 않는 케이스로 분류해봤습니다.

 

(1) 원본 Url의 중복을 허용하는 경우

 bitly 서비스를 벤치마킹해보면, 로그인을 한 사용자와, 로그인을 하지 않은 사용자에 따라서 다른 방식을 채택합니다.

먼저 로그인을 하지 않은 사용자의 경우는

 

 

 

  동일한 url을 단축시켜도 값이 다르게 나옵니다. 즉 중복을 허용하는 방식입니다. 이 방식은 database의 id값을 인코딩하는 방식입니다.

Mysql 기준으로 원본 Url을 저장할 테이블의 id에 AUTO_INCREMENT 속성을 주어서 데이터 삽입시 일정 숫자만큼 자동 증가를 하게 하며 이 id값을 Base62로 인코딩하여서 사용합니다.

이를 구성하기 위한 최소한의 테이블 구조는 다음과 같습니다.

 

Base62

 

 우선 database의 id값을 Base62로 인코딩한다고 했는데, 기존에 Base64 인코딩 방식은 많이 들어보셨어도, Base62는 생소하실 수도 있을 겁니다. 우선 Base64 인코딩에 사용되는 문자는 url에서의 단점이 있습니다.

해당 테이블을 보면 62번은 + 기호이고, 63번은 / 기호입니다.

Url에서는 "/"를 구분자로 사용을 하기 때문에, 만약에 인코딩을 했는데 /home으로 나오게 된다면, 기존 로직과는 다르게 동작할 수 있습니다. (만약 GET /url/{encodedUrl}로 요청을 한다고 가정했을 때, 저렇게 "/" 슬레시가 구분자로 들어오게 된다면, GET /url/home 이렇게 요청이 보내짐으로 path variable 매핑이 불가능해집니다. 추가로 다른 엔드포인트를 호출할 수 있는 사이드 이펙트까지 생길 수 있습니다.) 또한 + 기호도, url에서는 특수한 목적(공백을 나타냄)으로 사용된다고 합니다.(참고)

따라서 Base62는 이런 url에 safe하지 않은 문자들을 배제한, 즉 Url에 safe 한 문자들만 모은 인코딩 방식입니다. 위의 테이블에서 63, 62번을 제외하게 된다면, 알파벳 대, 소문자와 숫자만 남게 됩니다.

위에서 언급한 bitly서비스도 자세히 보면 https://bit.ly/3lsNzhu "/3lsNzhu"에 사용된 문자들을 보면, Base62 인코딩 방식을 사용하고 있음을 추론해볼 수 있습니다.

그렇다면 bitly 서비스는 왜 로그인을 하지 않은 유저가 생성하는 단축 Url을 중복을 허용해서 매번 생성 시마다 새로운 Url을 만드는 것일까요?

 

 중복을 허용하는 것은 비효율적인 문자열 연산을 피하려는 이유가 있습니다. 새로운 단축 Url을 생성하려고 할 때, 기존에 같은 Url이 있다면, 동일한 단축 Url을 만드는 것은 비효율적 일 것이라고 생각하실 수도 있습니다. 하지만 중복을 허용하지 않기 위해, Unique 제약조건을 걸거나, 데이터를 삽입하기 이전에 , 조회 연산으로 해당 Url이 있는지 확인하는 방법을 사용한다면, 데이터베이스 내부에서 문자열 비교 연산이 많이 일어나며 이는 상당히 비효율적 일 것입니다.

 예를 들어, 원본 url들이 https://bit.ly/abcd , https://bit.ly/abcbd 이런 식으로 동일한 도메인에 뒤에 오는 문자만 다르게 된다면, 앞에 도메인까지는 문자열 비교 연산이 이루어져야 합니다. 따라서 기존에 Url이 있는지 비교하지 않고 삽입하면, 즉 원본 Url의 중복을 허용한다면 이런 비효율적인 연산을 하지 않을 수 있습니다.

 

(2) 원본 Url의 중복을 허용하지 않는 경우

  bitly의 경우 로그인을 한 유저의 서비스 중에는 Url의 중복을 허용하지 않는 서비스가 있습니다.

 

 로그인을 한 후 생성하는 Url의 경우에서는 보시는 것과 같이, 위와 똑같이 네이버의 Url을 단축시켰지만, 아예 같은 Url은 Name을 부여해서 동일한 Url을 단축시킨다면, 같은 값을 유지하게 됩니다. 추가적으로 bitly에서는 로그인한 유저의 경우 요금제에 따라서, 해당 단축 Url을 관리해줍니다. 저 같은 경우는 무료요금제여서 총 해당 Url이 몇 번 클릭되었는지 혹은 클릭하기 이전의 Url은 무엇인지(referer), 어느 국가에서 클릭했는지의 정보를 주는 것을 확인할 수 있습니다. 유료로 사용하게 된다면 더 많은 혜택을 얻을 수 있습니다.

 

 이처럼 중복을 허용하지 않는 방식의 이점은 해당 단축 Url의 세부 정보를 저장하고 통계를 낼 수 있다는 점이 있습니다.

 중복을 허용하지 않고 Url을 단축시키는 방법은 원본 Url을 해싱하는 것입니다. 원본 Url을 해싱하고 해시값에 인덱스를 부여하면, 중복을  허용할 때의 주안점인 데이터베이스 내부에서의 불필요한 문자열 연산을 해결할 수 있습니다. 

 

그런데 해시함수를 사용하면 원본 Url보다 길이가 더 길어질 수 도 있지 않나요?

 

 

네 맞습니다. 해당 테이블을 보시면, MD5 같은 경우에는 16글자를 사용합니다. 물론 adler32 같은 32bit의 해시 함수도 있고, 이는 16진수 기준으로 8글자를 만들어 내지만, 16진수의 8글자로 Base62 인코딩을 하게 된다면, 6글자가 나오게 되는데, 이는 글자 수를 많이 줄여주는 장점이 있지만, 단점으로는 고르지 못한 분포도 및 충돌이 생길 확률이 일정 수준 이상의 데이터를 넘어가면 높아진다는 것입니다. 

 

 32비트의 해시 함수의 경우, 77163건의 데이터를 넘기게 되면 해시 충돌이 발생할 가능성이 50프로가 됩니다. 이는 생일 패러독스의 확률 계산이며, 실제 운영시 충돌이 발생한다는 확실성은 없지만, 그래도 대비를 해야 했습니다.

결론적으로 분포도가 고르고, 안정적인 SHA-256 해시 함수를 사용해서, 앞의 10글자를 자르고, 해당 16진수 값을 Base62로 인코딩하여 사용하기로 했습니다.

 

 우선 인코딩한 결과 값은 7자리의 문자열이 나오게 되는데, 이는 상용화되고 있는 서비스인 bitly에서 보이는 7자리를 보고, 7자리 문자열이면 62 ^ 7을 나타내며, 3.5216146e+12의 숫자 범위를 나타내며, 이는 단축될 수 있는 url의 개수이기 때문에, 우선 단축할 수 있는 개수면에서는 걱정이 없게 되며, 추가적으로 해시값의 앞 10글자를 자른 것이기 때문에, 32비트의 해시함수보다 충돌 가능성이 더 낮아지는 장점도 챙길 수 있게 되었습니다.

 

만약 해시 충돌이 발생한다면?

 SHA-256 해시 알고리즘이 충돌할 가능성은 없겠지만, 앞에 10글자를 자른 문자열이 겹칠 가능성은 있습니다. 만약에 겹칠 경우, 기존의 해시 충돌을 해결하는 방법인 체이닝이나, 개방 주소 방법을 쓰지 않고, 이 글에서 감흥을 얻어서 32 글자 중에서 10글자를 Unique 함을 만족시킬 때까지 추출하는 방향으로 구성하였습니다.  이 방법은 다른 해시 충돌의 해결책들보다 구현이 훨씬 쉽습니다. 하지만, 이 방법도 완전하게 해시 충돌을 처리하는 것은 아닙니다.  물론 현 상황에서 너무 먼 미래의 상황까지 고려를 해서 개발을 한다는 것은 구현 자체를 지연시키고, 또한 해시 충돌 자체도 일어날 확률이 적기 때문에 우선은 기능 구현에 중점을 둔 해결책입니다.

Done is better than perfect

결론적으로는, 방문자 서비스는 단축 Url 생성시 여러가지 정보를 같이 확인 할 수 있는, 원본 Url의 중복을 허용하지 않는 방식의 이점을 채택하기로 했습니다.

 

2) 사용자가 해당 Url을 클릭 시, 원래의 Url로 리다이렉트 되어야 합니다.

 사용자가 단축된 Url(단축 Url 도메인 + /{value})을 클릭한 경우, 해당 value 값을 다시 Base62로 decoding 한 후, 데이터베이스를 조회해서, 원본 Url로 리다이렉트 시킵니다.

 

그렇다면 잘못된 값을 요청하는 경우는?

 

bitly서비스의 경우에는 유효하지 않은 값을 주었을 때 bitly 도메인 내부에서 처리를 하는데

이런 식으로 자체 도메인 내부에서 잘못되었다는 페이지를 보여줍니다.

하지만, 방문 예약 서비스에서는 단축 Url 서비스를 api 서버로 사용을 할 것이기 때문에, 잘못된 Url 요청 시에 요청을 받는 도메인에서 처리하는 것은 api서버로서의 역할을 벗어난다고 생각을 했고, 사용자들이 단축 url을 요청을 하는 것은 문자메시지를 통해서 클릭을 하는 것이기 때문에 JSON으로 에러 메시지와 상태 코드로 응답을 하는 것은 적절하지 않았습니다. 그래서 잘못된 값(데이터 베이스 내부에 없는 값)을 요청하는 경우 프론트엔드의 에러 페이지로 리다이렉트 시키도록 하였습니다.

 

실행 흐름

그렇다면 위의 내용들을 고려해서 실제로 만든 서비스의 로직을 살펴보겠습니다.

 

1. 단축 Url을 생성할 때

 

 

단축 Url 서비스를 운영하고 있는 도메인에 /url이라는 엔드포인트로 원본 Url을 POST로 요청을 보내게 된다면,

단축 Url 서비스에서는 해당 Url을 해싱하고 데이터베이스에 저장하고, 요청을 보낸 서버에 해시값의 앞 10자리를 자르고, Base62 인코딩 한 값 7자리 문자열을 응답해줍니다. 이에 값을 받은 서버는 해당 Url 서비스의 도메인 링크에 + "/응답 값"을 붙여서 단축 Url 링크로 사용할 수 있게 됩니다.

 

 

2. 사용자가 실제 링크를 클릭했을 때

 

사용자가 링크를 클릭했을 경우, 인코딩 값을 GET 요청을 통해서 단축 Url 서비스에 보내게 됩니다.

단축 Url 서비스에서는 해당 값을 Base62으로 디코딩한 후, 원본 해시값의 앞 10자리를 얻어서 조회를 하며,

조회에 성공 시 데이터베이스에서 가져온 원본 Url로 사용자를 리다이렉트 시킵니다.

해당 코드는 여기서 확인 할 수 있습니다.

 

후기

처음에 단축 Url 서비스를 만드는 것이 몇몇 자료들을 찾아보면서, 생각보다 간단하다고 생각을 했었습니다. 하지만 실제로 구현을 하는 것은 생각보다 신경 쓸게 엄청 많았었습니다. 자료를 찾아보면서 공부만을 하는 것보다, 실제 부딪히면서 경험을 하는 것이 성장에 더 많은 도움이 된다는 것을 깨달았습니다. 추가적으로, 특정 서비스를 개발을 하려고 할 때, 이미 상용화되고 있는 서비스를 밴치 마킹하는 것이 정말 많은 도움이 된다는 것을 느꼈고 가능하면 상용화되고 있는 서비스의 유료로 사용되는 부분과 무료로 사용되는 부분의 차이점을 구별을 하는 것이 서비스가 돌아가고 있는 부분에 시각을 넓히는데 도움이 된다고 느꼈습니다.

'42' 카테고리의 다른 글

QueryDSL 도입기  (0) 2021.11.09
Java Out Of Memory Error: Java heap space  (0) 2021.09.19
Api key 적용하기  (0) 2021.08.22