[개발 공부]/[자바 JAVA]

[Java] 짧은 URL 생성 구현하기

wild keyboardist 2024. 11. 29. 17:12

[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_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 효과를 낼 수도 있다.