From a82ef55f1296b8fbcb102602d80d1c6697daf968 Mon Sep 17 00:00:00 2001 From: HaRim <76032947+HwangHarim@users.noreply.github.com> Date: Mon, 1 Jul 2024 01:04:25 +0900 Subject: [PATCH 1/2] =?UTF-8?q?fix=20:=20redis=20&=20cache=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9=ED=95=98=EC=97=AC=20=EC=A4=91=EB=B3=B5=20=EC=9A=94?= =?UTF-8?q?=EC=B2=AD=20=EC=B2=98=EB=A6=AC=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/service/urlshortener/cache/Cache.java | 16 +++++ .../urlshortener/cache/CacheFactory.java | 15 +++++ .../urlshortener/cache/CacheService.java | 56 ++++++++++++++++ .../urlshortener/error/dto/ErrorMessage.java | 3 + .../exception/InvalidJsonDataException.java | 9 +++ .../shortener/dto/ShortUrlModel.java | 14 ++++ .../shortener/service/ShortenerService.java | 34 ++++++++-- .../service/urlshortener/util/MapperUtil.java | 66 +++++++++++++++++++ 8 files changed, 206 insertions(+), 7 deletions(-) create mode 100644 src/main/java/org/service/urlshortener/cache/Cache.java create mode 100644 src/main/java/org/service/urlshortener/cache/CacheFactory.java create mode 100644 src/main/java/org/service/urlshortener/cache/CacheService.java create mode 100644 src/main/java/org/service/urlshortener/error/exception/InvalidJsonDataException.java create mode 100644 src/main/java/org/service/urlshortener/shortener/dto/ShortUrlModel.java create mode 100644 src/main/java/org/service/urlshortener/util/MapperUtil.java 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..97f132d --- /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) + ); + } +} 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..fd45e1c --- /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)); + } +} 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/dto/ShortUrlModel.java b/src/main/java/org/service/urlshortener/shortener/dto/ShortUrlModel.java new file mode 100644 index 0000000..4b3429a --- /dev/null +++ b/src/main/java/org/service/urlshortener/shortener/dto/ShortUrlModel.java @@ -0,0 +1,14 @@ +package org.service.urlshortener.shortener.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.time.LocalDateTime; + +@Getter +@AllArgsConstructor +public class ShortUrlModel { + private Long id; + private String originalUrl; + private LocalDateTime createAtl; +} 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..0d3c0d1 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,14 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.aspectj.weaver.ast.Not; +import org.service.urlshortener.cache.Cache; +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; @@ -13,11 +18,15 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import javax.crypto.NullCipher; +import java.util.concurrent.Callable; + @Slf4j @Service @RequiredArgsConstructor public class ShortenerService { private final OriginUrlRepository originUrlRepository; + private final CacheService cacheService; private final EncryptionService encryptionService; /** @@ -26,11 +35,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 +58,16 @@ 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 ShortUrlModel(findUrl.getId(), findUrl.getOriginUrl(), findUrl.getCreatedAt()); + }); + - return new OriginUrlResponse(originUrl.getOriginUrl()); + 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..d7e0eaa --- /dev/null +++ b/src/main/java/org/service/urlshortener/util/MapperUtil.java @@ -0,0 +1,66 @@ +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.SneakyThrows; +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(); + + 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); + } + } + + @SneakyThrows + 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); + } + } + + @SneakyThrows + public static T readValue(String json, Class clazz) { + try { + return mapper().readValue(json, clazz); + } catch (JsonProcessingException e) { + log.error("[ERROR] Exception -> {}", e.getMessage()); + throw new InvalidJsonDataException(ErrorMessage.INVALID_JSON_DATA_ERROR); + } + } +} From e6fe816d91fadf816f272db0c96f07da8cdf1cab Mon Sep 17 00:00:00 2001 From: HaRim <76032947+HwangHarim@users.noreply.github.com> Date: Mon, 1 Jul 2024 15:15:39 +0900 Subject: [PATCH 2/2] =?UTF-8?q?fix=20:=20redis=20&=20cache=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9=ED=95=98=EC=97=AC=20=EC=A4=91=EB=B3=B5=20=EC=9A=94?= =?UTF-8?q?=EC=B2=AD=20=EC=B2=98=EB=A6=AC=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../urlshortener/cache/CacheFactory.java | 2 +- .../urlshortener/cache/CacheService.java | 2 +- .../rest/ShortenerRestController.java | 2 +- .../shortener/dto/ShortUrlModel.java | 4 +++- .../shortener/service/ShortenerService.java | 6 ----- .../service/urlshortener/util/MapperUtil.java | 23 +++++++++++-------- 6 files changed, 19 insertions(+), 20 deletions(-) diff --git a/src/main/java/org/service/urlshortener/cache/CacheFactory.java b/src/main/java/org/service/urlshortener/cache/CacheFactory.java index 97f132d..b951ad2 100644 --- a/src/main/java/org/service/urlshortener/cache/CacheFactory.java +++ b/src/main/java/org/service/urlshortener/cache/CacheFactory.java @@ -12,4 +12,4 @@ public static Cache makeCachedQuiz(Long id){ 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 index fd45e1c..62052d7 100644 --- a/src/main/java/org/service/urlshortener/cache/CacheService.java +++ b/src/main/java/org/service/urlshortener/cache/CacheService.java @@ -53,4 +53,4 @@ public void delete(Cache cache) { public void asyncDelete(Cache cache) { CompletableFuture.runAsync(() -> delete(cache)); } -} +} \ No newline at end of file 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 93632e0..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,7 +49,7 @@ public void getOriginUrl( @PathVariable("shortCode") String shortCode, HttpServletResponse response ) throws IOException { - log.debug("shortUrl = {}", shortCode); + log.debug("shortCode = {}", shortCode); var originUrl = shortenerService.getOriginUrl(new ShortCodeRequest(shortCode)).getOriginUrl(); log.debug("originUrl = {}", 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 index 4b3429a..fe91a1c 100644 --- a/src/main/java/org/service/urlshortener/shortener/dto/ShortUrlModel.java +++ b/src/main/java/org/service/urlshortener/shortener/dto/ShortUrlModel.java @@ -2,13 +2,15 @@ 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 0d3c0d1..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,8 +2,6 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.aspectj.weaver.ast.Not; -import org.service.urlshortener.cache.Cache; import org.service.urlshortener.cache.CacheFactory; import org.service.urlshortener.cache.CacheService; import org.service.urlshortener.error.dto.ErrorMessage; @@ -18,9 +16,6 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import javax.crypto.NullCipher; -import java.util.concurrent.Callable; - @Slf4j @Service @RequiredArgsConstructor @@ -67,7 +62,6 @@ public OriginUrlResponse getOriginUrl(ShortCodeRequest request) { 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 index d7e0eaa..7b6d3aa 100644 --- a/src/main/java/org/service/urlshortener/util/MapperUtil.java +++ b/src/main/java/org/service/urlshortener/util/MapperUtil.java @@ -6,7 +6,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.service.urlshortener.error.dto.ErrorMessage; import org.service.urlshortener.error.exception.InvalidJsonDataException; @@ -18,7 +17,11 @@ public class MapperUtil { private static ObjectMapper mapper = new ObjectMapper(); - public static ObjectMapper mapper(){ + /** + * @return ObjectMapper + * @apiNote object mapper + **/ + public static ObjectMapper mapper() { var deserializationFeature = DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES; var serializationFeature = SerializationFeature.FAIL_ON_EMPTY_BEANS; @@ -32,19 +35,19 @@ public static ObjectMapper mapper(){ 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()); + 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); } } - @SneakyThrows public static T readValue(String json, TypeReference typeReference) { try { return mapper().readValue(json, typeReference); @@ -54,13 +57,13 @@ public static T readValue(String json, TypeReference typeReference) { } } - @SneakyThrows 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