URL Shortener - 단축 URL 무결성 체크 및 리팩토링
기존 URL Shortener의 문제점
무결성 문제
변환도 잘되고, 리디렉션도 잘 이뤄진다. 그럼 어떤 점이 문제일까. 저번 포스팅에서 언급했듯이 단축 URL을 아무리 UUID와 Base62로 랜덤하게 생성한다고 해도, 중복이 발생할 확률은 0이 아니다. 특히 앞 7자리만 사용하기 때문에 더 그렇다.
리디렉션 문제
그리고 리디렉션 로직에서도 문제가 있다.
const matchedPath = await client.models.Shortener.list({ filter: { transferUrl: { eq: id } }, });
미들웨어에서 이렇게 원본 url을 찾는 과정을 거친다. 뭔가 filter를 사용하면 filter에 해당하는 것만 리스트로 쭉 나올것 같지만 그렇지가 않다. amplify에서 일정 갯수 이상의 데이터가 존재하면 자동적으로 페이지네이션 처리가 된다. 지금 방식으로 진행하면 원본 url이 존재해도 첫페이지에 데이터가 없으면 404페이지로 리디렉션된다.
하나씩 문제를 해결해보겠다.
리디렉션 문제 해결
안타깝게도 amplify gen2에서는 특정 조건에 해당하는 한개의 데이터를 얻을수가 없다. 물론 get이라는 메서드는 있지만 id에 한정해서 get이 가능하다. 그렇다면 당장 해결 가능한 방법은 list사용시 반환되는 nextToken을 사용해서 재귀함수를 작성해주는 것이다.
const arr: Shortener[] = []; // 데이터 저장 배열 async function getOriginUrl(nextToken?: string): Promise<void> { const res = await client.models.Shortener.list({ nextToken, filter: { transferUrl: { eq: id } }, }); arr.push(...res.data); // 데이터 병합 if (res.nextToken) { // nextToken이 있을 경우 재귀 호출 await getOriginUrl(res.nextToken); } } await getOriginUrl();
이렇게 하면 모든 리스트를 돌며 필터에 해당하는 데이터를 배열에 추가한다. 이렇게만 해도 구현이 가능하다. 하지만 재귀함수를 통해서 데이터를 찾는 방법은 비효율적이라는 것을 알 것이다.
다른 방식으로 해결하기 - amplify get
그래서 우리가 채택한 방식은 id값으로 단축한 문자열을 넣어주는 것이다. 이렇게 데이터를 생성한다면 단축 URL과 id가 같은 값이 될것이고, amplify에서 get을 통해 한개의 데이터만 얻을수가 있다.
현재는 id값이 UUID형식으로 자동 생성된다. 물론 Id값을 우리가 직접 삽입하는 방식은 자동 생성보다는 불안하다. 하지만 단축 URL의 무결성을 보장해준다면 큰 문제가 없을 것으로 보인다.
const createData = await client.models.Shortener.create({ id: shortUrl, originUrl: newUrl, transferUrl: shortUrl, groupId: newGroupId ?? "none", });
그래서 데이터를 생성할때 기존에는 id값이 생략되었지만 지금은 우리가 수동으로 넣어주면 된다.
const { data } = await client.models.Shortener.get({ id });
그러면 이제 amplify의 get을 통해 한개의 데이터만 찾을 수 있다.
무결성 검사
const uuid = parseInt(uuidv4().replace(/-/g, ""), 16); const uuidToBase62 = toBase62(uuid); const shortUrl = uuidToBase62.toString().slice(0, 7); const createData = await client.models.Shortener.create({ originUrl: url, transferUrl: shortUrl, groupId: groupId ?? "none", });
기존 로직이다. Base62문자열 생성후 별도의 검사 없이 바로 데이터를 생성한다. 이 중간에 검사 로직을 넣어줄 것이다.
const changeUUIDtoBase62 = () => { const uuid = parseInt(uuidv4().replace(/-/g, ""), 16); const uuidToBase62 = toBase62(uuid); const shortUrl = uuidToBase62.toString().slice(0, 7); return shortUrl; };
우선 단축 URL을 계속 생성할수도 있기 때문에 별도의 함수로 분리해줬다.
const generateUniqueShortUrl = async ( newUrl: string, newGroupId: string | null, ) => { async function checkSameId(shortUrl: string): Promise<boolean> { const { data } = await client.models.Shortener.get({ id: shortUrl }); return !!data; } let isDuplicate = true; let shortUrl = ""; while (isDuplicate) { if (customUrl) { shortUrl = customUrl; } else { shortUrl = changeUUIDtoBase62(); } isDuplicate = await checkSameId(shortUrl); // 중복 여부 확인 } // 중복되지 않는 shortUrl 생성 완료, 데이터 생성 const createData = await client.models.Shortener.create({ id: shortUrl, originUrl: newUrl, transferUrl: shortUrl, groupId: newGroupId ?? "none", }); return createData; // 최종 생성된 데이터 반환 };
무결한 id를 만드는 방법은 간단하다. 해당 id에 해당하는 데이터가 있는지 계속해서 확인하는 방법이다. 우리는 id를 직접 삽입해주고 있고, id를 사용해서 리디렉션할 예정이기 때문에 이 과정이 더욱 필수적이다. 그래서 while문을 통해 isDuplicate가 fasle가 될때까지 반복을 시켜준다.
물론 UUID v4자체도 완전 랜덤 값이고, 그 값을 변환한 Base62값도 겹치긴 쉽지 않을 것이다. 하지만 저번에도 예상치 못한 부분에서 중복된 URL값이 생성된 경험이 있기 때문에 방지를 해준 것이다.
※ 추가적인 문제
내가 써본 URL Shortener는 URL 한개씩만 변환을 지원해줬다. 물론 유료결제를 한다면 다를지 모르지만 복수로 변환하는 기능은 잘 본적이 없다. 하지만 우리는 이 기능을 추가해줬다.
const transformedUrls = await Promise.all( urls.map(async (url: string) => { const result = await checkUrl(url, groupId, false); return { id: result.id, original: url, shortened: result.transferUrl, createdAt: new Date().toLocaleDateString(), group: groupList.find((item) => item.id === groupId)?.name, }; }), );
우리가 사용하는 API를 확인해보면 map을 통해서 여러개의 요청을 동시에 처리하고 있다. 물론 Promise.all비동기 실행을 하고 있지만 요청은 완료가 되어도 DB에 데이터가 추가되는 시간은 지체가 될수 있다.
생성 로직은 원본 URL이 존재하는 경우에 새로운 단축 URL을 생성하지 않고, 이미 생성된 URL을 리턴한다. 하지만 DB 데이터 생성 속도를 따라가지 못하고, 동일한 URL임에도 새로운 단축 URL 데이터가 생성된다.
하지만 이 문제에 대해서 크게 신경쓰지 않기로 했다. 왜냐하면 이 문제는 동일한 url을 동시에 변환할때에만 발생하기 때문이다.
https://www.naver.com/ https://www.naver.com/ https://www.naver.com/ https://www.naver.com/
이런 형식으로 textArea에 입력하는 상황인데 일부로 하지 않는 이상 이런 문제는 발생하지 않는다. 또한 URL Shortener의 문제는 본질은 단축 URL로 접근했을때 원본 URL로 리디렉션 시켜주는 용도이다. 그렇기 때문에 어떤 경로로 접근하더라도 원본으로는 리디렉션되기 때문에 문제가 없다.
잠재적인 문제로는 데이터가 방대하게 많아지는 경우인데 이 URL Shortener는 Back Office 서비스이기 때문에 사용되는 URL이 한정적이다. 그래서 이 문제는 넘어가기로 했다.
마무리
URL Shortener의 완성도가 조금더 올라간 것 같다. 이제 요구사항에는 거의 부합한 기능이 만들어 진것 같다. 리디렉션 속도도 빠르고 단축 URL로 접근해도 utm 데이터가 유실되지 않기 때문이다. 물론 기존의 bitly를 완전히 대체할 수 있는가에 대해서는 확신이 안선다. 왜냐하면 개발 기간이 너무 짧기도 했고 QA도 꼼꼼하게 이뤄진 것은 아니기 때문이다. 그렇기에 사용하면서 피드백이 발생하지 않을까 싶다.
다음은 추가 요구사항이었던 구글 스프레드 시트 확장 프로그램 구현이다. 사실 이런 URL은 스프레드 시트에서 따로 관리되고 있기 때문에 변환 자체는 스프레드 시트에서 실행하는 것이 더 빠른 동작 방식일 것이다. 그래서 다음은 구글 스프레드 시트의 Apps Script를 활용해서 만들어 보겠다.
개의 댓글
1
기존 URL Shortener의 문제점
무결성 문제
리디렉션 문제
리디렉션 문제 해결
다른 방식으로 해결하기 - amplify get
무결성 검사
※ 추가적인 문제
마무리