🫠가상 면접 사례로 배우는 대규모 시스템 설계 기초라는 책을 읽으면서 단축 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를 이용하였는데 이런 로직을 수행하고 있었는지 몰랐는데 많은 것을 알게 되었던 것 같습니다.