ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • API (3) - Url Shortener
    Back-end/나만의 API 2021. 8. 13. 16:35
    반응형

    안녕하세요. 이번 포스팅은 Url Shortener API를 개발한 경험을 정리하려고 합니다.

    일단 Url Shortener은 우리 주위에 많은 기업, 블로거들이 이용하고 있습니다.

    예를 들면 https://bit.ly/2V7X379 같은 도메인을 자주 보셨을겁니다.

    긴 Url을 줄여 가독성도 올리면서 또한 메일이나 메신저로 Url을 보낼 경우에도 제약을 피할 수 있다는 장점이 있습니다.

    또한 인스타그램에 인플루언서들도 많이 이용하고 있는 추세입니다.

    일단 대표적인 Url ShortenerBitly를 예시로 들겠습니다.

    Bitly는 회원들의 도메인을 줄여주는 역할도하지만 다양한 기능을 추가제공하고 있습니다.

    예를 들어서 Url의 클릭 수, 어느 나라에서 나의 Url에 접근했는지등 다양한 정보를 제공해주고 있습니다.

    이러한 장점에 의해서 많은 사람들이 이용하고 있습니다. 그럼 Bitly를 기반으로 Url Shortener를 만들어보겠습니다.

     

     

    🔑  동작 원리

    첫 번째로 Bitly는 어떻게 동작하는지 알아보겠습니다.

    로그인을 하지 않고 홈페이지에서 Url을 변환해봤습니다. 결과는 계속 다른 Short Url이 출력되는 것을 볼 수 있습니다.

    즉 Url의 중복 변환을 허용하고 있다는 뜻입니다.

    다음으로는 로그인을 한 후 Url변환을 해보겠습니다.

    계속 시도해도 똑같은 Short Url을 반환해줍니다. 즉 중복이 허용되지 않는다는 특징이 있습니다.

    그렇다면 저렇게 중복허용과 허용치 않음을 나누는 이유는 뭘까요?

    로그인을 하고 돈을 지불하면 짧아진 Url을 Bitly에서 관리해주고 다양한 기능을 제공해주기 때문입니다.

    로그인을 하지 않고 만든 Url은 언젠간 소멸되고 사용자가 어떻게 접근하고 얼마나 접근하는지 정보를 저장하지 않습니다.

    결과적으로는 Bitly가 정보를 관리해주느냐? 아니면 변환한 Url을 저장만 해주고 Redirect만 시켜주느냐의 차이입니다.

    그럼 저는 크게 2가지 경우로 나눠서 기능 구현을 설계해보도록 하겠습니다. 중복의 허용을 기준으로 설계하겠습니다.

     

     

     🧭  설계

    1. 중복을 허용하는 경우

     

    중복을 허용하는 경우는 계속해서 같은 Url이더라도 다른 Short Url을 생성해야합니다.

    즉, Origin Url은 중복하되 Short Url은 중복해서는 안된다는 뜻입니다.

    이것을 어떻게 구현할 수 있을까요? 바로 DB의 ID(AUTO_INCLEAMENT)값을 이용하는 것입니다.

    ID값을 인코딩하면 중복되지 않기때문에 안전하게 사용 할 수 있습니다.

    근데 여기서 궁금증이 하나 생깁니다.

    DB의 ID값을 DB에서 생성해 가져와야합니다. 아니면 프로그램에서 생성해서 넘겨 줄 수 있습니다.

    그러나 프로그램에서 생성 할 경우 프로그램이 종료되었다가 다시 켜지면 데이터는 휘발하게됩니다.

    그래서 어떻게든 DB에 접근해서 값을 가져와 해싱하고 BASE62진수로 바꿔줘야합니다.

    그런데 여기서 문제는 효율성입니다. 아래의 그림을 잠시 보시죠.

    프로그램을 가동하고 시간을 측정해봤습니다. 일단 로직에 SAVE기능 중복 체크기능은 무조건 있어야합니다.

    물론 중복이 일어날 확률은 매우 적지만 그래도 체크해야합니다. 그럼 위의 두 기능은 DB에 접근해봐야겠죠?

    DB에 접근하고 값을 가져오는 시간이 거의 80~90%에 해당한다는 사실을 눈으로 확인하실 수 있습니다.

    그러면 ID값을 가져와 인코딩을 하면 시간이 더 많이 걸릴 수도 있다라는 생각이듭니다.

    그렇게 생각을 해본 결과 오직 기능구현에 초점을 맞췄습니다. 로그인을 안할 경우 기능은 크게 두 가지입니다.

    중복되는 Short Url은 안될 것, Base62인코딩 된 값으로 접근했을 때 OriginUrl에 Redirect가 가능할 것.

    결과적으로 저는 아래와 같이 난수를 발생시켜 Hash알고리즘을 이용해 16진수 64글자를 얻은 뒤 랜덤으로 10글자를 추출하여 62Base로 인코딩하는 방법을 채택하였습니다.

     

     

    또 여기서 질문을 할 수 있습니다.

    왜 62Base로 바로 인코딩하면 되지 Hash알고리즘을 사용하냐라고 질문하실 수 있습니다.

    그것은 중복되는 확률을 더 적게하기 위해서입니다. 그리고 Hash알고리즘을해도 프로그램 실행시간 중 약 2%정도 차지합니다.

    결과적으로 우리가 String을 인코딩하는 과정은 많아도 실행시간 중 10~15%정도밖에 되지 않기에 어떻게든 중복만 줄이면 됩니다.그래서 Hash알고리즘 + Random10글자 추출을 이용하여 더 견고하게 프로그램을 구성하였습니다.

     

     

    이제는 인코딩하는 과정을 설명드리겠습니다.

    저희는 인코딩을 할 때 최대한 많은 경우의 수로 인코딩을 해야합니다.

    대부분 16진수를 베이스로 인코딩을 하는데 저는 62진수를 베이스로 인코딩을 하겠습니다.

    왜냐하면 더 많은 경우의 수를 가져 중복을 최대한 피할 수 있기 때문입니다.

    예를 들어 4자리를 인코딩한다고 했을 때 16진수65,536가지, 62진수14,776,336가지의 경우의 수가 나옵니다.

    훨씬 중복될 확률이 낮아집니다. 그리고 64진수에서 '+''/'은 제외해주셔야합니다.

    왜냐하면 '+'같은 경우는 Url의 공백을 나타내주는 문자이고 '/'경로를 나타내주는 문자이기때문입니다.

    예를 들어 '+'를 인코딩 했다고하면 Url중간에 공백을 인식못해 다른 Path로 접속할 수 있습니다.

    다음에 '/'를 인코딩 해버리면 구분자를 인식하지 못해 Path가 아예 달라져버리는 상황이 발생할 수 있습니다.

    이렇게 하여 '+''/'을 제외한 62베이스 인코딩으로 바꾸겠습니다.

     

    다음으로  해쉬 알고리즘에대해 알아보겠습니다.

    해쉬 알고리즘은 원래 파일을 전송하고 받을 때 무결성 검사에 사용하는 알고리즘입니다.

    글자가 하나만 달라져도 아예 다른 값이 나옵니다. 해쉬 알고리즘을 이용하여 12와 13을 바꿔보겠습니다.

    이렇게 아예 달라지는 모습을 보여줍니다.

    //12
    example.demo.ShortenApplicationTests   : 5aadb45520dcd8726b2822a7a78bb53d794f557199d5d4abdedd2c55a4bd6ca73607605c558de3db80c8e86c3196484566163ed1327e82e8b6757d1932113cb8
    //13
    example.demo.ShortenApplicationTests   : 413f2ba78c7ed4ccefbe0cc4f51d3eb5cb15f13fec999de4884be925076746663aa5d34476a3df4a8729fd8eea01defa4f3f66e99bf943f4d84382d64bbbfa9e

     

    위의 사진과 같이 해쉬 암호화 알고리즘의 종류입니다. 요즘 MD5는 보안적으로 취약하기 때문에 사용하지 않는다고 합니다.

    MD5는 9e107d9d372bb6826bd81d3542a419d6  32글자를 16진수로 나타내어줍니다.

    분포도가 고르지 못하여 나중에 데이터가 많아질 경우 충돌이 발생하게 될 수도 있습니다.

     

     

    그래서 저는 제일 많은 선택지가 나오는 SHA512를 선택하겠습니다.

    SHA512는 반환 값이 64글자의 16진수로 나타내어 줍니다.

    그럼 16Bit니깐 한 글자당 4Bit로 구성되어있겠죠? 그럼 여기서 잠깐 생각을해보겠습니다. 저희는 62진수로 나타내고 싶습니다.

    그럼 6Bit(0 ~ 64)를 읽어들여야 합니다. 아래 사진은 4Bit를 6Bit로 읽어 들이는 과정입니다.

     

     

    그럼 이렇게 읽어서 62진수 글자 7글자(최종결과)를 만들려면 몇 Bit가 필요할까요? 바로 42Bit가 필요합니다.

    그런데 저희는 40 or 44Bit를 얻을 수 밖에 없습니다.

     

    <40Bit의 경우>

    "0000000000"일 경우 : aaaaaab

    "ffffffffff"일 경우 : twksJO6

     

    최대 값과 최소 값이 인코딩 되어져 나온 모습입니다. 7자리 범위내에서 모든 것이 다 해결되는 모습입니다.

    그래서 40Bit를 사용해 7자리 인코딩 값을 생성하겠습니다.

     

     

    이렇게 중복허용이 가능한 함수를 설계해봤습니다. 다음으로 Url중복 불가능 한 경우를 살펴보겠습니다.

     

     

    2. 중복이 불가능 한 경우

     

    중복이 불가능 한 경우는 위의 중복 가능한 경우와 달리 사용자의 정보가 같이 전달됩니다.

    그래서 이러한 정보를 이용하여 중복을 체크하게 됩니다. 그럼 순차적으로 살펴보겠습니다.

    Bitly를 참고하니 "naver.com", "NAVER.COM", "Naver.com"은 같은 인코딩 값이 나왔습니다.

    그래서 저는 Url전체를 모두 소문자로 변경하여 인코딩을 시작하였습니다.

    다음으로 OriginUrl을 해싱하고 Base62인코딩을 합니다. 그래서 처음에는 Url이 깨지지 않게 UTF-8로 인코딩을 합니다.

    왜냐하면 운영시스템에 따라서 인식을 달리할 수 있기 때문입니다. 위에서는 Url이 아닌 숫자를 인코딩해서 따로 안해줬습니다.

    Origin : https://naver.com
    UTF-8 : \x68\x74\x74\x70\x73\x3a\x2f\x2f\x6e\x61\x76\x65\x72\x2e\x63\x6f\x6d

     

     

    그 다음 Url을 해싱하고 인코딩을 할려고하는데 문제가 발생했습니다.

    왜냐하면 Url을 해싱한 것이기때문에 16진수 64글자는 똑같이 나올 것입니다. 

    그럼 앞의 10글자를 추출하면 똑같이 중복이 안되겠죠? 그러나 사용자1과 사용자2가 있다고 가정합시다.

    사용자1"naver.com" Short Url은 "bit.ly/ABCDEFG"입니다.

    사용자2"naver.com" Short Url은 "bit.ly/ABCDEFG"입니다.

    왜냐하면 같은 서버에 접근하여 Short Url을 얻어가기 때문입니다.

    사용자의 이름은 다르지만 Url은 같은 것을 볼 수 있습니다. 외부 Get요청이 올 경우 따로 관리해줄 수 없습니다. 

    이것을 어떻게 해결해야 할까요? 저는 중복 체크를 사용해서 해결하였습니다.

    어차피 중복을 체크해야하는데 2번 체크해서 해결하기로해 프로그램 시간을 측정해봤습니다.

            if (urlRepository.existsByname(name)) {
                if (urlRepository.existsByoriginurl(originUrl)) {
                    throw new EntityExistsException(originUrl);
                }
            }

    위의 결과를 보시면 내부적으로 한 번더 체크하는 부분은 영향이 미미한 것으로 보입니다.

    그 이유는 Hibernate에서 제공하는 커넥션 객체를 반납하지 않고 같은 트랜잭션내에서 사용했기 때문이라고 저는 추측합니다.

    (위의 내용은 저의 추측입니다.)

    이렇게 중복된 Short Url을 방지하고 생성하여 개인적으로 관리할 수 있는 Url을 생성해봤습니다.

    이제는 직접 어떻게 구현하였는지 살펴보도록 하겠습니다.

     

    📽️  설계도 및 구현

     

    1. 중복이 가능한 경우

     

    중복 가능한 경우의 큰 로직은 위의 그림과 같습니다.

    앞에서  Post요청으로 OriginUrl이 들어오면 이것이 유효한 Url인지 검사를 합니다.

        public String checkUrl(String originUrl) throws ValidationException {
            int index = originUrl.indexOf("://"); //앞의 http://, https:// ftp://검사
            String changedUrl = "";
            if (index == -1)
                originUrl = "https://" + originUrl;
            if (validateUrl(originUrl) == false) //SCHEME없으면 인증이 안되기 때문에 앞에서 확인해줌.
                throw new ValidationException(originUrl);
            return originUrl;
        }
    
        private boolean validateUrl(String originUrl) {
            UrlValidator urlValidator = new UrlValidator();
            return urlValidator.isValid(originUrl);
        }

     그 다음은 난수를 발생시켜 위의 시퀀스에 맞게 인코딩합니다.

     

     

    2.  중복이 불가능한 경우

    중복이 불가능 한 경우는 OriginUrl을 소문자화 시킨 후 유효성을 검증합니다.

    그리고 UTF-8형식으로 인코딩을 해줍니다. 인코딩을 하고 바로 Convert로직을 실행시켜줍니다.

        private String makeExtract10Char(String originUrl) throws UnsupportedEncodingException, NoSuchAlgorithmException {
            String encodedUtf8 = URLEncoder.encode(originUrl, "UTF-8");
            String convertedBySha512 = sha512Converter.convert512(encodedUtf8);
            String extract10Char = makeFront10Char(originUrl, convertedBySha512);
            return extract10Char;
        }

    이렇게 두 개의 Service로직을 구현해봤습니다.

    코드는 코드 저장소에서 확인하실 수 있습니다.

     

    ❗  프로젝트 회고

     

    이번 서비스를 직접 생각하고 개발해보면서 간단한 동작이고 Base62로 하면 되겠구나라고 생각했습니다.

    그러나 중복 허용과 중복 허용하지 않음에 관해서 순간적으로 멘붕이 왔습니다.

    코드는 손대지 못하고 생각만 3일 정도 해본것 같습니다. 그리고 계속해서 이해가가지 않는 부분을 이해하고 생각하려는 과정이 많았습니다. 그렇게 하다보니 저만의 독특한 Url Shortener가 생성되었습니다. 물론 주변에서 보기에 더 효율적인 방법이 있을 것입니다.

    그러면 이제 그것에 대해서 계속 생각해보고 이해하면서 받아들일 생각입니다. 혹시나 좋은 방법있으면 댓글 달아주시길 바랍니다.

    오랜만에 깊게 생각하고 코드를 짜보았습니다.

    이렇게 생각해보는 습관을 계속 길러가면 좋겠다는 생각이듭니다.

    구현은 누구나 가능하지만 깊게 이해하고 설명할 수 있는 엔지니어가 되야겠다는 다짐을 하게 해준 프로젝트였습니다. 

    반응형

    'Back-end > 나만의 API' 카테고리의 다른 글

    API (4) - Url Shortener + 42Ouath API  (0) 2021.08.29
    API (2) - 42OAuth API  (0) 2021.07.28
    API (1) - Login API  (0) 2021.07.07

    댓글

Designed by Tistory.