🫠가상 면접 사례로 배우는 대규모 시스템 설계 기초라는 책을 읽으면서 단축 URL을 어떻게 설계를 해야 하는지를 공부하였습니다.
그래서 이번 기회에 직접 해당 로직을 구현해보자는 마음을 먹고 진행하게 되었습니다.
spring boot
,MySQL
,redis
,docker
-> 도커는 제가 현재 sql이 작동을 안해서 docker를 이용하였습니다.
🧐단축 url이란 예를 들어서 https://github.com/GreenTea9227/shortUrl 라는 url이 들어오게 되면 해당 url을 줄여서 https://localhost:8080/abcd 와 같은 형태로 줄여주는 것을 말합니다.
설계는 간단합니다.
사용자 -> 서버 -> 캐시(Redis) -> db(MySQL)의 과정을 거쳐서 Long Url을 shortUrl로 변환하여 저장할 것입니다.
아래와 같은 요청 과정을 거칩니다.
🤗저는 아래와 같은 Entity를 생성하였습니다.
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Entity
public class ShortUrl {
@Id
private Long id;
private String shortedUrl;
private String originalUrl;
}
id
에는Long
으로 넣을 것인데 저는 직접id
를 생성하여 넣을 것이기에@GeneratedValue
는 넣지 않았습니다.
그리고 shortedUrl에는 단축 url을 넣고 originalUrl에는 Long url을 넣을 것입니다.
public interface UrlRepository extends JpaRepository<ShortUrl,String> {
Optional<ShortUrl> findByOriginalUrl(String original);
Optional<ShortUrl> findByShortedUrl(String shortUrl);
}
🤔
repository
입니다. short, original을 찾을 수 있게 메소드를 생성해놓았습니다.
@Component
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class MyLocation {
public static String location;
@Value("${custom.location}")
public void setLocation(String location) {
this.location = location;
}
}
location의 경우 나중에 사용하기 위해 미리 만들어 두었습니다.
@RequiredArgsConstructor
@RequestMapping
@Controller
public class UrlController {
private final ShortService service;
@ResponseBody
@PostMapping("/short/request")
public ResponseEntity<String> requestShortUrl(String originalUrl) {
String shortUrl = service.makeShortUrl(originalUrl);
return new ResponseEntity<>(shortUrl, HttpStatus.OK);
}
@GetMapping("/{path}")
public String redirectToOriginal(@PathVariable String path) {
String originalUrl = service.requestShortUrl(path);
return "redirect:%s".formatted(originalUrl);
}
}
>
🫡/short/request로 Long url을 보내주면 해당 url을 단축시켜서 반환 하도록 하였습니다.
/{path}의 경우 나중에 단축 url로 요청이 오게 되면 해당 original url로 보내줄 예정입니다.
결국 단축 url의 과정은 아래와 같습니다.
- short url -> db에서 해당 값이 있는지 찾기
db에는 현재 id, shortUrl, longUrl이 저장되어 있는 상태이기에 shortUrl를 통해 값을 찾는다면 longUrl도 찾을 수 있음- 없다면 에러 있다면 해당 값을 컨트롤러로 반환
- 컨트롤러는 해당 값을 이용해서 redirect 처리
😭가장 중요한 부분입니다. 먼저 코드를 보고 설명하겠습니다.
@Slf4j
@RequiredArgsConstructor
@Transactional
@Service
public class ShortService {
private final RedisTemplate<String,Object> redisTemplate;
private final UrlRepository urlRepository;
private final EncodingService base62Utils;
public String makeShortUrl(String originalUrl) {
...
}
public String requestShortUrl(String shortUrl) {
...
}
}
public String makeShortUrl(String originalUrl) {
Optional<ShortUrl> byOriginalUrl = urlRepository.findByOriginalUrl(originalUrl);
if (byOriginalUrl.isPresent()) {
return byOriginalUrl.get().getShortedUrl();
}
Long shaEncode = base62Utils.shaEncode(originalUrl);
String base62Encode = base62Utils.base62Encode(shaEncode);
urlRepository.save(new ShortUrl(shaEncode,base62Encode, originalUrl));
redisTemplate.opsForValue().set(base62Encode,originalUrl,3, TimeUnit.DAYS);
return MyLocation.location+"/"+base62Encode;
}
makeShortUrl()
에 original을 넣어주면 해당 값을 먼저 db에서 찾습니다. db에 있다는 것은 이미 한번 단축을 했던 값이므로 단축 시킬 필요 없이 해당 값을 넘겨주면 됩니다. 없다면 다음 단계로 갑니다.- 해당 값을
sha-256
으로 인코딩 합니다. 인코딩 과정은 다음 단계에서 살펴보겠습니다. 여기서는 해당 값을sha-256
으로 인코딩 한 후에 해당 값을16진수
long으로 변환 해주었다고 이해하시면 됩니다.(id로 이용)- 위에서 인코딩한 값을
base62
로 인코딩 합니다. 이 값이 최종 shortUrl입니다. 해당 과정도 다음 단계에서 살펴보겠습니다.- 해당 값을
redis
에 저장합니다. 이를 통해 후에 요청이 온다면 빠르게 값을 찾을 수 있습니다.- 결과 값을 반환합니다. http://localhost:8080/shorturl의 형태로 반환이 됩니다.
public String requestShortUrl(String shortUrl) {
if (redisTemplate.hasKey(shortUrl)) {
return (String) redisTemplate.opsForValue().get(shortUrl);
}
Optional<ShortUrl> byShortedUrl = urlRepository.findByShortedUrl(shortUrl);
if (byShortedUrl.isPresent()) {
ShortUrl shortResult = byShortedUrl.get();
redisTemplate.opsForValue().set(shortResult.getShortedUrl(),shortResult.getOriginalUrl(),3,TimeUnit.DAYS);
return byShortedUrl.get().getOriginalUrl();
}
throw new NoSuchUrlException();
}
- 요청된 shortUrl의 값이
redis
에 있는지 확인 합니다. 키가 존재할 경우 해당 값을 반환해줍니다. 없다면 다음 단계를 진행합니다.- db에서 shortUrl을 찾습니다. 있다면 해당 객체에서 originalUrl을 반환하고
redis
에 저장합니다. 없다면 에러를 냅니다.
위에서는 shortUrl을 통해 찾고있지만
base62
를 통해 디코딩하여서 찾아도 됩니다.(pk
이용)🧐위 서비스를 통해 original <-> shortUrl을 진행하였습니다. 이번에는 인코딩 과정에 대해서 알아보겠습니다.
🧐인코딩을 하는 여러이유가 있습니다.
sha-256
으로 인코딩 하고나서 16진수로 변경한 이유는 중복되지 않은 아이디 값을 만들기 위해서입니다.(해시 충돌 제거)base 62
로 인코딩 한 이유는 위 id값을 그대로 사용할 경우 생각보다 긴 값이 나오게 되는데 이를base62
로 인코딩 함으로써 길이를 줄일 수 있기 때문입니다.아래는 해당 코드입니다.
@Service
public class EncodingService {
public Long shaEncode(String originalUrl) {
StringBuilder hexString = new StringBuilder();
try {
MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
byte[] digest = messageDigest.digest(originalUrl.getBytes());
for (byte hashByte : digest) {
String hex = Integer.toHexString(0xff & hashByte);
hexString.append(hex);
}
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return Long.parseLong(hexString.substring(0, 16), 16);
}
public String base62Encode(Long decimalValue) {
String base = Base62Character.BASE.getBASE62_CHARS();
StringBuilder result = new StringBuilder();
while (decimalValue > 0) {
int index = (int) (decimalValue % 62);
char base62Char = base.charAt(index);
result.insert(0, base62Char);
decimalValue /= 62;
}
return result.toString();
}
}
MessageDigest
를 이용하여SHA-256
으로 인코딩하였습니다.- Base62Character의 값은 "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"와 같습니다. 이를 enum으로 만든 것입니다.
🫡이제 요청을 해보겠습니다.
postman으로 요청한 결과 아래와 같은 결과가 나왔습니다.
1. 원하는 주소를 단축 url로 변경 시도
요청 url : http://localhost:8080/short/request
요청 값 : https://github.com/GreenTea9227/shortUrl
결과 : http://localhost:8080/iihNbhpCO1J
2. shortUrl요청
요청 : http://localhost:8080/iihNbhpCO1J
결과 : 해당 주소로 이동
🥳위 과정을 통해 단축 url을 만들었습니다. 기존에는 네이버에서 제공하는 api를 이용하였는데 이런 로직을 수행하고 있었는지 몰랐는데 많은 것을 알게 되었던 것 같습니다.