diff --git a/src/main/java/org/service/urlshortener/cache/Cache.java b/src/main/java/org/service/urlshortener/cache/Cache.java new file mode 100644 index 0000000..0d9c751 --- /dev/null +++ b/src/main/java/org/service/urlshortener/cache/Cache.java @@ -0,0 +1,16 @@ +package org.service.urlshortener.cache; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.Duration; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class Cache { + private String key; + private Class type; + private Duration duration; +} diff --git a/src/main/java/org/service/urlshortener/cache/CacheFactory.java b/src/main/java/org/service/urlshortener/cache/CacheFactory.java new file mode 100644 index 0000000..b951ad2 --- /dev/null +++ b/src/main/java/org/service/urlshortener/cache/CacheFactory.java @@ -0,0 +1,15 @@ +package org.service.urlshortener.cache; + +import org.service.urlshortener.shortener.dto.ShortUrlModel; + +import java.time.Duration; + +public class CacheFactory { + public static Cache makeCachedQuiz(Long id){ + return new Cache<>( + "url:short:"+id, + ShortUrlModel.class, + Duration.ofMinutes(60) + ); + } +} \ No newline at end of file diff --git a/src/main/java/org/service/urlshortener/cache/CacheService.java b/src/main/java/org/service/urlshortener/cache/CacheService.java new file mode 100644 index 0000000..62052d7 --- /dev/null +++ b/src/main/java/org/service/urlshortener/cache/CacheService.java @@ -0,0 +1,56 @@ +package org.service.urlshortener.cache; + +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import org.service.urlshortener.util.MapperUtil; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; + +import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; + +@Service +@RequiredArgsConstructor +public class CacheService { + private final StringRedisTemplate redisTemplate; + + public T getOrNull(Cache cache) { + var data = redisTemplate.opsForValue().get(cache.getKey()); + return (data != null) ? MapperUtil.readValue(data, cache.getType()) : null; + } + + @SneakyThrows + public T get(Cache cache, Callable callable) { + var data = getOrNull(cache); + + if (data == null) { + var calledData = callable.call(); + + asyncSet(cache, calledData); + + return calledData; + } else { + return data; + } + } + + public void set(Cache cache, T data) { + redisTemplate.opsForValue().set( + cache.getKey(), + MapperUtil.writeValueAsString(data), + cache.getDuration() + ); + } + + public void asyncSet(Cache cache, T data) { + CompletableFuture.runAsync(() -> set(cache, data)); + } + + public void delete(Cache cache) { + redisTemplate.delete(cache.getKey()); + } + + public void asyncDelete(Cache cache) { + CompletableFuture.runAsync(() -> delete(cache)); + } +} \ No newline at end of file diff --git a/src/main/java/org/service/urlshortener/error/dto/ErrorMessage.java b/src/main/java/org/service/urlshortener/error/dto/ErrorMessage.java index 2dad621..7139bab 100644 --- a/src/main/java/org/service/urlshortener/error/dto/ErrorMessage.java +++ b/src/main/java/org/service/urlshortener/error/dto/ErrorMessage.java @@ -16,6 +16,8 @@ public enum ErrorMessage { RATE_LIMIT_EXCEEDED(HttpStatus.BAD_REQUEST, "요청 횟수 초과"), NOT_FINISH_DELETE_SIX_MONTHS_OLD_DATA(HttpStatus.NO_CONTENT, "6개월이 지난 데이터 삭제 실패"), + + INVALID_JSON_DATA_ERROR(HttpStatus.BAD_REQUEST, "json data error"), ; private final HttpStatus status; private final String message; @@ -24,4 +26,5 @@ public enum ErrorMessage { this.status = status; this.message = message; } + } \ No newline at end of file diff --git a/src/main/java/org/service/urlshortener/error/exception/InvalidJsonDataException.java b/src/main/java/org/service/urlshortener/error/exception/InvalidJsonDataException.java new file mode 100644 index 0000000..976dc97 --- /dev/null +++ b/src/main/java/org/service/urlshortener/error/exception/InvalidJsonDataException.java @@ -0,0 +1,9 @@ +package org.service.urlshortener.error.exception; + +import org.service.urlshortener.error.dto.ErrorMessage; + +public class InvalidJsonDataException extends BusinessException { + public InvalidJsonDataException(ErrorMessage message) { + super(message); + } +} diff --git a/src/main/java/org/service/urlshortener/shortener/comtroller/rest/ShortenerRestController.java b/src/main/java/org/service/urlshortener/shortener/comtroller/rest/ShortenerRestController.java index ae0d28d..44ce471 100644 --- a/src/main/java/org/service/urlshortener/shortener/comtroller/rest/ShortenerRestController.java +++ b/src/main/java/org/service/urlshortener/shortener/comtroller/rest/ShortenerRestController.java @@ -49,10 +49,8 @@ public void getOriginUrl( @PathVariable("shortCode") String shortCode, HttpServletResponse response ) throws IOException { - log.debug("shortUrl = {}", shortCode); - var originUrl = shortenerService.getOriginUrl( - new ShortCodeRequest(shortCode.replace(UrlDomain.URL,"")) - ).getOriginUrl(); + log.debug("shortCode = {}", shortCode); + var originUrl = shortenerService.getOriginUrl(new ShortCodeRequest(shortCode)).getOriginUrl(); log.debug("originUrl = {}", originUrl); response.sendRedirect(originUrl); diff --git a/src/main/java/org/service/urlshortener/shortener/dto/ShortUrlModel.java b/src/main/java/org/service/urlshortener/shortener/dto/ShortUrlModel.java new file mode 100644 index 0000000..fe91a1c --- /dev/null +++ b/src/main/java/org/service/urlshortener/shortener/dto/ShortUrlModel.java @@ -0,0 +1,16 @@ +package org.service.urlshortener.shortener.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Getter +@AllArgsConstructor +@NoArgsConstructor +public class ShortUrlModel { + private Long id; + private String originalUrl; + private LocalDateTime createAtl; +} \ No newline at end of file diff --git a/src/main/java/org/service/urlshortener/shortener/service/ShortenerService.java b/src/main/java/org/service/urlshortener/shortener/service/ShortenerService.java index 022ecb0..9228f97 100644 --- a/src/main/java/org/service/urlshortener/shortener/service/ShortenerService.java +++ b/src/main/java/org/service/urlshortener/shortener/service/ShortenerService.java @@ -2,9 +2,12 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.service.urlshortener.cache.CacheFactory; +import org.service.urlshortener.cache.CacheService; import org.service.urlshortener.error.dto.ErrorMessage; import org.service.urlshortener.error.exception.url.NotFoundUrlException; import org.service.urlshortener.shortener.domain.OriginUrl; +import org.service.urlshortener.shortener.dto.ShortUrlModel; import org.service.urlshortener.shortener.dto.request.OriginUrlRequest; import org.service.urlshortener.shortener.dto.request.ShortCodeRequest; import org.service.urlshortener.shortener.dto.response.OriginUrlResponse; @@ -18,6 +21,7 @@ @RequiredArgsConstructor public class ShortenerService { private final OriginUrlRepository originUrlRepository; + private final CacheService cacheService; private final EncryptionService encryptionService; /** @@ -26,11 +30,15 @@ public class ShortenerService { */ @Transactional public ShortCodeResponse createShortUrl(OriginUrlRequest request) { - if (originUrlRepository.existsByOriginUrl(request.getOriginUrl())) { - Long id = originUrlRepository.findByOriginUrl(request.getOriginUrl()).get().getId(); - return new ShortCodeResponse(encryptionService.encode(id)); - } OriginUrl url = originUrlRepository.save(new OriginUrl(request.getOriginUrl())); + cacheService + .asyncSet(CacheFactory + .makeCachedQuiz( + url.getId()), + new ShortUrlModel( + url.getId(), + url.getOriginUrl(), + url.getCreatedAt())); return new ShortCodeResponse(encryptionService.encode(url.getId())); } @@ -45,9 +53,15 @@ public ShortCodeResponse createShortUrl(OriginUrlRequest request) { @Transactional(readOnly = true) public OriginUrlResponse getOriginUrl(ShortCodeRequest request) { var originUrlId = encryptionService.decode(request.getShortCode()); - var originUrl = originUrlRepository.findById(originUrlId) - .orElseThrow(() -> new NotFoundUrlException(ErrorMessage.NOT_FOUND_URL)); + var cache = CacheFactory.makeCachedQuiz(originUrlId); + var resultUrl = cacheService.get(cache, () -> { + var findUrl = originUrlRepository + .findById(originUrlId) + .orElseThrow(() -> new NotFoundUrlException(ErrorMessage.NOT_FOUND_URL)); - return new OriginUrlResponse(originUrl.getOriginUrl()); + return new ShortUrlModel(findUrl.getId(), findUrl.getOriginUrl(), findUrl.getCreatedAt()); + }); + + return new OriginUrlResponse(resultUrl.getOriginalUrl()); } } \ No newline at end of file diff --git a/src/main/java/org/service/urlshortener/util/MapperUtil.java b/src/main/java/org/service/urlshortener/util/MapperUtil.java new file mode 100644 index 0000000..7b6d3aa --- /dev/null +++ b/src/main/java/org/service/urlshortener/util/MapperUtil.java @@ -0,0 +1,69 @@ +package org.service.urlshortener.util; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import lombok.extern.slf4j.Slf4j; +import org.service.urlshortener.error.dto.ErrorMessage; +import org.service.urlshortener.error.exception.InvalidJsonDataException; + +import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL; +import static com.fasterxml.jackson.databind.SerializationFeature.WRITE_DATES_AS_TIMESTAMPS; + +@Slf4j +public class MapperUtil { + private static ObjectMapper mapper = new ObjectMapper(); + + /** + * @return ObjectMapper + * @apiNote object mapper + **/ + public static ObjectMapper mapper() { + var deserializationFeature = DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES; + var serializationFeature = SerializationFeature.FAIL_ON_EMPTY_BEANS; + + mapper + .setSerializationInclusion(NON_NULL); + + mapper + .configure(deserializationFeature, false) + .configure(serializationFeature, false); + + mapper + .registerModule(new JavaTimeModule()) + .disable(WRITE_DATES_AS_TIMESTAMPS); + + return mapper; + } + + public static String writeValueAsString(Object object) { + try { + return mapper().writeValueAsString(object); + } catch (JsonProcessingException e) { + log.error("[ERROR] Exception -> {}", e.getMessage()); + throw new InvalidJsonDataException(ErrorMessage.INVALID_JSON_DATA_ERROR); + } + } + + public static T readValue(String json, TypeReference typeReference) { + try { + return mapper().readValue(json, typeReference); + } catch (JsonProcessingException e) { + log.error("[ERROR] Exception -> {}", e.getMessage()); + throw new InvalidJsonDataException(ErrorMessage.INVALID_JSON_DATA_ERROR); + } + } + + public static T readValue(String json, Class clazz) { + try { + log.info("json = {}", json); + return mapper().readValue(json, clazz); + } catch (JsonProcessingException e) { + log.error("[ERROR] Exception -> {}", e.getMessage()); + throw new InvalidJsonDataException(ErrorMessage.INVALID_JSON_DATA_ERROR); + } + } +} \ No newline at end of file