[DSC] Node.js + React를 통한 웹플랫폼 제작기 5주차 - Base 62 인코딩

600g (Kim Dong Geun)·2020년 7월 28일
0

5주차 할 것

  • RandomString에 대한 문제점
  • Base62 encoding이란?
  • Url Model 만들기
  • Base62 알고리즘 구현
  • Base62 Controller 구현
  • Random String과의 차이점

4주차까지 해서 bitly 프로젝트를 완성시켰습니다. 기말고사 끝나고 방학 끝나고 한다는게 벌써 7월이 지나가네요.

Random String에 대한 문제점

저번에 저희가 random string을 이용하여 LongUrl을 short Url로 변경시켜봤습니다. random string으로 만들어지는 short url이 원래의 longurl로 1:1 매칭이 되는 간단한 구조 였습니다.
다만, 기존의 random String을 사용하면 발생하는 문제점이 있습니다.

DB에 short url양이 많아질 수록, 중복을 체크하는시간이 많이 소요된다.

Random은 말그대로 임의대로 만들겠다는 뜻입니다. 즉 주사위를 돌렸을 때, 처음 나온 수가 6이라고 했을때 다음 나온 수가 또 6이 아니라는 법이 없다 이말인거죠.

그러면 저희는 앞서 random string을 만들고 그 값이 이미 디비에 저장되어있으면, 디비에 저장된 값이 없을때 까지 random string을 다시 만들었죠.

처음에는 중복된 값이 없기 때문에 어느정도 성능이 보장되겠지만 나중에 가면 어떻게 될까요?
당연히 성능이 떨어지겠죠.

좀 더 쉽게 설명드리겠습니다.
저희는 중복없이 주사위에 나온 수를 종이에 1~6까지 기록하려고 합니다.
몇 번 돌려야 될까라는 문제와 유사합니다.
처음에는 종이에 기록된 수가 없기 때문에 한번만에 종이에 기록할 수 있습니다.
예를들어 6이라는 숫자가 나왔다고 해봅시다.
다음은 1~5라는 수가 나오면 종이에 기록 6이라는 숫자가 나오면 주사위를 다시 던집니다.
점차 종이에 기록되는 수가 많을 수록 주사위를 다시 던질 확률이 많아지겠죠?
즉 종이에 기록되는 수가 많을수록 다음 수를 넣을 성능이 떨어진다 라고 생각하면 될 것 같슴다.

아무튼 설명이 길어져버렸는데 이걸 해결하기 위해서 Base62라는 Encoding 방식이 존재합니다.

Base62 Encoding이란?

흔히들 Base64 인코딩이라고 많이 들어보셨을 겁니다. 구현하는 방식은 똑같습니다. 어떠한 값을 64진수로 표현하는 것을 base64, 어떠한 값을 62진수로 나타내는 것을 Base62 인코딩이라고합니다.

Base 62는 흔희들 Url Encoding에 적합하게 사용되고 있습니다 (특수문자가없기 때문)

위에 설명드렸듯이 62진수로 나타내는 표현은 알파벳 26자 * 2 (소문자,대문자) + 숫자 10 하면 62라는 수가 나오죠.

즉 임의의 String이 있으면 이 값을 => 알파벳과 숫자로만 표현한 값으로 바꾸겠다를 Base62 Encoding이라 보시면 됩니다.

기존의 random string -> long url 매칭 방식에서
id -> long url을 사용할 것입니다.

base62 알고리즘 구현

자, 그럼 코드를 구현 해보도록 하겠습니다.

  • service/UrlService2.js
const UrlModel = require("../model/url2");

class UrlService2 {
  constructor() {
    this.codec = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789".split('');
  }
  
  encode(param){
    if(param === 0 ) return 0;
    let result = [];
    while(param > 0){
      result = [this.codec[param % 62],...result];
      param = Math.floor(param/62);
    }
    return result.join('');
  }

  decode(str){
    return str.split('')
      .reverse()
      .reduce((prev,curr,i) => prev + this.codec.indexOf(curr) * (62** i),0);
  }

  async createUrl(longUrl) {
    const newUrl = new UrlModel({
      longUrl : longUrl
    });
    await newUrl.save();
    return newUrl;
  }

  async findOneByBase64(encodedStr) {
    const id = this.decode(encodedStr);
    return await UrlModel.findOne({_id : id});
  }
}

기존의 url 서비스1을 그대로 복사해서 2로 만들고 다음과 같이 만들겠습니다.
사실 구현을 좀 더 깔끔하게 하려면 Base62와 관련된 부분은 따로 클래스(Base62Helper등과 같이)를 만들어 두고 urlSevice 를 Interface로 두고 UrlServiceImpl, UrlServiceImpl2등으로 만들어 두는게 더 좋지만 그냥 넘어갑시다 ㅎㅎ.

각각의 encode 하는 이론적인 부분은 base 62 관련 글을 읽어보시면 많이 있으니 참고하시길 바라겠습니다. (선수 과제로도 알아오라고 했으니!)

idlong url
1http://www.naver.com
2http://velog.com/@ehdrms2034

위 처럼 id는 auto Increment로 설정을 해두고 long Url을 매칭할겁니다.
그리고 Controller에서 받는 것은 id값을 Base 62로 Encoding 한 값이 되겠죠.

악,, urlModel에 대한 언급을 안했네요. urlModel에 대한 부분은 아래에 있습니다. 이 부분은 지금은 그냥 넘어 가주세용!

Mongoose - Sequence 설치

