[JAVA] HOW TO MAKE A SHORTEN URL
[기능 설계]
1) Base62로 인코딩/디코딩하는 기능을 가진 Base62Util을 만든다.
2) 원본 URL과 짧은 URL 등의 정보를 관리하는 테이블을 생성한다.
- URL_NO (AUTO INCREMENT, PK) // Base62 인코딩/디코딩할 값
- SHORT_URL // URL_NO를 인코딩한 값이 shortUrl이 됨
- ORI_URL
- REG_USER
- REG_DT
//짧은 URL 생성 후 저장
3) 입력받은 원본 URL을 (DB에 동일 URL 존재 유무 확인 후) 없으면 테이블에 INSERT한다.
4) INSERT시에 AUTO INCREMENT로 생성된 URL_NO를 조회하여 Base62Util로 인코딩한다.
5) 인코딩된 URL_NO를 SHORT_URL 컬럼에 UPDATE한다.
//짧은 URL 인입 시, redirect
6) 짧은 URL을 @PathVariable로 전달받아, Base62Util로 디코딩한다.
7) 디코딩한 짧은 URL은 URL_NO 이므로, 이를 이용하여 원본 URL을 조회한다.
8) HttpStatus.MOVED_PERMANENTLY(301 응답)를 이용하여 원본 URL로 redirect한다.
1) Base62로 인코딩/디코딩하는 기능을 가진 Base62Util을 만든다.
- Base62는 Base64에서 +, / 를 제외한 것으로, 결과물인 짧은 URL에 +, /가 들어가지 않도록 하기 위해 사용한다.
public class Base62Util {
private static final char[] BASE62 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789".toCharArray();
private static int base = BASE62.length;
// int형 param을 encoding 한다.
public static String encode(int param) {
StringBuilder sb = new StringBuilder();
while(param > 0) {
int i = param % base;
sb.append(BASE62[i]);
param /= base;
}
return sb.toString();
}
// long형 param을 encoding 한다.
public static String encodeLong(long param) {
StringBuilder sb = new StringBuilder();
while(param > 0) {
int i = (int) (param % base);
sb.append(BASE62[i]);
param /= base;
}
return sb.toString();
}
// String param을 받아, int형으로 decoding 한다.
public static int decode(String param) {
int result = 0;
int power = 1;
for(int i=0; i<param.length(); i++) {
int digit = new String(BASE62).indexOf(param.charAt(i));
result += digit * power;
power *= base;
}
return result;
}
// String param을 받아, long형으로 decoding 한다.
public static long decodeLong(String param) {
long result = 0;
long power = 1;
for(int i=0; i<param.length(); i++) {
int digit = new String(BASE62).indexOf(param.charAt(i));
result += digit * power;
power *= base;
}
return result;
}
}
2) 원본 URL과 짧은 URL 등의 정보를 관리하는 테이블을 생성한다.
- PK값은 UUID, timestamp + Math.random()값, AUTO INCREMENT 등 결정하기 나름인데,
UUID는 자릿수가 너무 길어 적합하지 않고, 콕 집어 AUTO INCREMENT를 사용했다. 사실 귀찮은 건 안비밀
- DB: Mysql
- 테이블명: TBL_URL_M
컬럼명 | # | Data Type | Not Null | Auto Increment | Key | Default | Comment |
URL_NO | 1 | INT(10) | [v] | [v] | PRI | PK | |
SHORT_URL | 2 | TEXT | [ ] | [ ] | NULL | 짧은URL | |
ORI_URL | 3 | TEXT | [v] | [ ] | 원본URL | ||
REG_USER | 4 | VARCHAR(10) | [ ] | [ ] | NULL | 등록자 | |
REG_DT | 5 | DATETIME | [ ] | [ ] | NULL | 등록일시 |
3) 입력받은 원본 URL을 (DB에 동일 URL 존재 유무 확인 후) 없으면 테이블에 INSERT한다.
- 참고로, Mysql 기준이다.
/** 원본 URL로 조회하여 중복 정보 확인 */
<select id="selectUrlNoByOriUrl" parameterType="map" resultType="CamelMap">
SELECT
URL_NO
,SHORT_URL
,ORI_URL
,REG_USER
,REG_DT
FROM TBL_URL_M
WHERE 1=1
AND ORI_URL = #{oriUrl}
</select>
/** 원본 URL 정보 INSERT */
/** AUTO INCREMENT이므로, URL_NO는 따로 넣어주지 않는다 */
<insert id="insertOriUrl" parameterType="map">
INSERT INTO TBL_URL_M
(
SHORT_URL
,ORI_URL
,REG_USER
,REG_DT
)
VALUES
(
#{shortUrl}
,#{oriUrl}
,#{regUser}
,NOW()
)
</insert>
4) INSERT시에 AUTO INCREMENT로 생성된 URL_NO를 조회하여 Base62Util로 인코딩한다.
- ServiceImpl.java (extract)
import curryKing.admin.common.util.Base62Util;
@Transactional
@Override
public Map<String, Object> createShortUrlService(Map<String, Object> paramMap) throws Exception {
Map<String, Object> resultMap = new HashMap<String, Object>();
String baseUrl = properties.getProperty("base.url");
String prefix = "/move/";
try {
resultMap = mainMapper.selectUrlNoByOriUrl(paramMap);
//중복 url인 경우,
if(resultMap != null) {
resultMap.put("result", "FAIL");
resultMap.put("msg", "이미 존재하는 URL입니다.");
resultMap.put("resultUrl", baseUrl + resultMap.get("shortUrl"));
//신규 url의 경우,
} else {
mainMapper.insertOriUrl(paramMap); // 신규 insert
resultMap = mainMapper.selectUrlNoByOriUrl(paramMap); //AUTO INCREMENT된 URL_NO 조회
String shortUrl = Base62Util.encode((int) resultMap.get("urlNo")); // URL_NO 인코딩
resultMap.put("shortUrl", prefix + shortUrl);
resultMap.put("resultUrl", baseUrl + prefix + shortUrl);
mainMapper.updateShortUrl(resultMap); //인코딩된 URL_NO를 SHORT_URL컬럼에 UPDATE
resultMap.put("result", "SUCCESS");
}
} catch(Exception e) {
e.printStackTrace();
}
return resultMap;
}
5) 인코딩된 URL_NO를 SHORT_URL 컬럼에 UPDATE한다.
- 짧은 URL생성하여 DB저장까지 완료
<update id="updateShortUrl" parameterType="map">
UPDATE TBL_URL_M
SET SHORT_URL = #{shortUrl}
WHERE URL_NO = #{urlNo}
</update>
6) 짧은 URL을 @PathVariable로 전달받아, Base62Util로 디코딩한다.
- Controller.java (extract)
- @PathVariable은 @RequestMapping 내의 {shortenUrl} 부분을 동적으로 전달받을 수 있는 기능을 제공한다.
- @RequestMapping {shortenUrl} 과 @PathVariable String shortenUrl 변수명은 반드시 동일해야한다.
@ResponseBody
@RequestMapping(value = "/move/{shortenUrl}")
public ResponseEntity<String> redirectShortenUrl(@PathVariable String shortenUrl) throws Exception {
Map<String, Object> paramMap = new HashMap<>();
paramMap.put("shortUrl", shortenUrl);
//짧은URL 정보로 조회하여 원본URL 찾기
Map<String, Object> resultMap = mainService.selectOriUrlService(paramMap);
//redirect하기
HttpHeaders headers = new HttpHeaders();
headers.setLocation(URI.create(resultMap.get("oriUrl").toString()));
return new ResponseEntity<String>(headers, HttpStatus.MOVED_PERMANENTLY);
}
7) 디코딩한 짧은 URL은 URL_NO이므로, 이를 이용하여 원본 URL을 조회한다.
import curryKing.admin.common.util.Base62Util;
@Override
public Map<String, Object> selectOriUrlService(Map<String, Object> paramMap) throws Exception {
Map<String, Object> resultMap = new HashMap<String, Object>();
try {
if(paramMap.get("shortUrl") != null) {
//클라이언트로부터 요청받은 shortUrl 디코딩
paramMap.put("urlNo", Base62Util.decode(paramMap.get("shortUrl").toString()));
//원본URL 조회
resultMap = mainMapper.selectOriUrlByUrlNo(paramMap);
if(resultMap != null && resultMap.size() > 0) {
resultMap.put("result", "SUCCESS");
} else {
resultMap.put("result", "FAIL");
resultMap.put("msg", "원본 URL이 존재하지 않습니다.");
}
}
} catch(Exception e) {
e.printStackTrace();
}
return resultMap;
}
8) HttpStatus.MOVED_PERMANENTLY(301 응답)를 이용하여 원본 URL로 redirect한다.
- 6) Controller 참고
[결과물]
- 결과 URL 예시: https://curryking.co.kr/move/B (실제 URL 아님)
- 테이블명: TBL_URL_M
URL_NO | SHORT_URL | ORI_URL | REG_USER | REG_DT |
1 | /move/B | https://www.google.com/search?q=nestat+linux& | curryMaster | 2024-11-29 16:10:32.000 |
[추가 보완할 점]
- AUTO INCREMENT를 사용하였으므로, 결과 URL의 길이가 일정하지 않을 수 있다.
- 짧은 URL 데이터가 쌓이다보면 나중엔 전혀 사용하지 않는 데이터들이 점점 남겨지게 되므로,
배치를 통한 주기적 데이터 정리가 필요할 수 있다.
- VALID_DT 같은 컬럼을 두어, 특정 일자가 지나면 사용하지 않도록 할 수도 있다.
- 지속적인 관리가 필요하다면 DEL_YN 같은 컬럼을 두어 DELETE 효과를 낼 수도 있다.
'[개발 공부] > [자바 JAVA]' 카테고리의 다른 글
[JAVA - Spring Boot] 스프링부트 사용하기(1) (0) | 2022.09.24 |
---|---|
[JAVA] 다차원 배열 (Java Array) / 배열의 복사 (0) | 2022.06.14 |
[JAVA] 자바 반복문 정리 - for / while / do while (0) | 2022.06.09 |
[JAVA] 자바 변수의 종류(기본형) (0) | 2022.06.09 |