Skip to content

Commit

Permalink
Merge pull request #13 from selab-hs/develop
Browse files Browse the repository at this point in the history
[Develop] redis cache 적용
  • Loading branch information
HwangHarim authored Jul 1, 2024
2 parents ed49d9e + d627cda commit 4b5993b
Show file tree
Hide file tree
Showing 9 changed files with 207 additions and 11 deletions.
16 changes: 16 additions & 0 deletions src/main/java/org/service/urlshortener/cache/Cache.java
Original file line number Diff line number Diff line change
@@ -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<T> {
private String key;
private Class<T> type;
private Duration duration;
}
15 changes: 15 additions & 0 deletions src/main/java/org/service/urlshortener/cache/CacheFactory.java
Original file line number Diff line number Diff line change
@@ -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<ShortUrlModel> makeCachedQuiz(Long id){
return new Cache<>(
"url:short:"+id,
ShortUrlModel.class,
Duration.ofMinutes(60)
);
}
}
56 changes: 56 additions & 0 deletions src/main/java/org/service/urlshortener/cache/CacheService.java
Original file line number Diff line number Diff line change
@@ -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> T getOrNull(Cache<T> cache) {
var data = redisTemplate.opsForValue().get(cache.getKey());
return (data != null) ? MapperUtil.readValue(data, cache.getType()) : null;
}

@SneakyThrows
public <T> T get(Cache<T> cache, Callable<T> callable) {
var data = getOrNull(cache);

if (data == null) {
var calledData = callable.call();

asyncSet(cache, calledData);

return calledData;
} else {
return data;
}
}

public <T> void set(Cache<T> cache, T data) {
redisTemplate.opsForValue().set(
cache.getKey(),
MapperUtil.writeValueAsString(data),
cache.getDuration()
);
}

public <T> void asyncSet(Cache<T> cache, T data) {
CompletableFuture.runAsync(() -> set(cache, data));
}

public <T> void delete(Cache<T> cache) {
redisTemplate.delete(cache.getKey());
}

public <T> void asyncDelete(Cache<T> cache) {
CompletableFuture.runAsync(() -> delete(cache));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -24,4 +26,5 @@ public enum ErrorMessage {
this.status = status;
this.message = message;
}

}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -18,6 +21,7 @@
@RequiredArgsConstructor
public class ShortenerService {
private final OriginUrlRepository originUrlRepository;
private final CacheService cacheService;
private final EncryptionService encryptionService;

/**
Expand All @@ -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()));
}
Expand All @@ -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());
}
}
69 changes: 69 additions & 0 deletions src/main/java/org/service/urlshortener/util/MapperUtil.java
Original file line number Diff line number Diff line change
@@ -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> T readValue(String json, TypeReference<T> 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> T readValue(String json, Class<T> 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);
}
}
}

0 comments on commit 4b5993b

Please sign in to comment.