Mongo DB는 기본적으로 _id를 생성할 때, Auto Increment로 생성하지 않습니다.
Object에 대한 생성 규칙이 존재하고 그 규칙은 중복값을 만들어 내지 않습니다.
https://blog.seulgi.kim/2014/09/mongodb-objectid.html <= 참고.

그렇지만 Base 62 Encoding을 사용하기에는 Mongo DB에서 지원하는 id 생성규칙이 적합하지 않습니다.
그래서 AutoIncrement 처럼 동작하게 해주는 Mongoose-Sequence Plugin 을 설치할 것입니다.

설치 방법은 간단합니다.

npm install mongoose-sequence 혹은 yarn add mongoose-sequence

설치가 완료 될 겁니다.

그럼 위에서 넘어가버린 Url Model 을 만들어봅시다.

  • model/url2.js
const mongoose = require("mongoose");
const AutoIncrementFactory = require("mongoose-sequence");

const { Schema } = mongoose;

const AutoIncrement = AutoIncrementFactory(mongoose.connection);

const url2 = new Schema(
  {
    _id: {
      type: Number,
    },

    longUrl: {
      type: String,
      required: true,
    },
  },
  { _id: false }
);

url2.plugin(AutoIncrement);
module.exports = mongoose.model("url2", url2);

기존의 _id를 생성하는 default 규칙을 해제하고 mongoose-sequence를 url2 모델에 적용시킵니다.

그리고 이제 뭐 생성명령을 몇번 내려보면 ,

원하는대로 값이 잘 나오는 것을 볼 수 있네요. 이제 Controller에서 longUrl을 받으면 사용자에게 Base 62로 인코딩 값을 출력하고, Base62로 인코딩된 값이 들어오면 id값으로 바꿔줍시다.

Controller 구현하기

  • routes/url2.js
const express = require('express');
const router = express.Router();

const UrlService = require('../service/UrlService2');

const urlService = new UrlService();

function generateBody(response, message, data) {
  return { response, message, data };
}

//Create Short Url
router.post('/',async(req,res)=>{
  const {longUrl} = req.body;
  try {
    if (longUrl === undefined)
      throw new Error("request body값이 조회되지 않음");
    const newUrl = await urlService.createUrl(longUrl);

    return res.json(
      generateBody("success", "short Url 요청 성공", {
        longUrl: newUrl.longUrl,
        shortUrl: urlService.encode(newUrl._id),
      })
    );
  } catch (error) {
    return res.json(
      generateBody("error", "shortUrl을 요청 실패", error.message)
    );
  }
});

//ShortUrl 들어오면 LongUrl
router.get("/:shortUrl", async (req, res) => {
  const { shortUrl } = req.params;
  try {
    if (shortUrl === undefined)
      throw new Error("request path 값이 존재하지 않습니다.");
    const data = await urlService.findOneByBase64(shortUrl);

    if (data === null || data === undefined)
      throw new Error("short Url(" + shortUrl + ")값이 정확하지 않습니다.");

    return res.json(
      generateBody(
        "success",
        "url을 성공적으로 들고왔습니다.",
        data.longUrl
      )
    );
  } catch (error) {
    return res.json(
      generateBody("error", "요청에 실패했습니다.", error.message)
    );
  }
});

module.exports = router;

기존의 routes/index.js를 그대로 복사붙여넣기 하고, 필요한 부분만을 변경했습니다.
현재는 보여주기 위해서 이렇게 처리했지만 위~~에 언급한것처럼 urlSevice interface를 만들고 각각의 service를 구현한다면 (혹은 팩토리 메소드 패턴을 이용하면) 좀 더 유지보수하기 편한 환경이 되겠지요!

그리고 app.js에 다음 두줄을 넣어줍시다.

...
const url2Router = require('./routes/url2');
... //비슷한 놈들 사이에 넣어주시면 됨다 위에도 아래도
... 
app.use('/url2',url2Router);

그럼 테스트 결과를 한번 보겠습니다.

  • long url -> short url

  • short url -> long url

네 결과값이 모두 같기 때문에, 프론트엔드에서 Axios 부분의 url만 변경해주면 똑같이 프론트에서도 동작할 것으로 보이기 때문에 생략하도록 하겠습니다!.

Random String과의 차이점

음 마지막으로 한 번 정리를 해주고 싶습니다.
서버는 아무래도 사용자의 요청을 빠르게 응답해주는것이 좋습니다.

하지만 random string으로 만든다면, 아무래도 성능상 이슈가 있겠죠.
비단 그것 뿐만의 문제가 아니라 base62는 id(index)값을 이용하여 long Url에 대해 접근을 하기 때문에 속도 또한 월등히 빠르다고 알고 있습니다.

RDBMS, mongo 둘다 index 조회를 지원하고있으며 mongo가 index 조회시 속도가 꽤 빠른 것으로 알고있어서 이번프로젝트에 몽고 디비를 사용했습니다.

정리하자면~

Random String은 구리다~ Base62가 보편화된 방법이다~

라고 말해드리고 싶네요^^

또한 두가지 방법론을 제시한 이유는, 같은 것을 만들더라도 더 효율적이고 좋은 것을 만드는 개발자가 더 가치있겠죠?

좋은 개발자가 됩시다 여러분ㅎㅎ 👍

-끝-

profile
수동적인 과신과 행운이 아닌, 능동적인 노력과 치열함

1개의 댓글

comment-user-thumbnail
2020년 10월 14일

좋은 글 잘 봤습니다.
깔끔하게 잘 정리해주셨네요^^

답글 달기