(1) - 에서 정리한 내용을 바탕으로 한번 구현을 해보겠다.
https://spongecake.tistory.com/183
[Spring] - URL Shortener 설계 - (1) - 정리
이전 프로젝트에서 초대기능을 구현한다고 URL Shortener를 구현했었는데 급하게 구현한다고 제대로 하지 못한 것 같아서 그것을 보완해보기 위해 한번 처음부터 구현해보는 것을 목표로 한다. 여
spongecake.tistory.com
구현할 내용은 아래와 같다.
1. 입력으로 긴 URL을 받는다.
2. 데이터베이스에 해당 URL이 있는지 검사한다.
3. 데이터베이스에 있다면 해당 URL에 대한 단축 URL을 DB에서 꺼내 반환하면 된다.
4. 데이터베이스에 없을 경우에는 해당 URL는 새로 접수된 것이므로 유일한 ID를 생성한다 ( auto_increment, UUID, 트위터 스노플레이크 등으로 유니크한 ID 생성)
5. Base-62를 통해 , 해당 ID를 단축 URL로 변환
6. ID, 단축 URL, 원본 URL로 DB 레코드를 만든 후 단축 URL을 클라이언트에 전달.
지금은 프로젝트에 바로 사용할 것이 아니기 때문에 기능만 구현하면서 이해하는 식으로 간단하게만 해볼 생각이다.
일단 폴더구조는 아래와 같이 간단하게 구현했다.
1. 입력으로 긴 URL을 받는다.
(1) - 에서 정리한 내용을 바탕으로 서버에서 동작하기 위해 API를 POST, GET으로 각각 하나씩 만들어 준다.
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/data")
public class UrlController {
private final UrlService urlService;
@PostMapping("/shorten")
public ApiResponse<ApiResponse.SuccessBody<String>> short(@RequestBody LongUrlRequest request) {
String shortUrl = urlService.save(request.longURL);
return ApiResponseGenerator.success(shortUrl,HttpStatus.OK, MessageCode.SUCCESS);
}
@GetMapping("/{shortUrl}")
public ResponseEntity<String> move(@PathVariable String shortUrl) {
UrlEntity url = urlService.search(shortUrl);
HttpHeaders headers = new HttpHeaders();
headers.setLocation(URI.create(url.getLongUrl()));
return new ResponseEntity(headers, HttpStatus.MOVED_PERMANENTLY);
}
}
여기서 POST의 역할은 긴 URL을 전달 받아서 해당 URL을 짧은 URL로 바꿔주는 역할이다
GET의 역할은 짧은 URL을 입력했을 때 긴 URL의 원래 사이트로 이동시켜주는 역할이다.
2. 3.데이터베이스 해당 URL이 있는지 검사한다/ 있으면 DB에 있는 ShortUrl 반환
Service/UrlServiceImpl
@Override
public String save(String longUrl) {
//longUrl
Optional<UrlEntity> url = urlRepository.findByLongUrl(longUrl);
if(url.isPresent()){ // url이 DB에 있을 경우
return url.get().getShortUrl();
}else{ // url이 DB에 없을 경우
//... 없을 경우 로직
}
}
urlRepository에서 해당 longUrl에 해당하는 값을 찾아서 값이 있는지 없는지 확인하고 있을 경우 ShortUrl 반환
4.데이터베이스에 없을 경우에는 해당 URL는 새로 접수된 것이므로 유일한 ID를 생성한다.
Service/UrlServiceImpl
@Override
public String save(String longUrl) {
//longUrl
Optional<UrlEntity> url = urlRepository.findByLongUrl(longUrl);
if(url.isPresent()){
return url.get().getShortUrl();
}else{
SnowFlake snowFlake = new SnowFlake(1,1); // 인자가 1,1인 이유는 현재는 간단하게 구현해놔서 분산 데이터 시스템이 아니기 때문에 1,1로 지정
Long id = snowFlake.nextId();
// ... BASE62로 변환
}
}
없을 경우에 snowFlake를 통해 유니크한 ID값을 생성한다.
util/SnowFlake
public class SnowFlake {
private final static long START_STMP = 1480166465631L;
private final static long SEQUENCE_BIT = 12;
private final static long MACHINE_BIT = 5;
private final static long DATACENTER_BIT = 5;
private final static long MAX_DATACENTER_NUM = -1L ^ (-1L << DATACENTER_BIT);
private final static long MAX_MACHINE_NUM = -1L ^ (-1L << MACHINE_BIT);
private final static long MAX_SEQUENCE = -1L ^ (-1L << SEQUENCE_BIT);
private final static long MACHINE_LEFT = SEQUENCE_BIT;
private final static long DATACENTER_LEFT = SEQUENCE_BIT + MACHINE_BIT;
private final static long TIMESTMP_LEFT = DATACENTER_LEFT + DATACENTER_BIT;
private long datacenterId;
private long machineId;
private long sequence = 0L;
private long lastStmp = -1L;
public SnowFlake(long datacenterId, long machineId) {
if (datacenterId > MAX_DATACENTER_NUM || datacenterId < 0) {
throw new IllegalArgumentException("datacenterId can't be greater than MAX_DATACENTER_NUM or less than 0");
}
if (machineId > MAX_MACHINE_NUM || machineId < 0) {
throw new IllegalArgumentException("machineId can't be greater than MAX_MACHINE_NUM or less than 0");
}
this.datacenterId = datacenterId;
this.machineId = machineId;
}
public synchronized long nextId() {
long currStmp = getNewstmp();
if (currStmp < lastStmp) {
throw new RuntimeException("Clock moved backwards. Refusing to generate id");
}
if (currStmp == lastStmp) {
sequence = (sequence + 1) & MAX_SEQUENCE;
if (sequence == 0L) {
currStmp = getNextMill();
}
} else {
sequence = 0L;
}
lastStmp = currStmp;
return (currStmp - START_STMP) << TIMESTMP_LEFT
| datacenterId << DATACENTER_LEFT
| machineId << MACHINE_LEFT
| sequence;
}
private long getNextMill() {
long mill = getNewstmp();
while (mill <= lastStmp) {
mill = getNewstmp();
}
return mill;
}
private long getNewstmp() {
return System.currentTimeMillis();
}
}
https://github.com/beyondfengyu/SnowFlake/blob/master/SnowFlake.java
URL 변환기의 이해를 위해 구현하고 있으므로 스노우 플레이크는 참고했음
5.Base-62를 통해 , 해당 ID를 단축 URL로 변환, 단축 URL을 클라이언트에 전달
스노우 플레이크를 통해서 유니크한 ID를 생성했고 해당 ID로 ShortURL을 만들기 위해서
BASE62를 통해 짧은 문자열을 생성한다.
BASE64 vs BASE62
2가지의 방법이 있는데 BASE62를 사용하는 이유는 BASE64에는 +, /와 같은 특수문자들이 있기때문에 해당 특수문자들을 사용하지 않기 위해서 BASE62를 사용하는게 더 낫다고 생각합니다.
String str = base.encode(id);
UrlEntity entity = new UrlEntity(snowFlake.nextId(),longUrl,str);
urlRepository.save(entity);
return entity.getShortUrl();
util/BaseConversion
@Service
public class BaseConversion {
private static final String allowedString = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
private char[] allowedCharacters = allowedString.toCharArray();
private int base = allowedCharacters.length;
public String encode(long input) {
StringBuilder encodedString = new StringBuilder();
if (input == 0) {
return String.valueOf(allowedCharacters[0]);
}
while (input > 0) {
encodedString.append(allowedCharacters[(int) (input % base)]);
input = input / base;
}
System.out.println("encodedString : " + encodedString);
return encodedString.reverse().toString();
}
}
6.전달된 URL을 통해 긴 URL 접속
짧게 줄여진 URL을 통해 긴 URL을 가져와서 Redirect
@GetMapping("/{shortUrl}")
public ResponseEntity<String> move(@PathVariable String shortUrl) {
UrlEntity url = urlService.search(shortUrl);
HttpHeaders headers = new HttpHeaders();
headers.setLocation(URI.create(url.getLongUrl()));
return new ResponseEntity(headers, HttpStatus.MOVED_PERMANENTLY);
}
LongURL 전달 - shortURL 반환
DB에 저장된 상태
ShortURL을 전달하고 구글로 Redirect된 모습
postman으로 redirect를 했는데 이미지나 글꼴들이 다 깨져서 나오는데 왜 그런지는 아직 찾지 못했다.
찾아서 해결해 볼 생각이다.
간단하게 URL 단축기를 구현해봤는데 생각보다 간단한 것 같으면서도 생각보다 어려운 것 같은 느낌
언젠가 프로젝트를 하게 됐을 때 초대 기능에 이 단축기를 사용해봐도 좋을 것 같다.
하나의 지식이 더 쌓인 느낌.
보면서 좀 부족하다거나 모자라다 싶은 것들은 언제든지 피드백 해주시면 감사히 받겠습니다.
참고
가상 면접 사례로 배우는 대규모 시스템 설계 기초 8장 - https://product.kyobobook.co.kr/detail/S000001033116
가상 면접 사례로 배우는 대규모 시스템 설계 기초 | 알렉스 쉬 - 교보문고
가상 면접 사례로 배우는 대규모 시스템 설계 기초 | 페이스북의 뉴스 피드나 메신저,유튜브, 구글 드라이브 같은 대규모 시스템은 어떻게 설계할까? IT 경력자라도 느닷없이 대규모 시스템을 설
product.kyobobook.co.kr
https://github.com/beyondfengyu/SnowFlake/blob/master/SnowFlake.java
전체코드
https://github.com/wnstn819/URLShortener
wnstn819/URLShortener
Contribute to wnstn819/URLShortener development by creating an account on GitHub.
github.com