-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Feature: 정답 제출 POST API 멱등성 처리 (#25)
* feat: 정답 제출 API 멱등성 구현 - 커스텀 필터를 통한 서블릿 응답 래핑 - 인터셉터와 캐시를 이용한 멱등성 DB 구현 * feat: 키가 존재하지 않는 경우 스킵 * feat: 인터셉터에서 반환하는 형식 및 로그 수정 * refactor: 멱등성 관련 클래스 패키지 분리
- Loading branch information
Showing
7 changed files
with
234 additions
and
2 deletions.
There are no files selected for viewing
22 changes: 22 additions & 0 deletions
22
src/main/java/org/cotato/csquiz/common/config/WebConfig.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
package org.cotato.csquiz.common.config; | ||
|
||
import lombok.RequiredArgsConstructor; | ||
import org.cotato.csquiz.common.idempotency.IdempotencyInterceptor; | ||
import org.cotato.csquiz.common.idempotency.IdempotencyRedisRepository; | ||
import org.springframework.context.annotation.Configuration; | ||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry; | ||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; | ||
|
||
@Configuration | ||
@RequiredArgsConstructor | ||
public class WebConfig implements WebMvcConfigurer { | ||
|
||
private final IdempotencyRedisRepository idempotencyRedisRepository; | ||
|
||
@Override | ||
public void addInterceptors(InterceptorRegistry registry) { | ||
registry.addInterceptor(new IdempotencyInterceptor(idempotencyRedisRepository)) | ||
.addPathPatterns("/v1/api/record/reply") | ||
.order(1); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
23 changes: 23 additions & 0 deletions
23
src/main/java/org/cotato/csquiz/common/idempotency/CustomServletWrappingFilter.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
package org.cotato.csquiz.common.idempotency; | ||
|
||
import jakarta.servlet.FilterChain; | ||
import jakarta.servlet.ServletException; | ||
import jakarta.servlet.http.HttpServletRequest; | ||
import jakarta.servlet.http.HttpServletResponse; | ||
import java.io.IOException; | ||
import org.springframework.stereotype.Component; | ||
import org.springframework.web.filter.OncePerRequestFilter; | ||
import org.springframework.web.util.ContentCachingResponseWrapper; | ||
|
||
@Component | ||
public class CustomServletWrappingFilter extends OncePerRequestFilter { | ||
@Override | ||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) | ||
throws ServletException, IOException { | ||
|
||
final ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response); | ||
filterChain.doFilter(request, responseWrapper); | ||
|
||
responseWrapper.copyBodyToResponse(); | ||
} | ||
} |
71 changes: 71 additions & 0 deletions
71
src/main/java/org/cotato/csquiz/common/idempotency/IdempotencyInterceptor.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
package org.cotato.csquiz.common.idempotency; | ||
|
||
import com.fasterxml.jackson.databind.ObjectMapper; | ||
import jakarta.servlet.http.HttpServletRequest; | ||
import jakarta.servlet.http.HttpServletResponse; | ||
import lombok.RequiredArgsConstructor; | ||
import lombok.extern.slf4j.Slf4j; | ||
import org.cotato.csquiz.common.error.ErrorCode; | ||
import org.cotato.csquiz.common.error.response.ErrorResponse; | ||
import org.springframework.http.HttpStatus; | ||
import org.springframework.stereotype.Component; | ||
import org.springframework.web.servlet.HandlerInterceptor; | ||
import org.springframework.web.servlet.ModelAndView; | ||
import org.springframework.web.util.ContentCachingResponseWrapper; | ||
|
||
@Slf4j | ||
@Component | ||
@RequiredArgsConstructor | ||
public class IdempotencyInterceptor implements HandlerInterceptor { | ||
|
||
private static final String IDEMPOTENCY_HEADER = "Idempotency-Key"; | ||
private final IdempotencyRedisRepository idempotencyRedisRepository; | ||
|
||
@Override | ||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) | ||
throws Exception { | ||
String idempotencyKey = request.getHeader(IDEMPOTENCY_HEADER); | ||
if (idempotencyKey == null) { | ||
return true; | ||
} | ||
ObjectMapper objectMapper = new ObjectMapper(); | ||
|
||
if (idempotencyRedisRepository.hasSucceedResult(idempotencyKey)) { | ||
response.getWriter() | ||
.write(objectMapper.writeValueAsString( | ||
idempotencyRedisRepository.getSucceedResponse(idempotencyKey))); | ||
log.info("[멱등성 DB에 데이터 존재]"); | ||
return false; | ||
} | ||
|
||
if (idempotencyRedisRepository.isProcessing(idempotencyKey)) { | ||
response.setStatus(HttpStatus.CONFLICT.value()); | ||
response.setContentType("application/json; charset=UTF-8"); | ||
response.getWriter() | ||
.write(objectMapper.writeValueAsString(ErrorResponse.of(ErrorCode.PROCESSING, request))); | ||
log.warn("[요청은 왔지만 아직 처리 중]"); | ||
return false; | ||
} | ||
|
||
// 캐시에 결과가 존재하지 않으면 -> 처리중이란 값을 넣고 컨트롤러를 실행함 | ||
idempotencyRedisRepository.saveStatusProcessing(idempotencyKey); | ||
return true; | ||
} | ||
|
||
@Override | ||
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, | ||
ModelAndView modelAndView) throws Exception { | ||
String idempotencyKey = request.getHeader(IDEMPOTENCY_HEADER); | ||
if (idempotencyKey == null) { | ||
return; | ||
} | ||
ObjectMapper objectMapper = new ObjectMapper(); | ||
|
||
final ContentCachingResponseWrapper responseWrapper = (ContentCachingResponseWrapper) response; | ||
|
||
idempotencyRedisRepository.saveSucceedResult(idempotencyKey, | ||
objectMapper.readTree(responseWrapper.getContentAsByteArray())); | ||
|
||
responseWrapper.copyBodyToResponse(); | ||
} | ||
} |
75 changes: 75 additions & 0 deletions
75
src/main/java/org/cotato/csquiz/common/idempotency/IdempotencyRedisRepository.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
package org.cotato.csquiz.common.idempotency; | ||
|
||
import java.util.concurrent.TimeUnit; | ||
import lombok.RequiredArgsConstructor; | ||
import lombok.extern.slf4j.Slf4j; | ||
import org.springframework.data.redis.core.RedisTemplate; | ||
import org.springframework.stereotype.Repository; | ||
|
||
@Slf4j | ||
@Repository | ||
@RequiredArgsConstructor | ||
public class IdempotencyRedisRepository { | ||
|
||
private static final String KEY_PREFIX = "$Idempotency "; | ||
private static final int RESPONSE_EXPIRATION = 30; | ||
|
||
private final RedisTemplate<String, Object> redisTemplate; | ||
|
||
|
||
public boolean hasSucceedResult(String idempotencyKey) { | ||
String key = KEY_PREFIX + idempotencyKey; | ||
IdempotencyResponse response = (IdempotencyResponse) redisTemplate.opsForValue().get(key); | ||
|
||
if (response == null) { | ||
return false; | ||
} | ||
return response.isSucceed(); | ||
} | ||
|
||
public void saveStatusProcessing(String idempotencyKey) { | ||
String key = KEY_PREFIX + idempotencyKey; | ||
IdempotencyResponse response = IdempotencyResponse.builder() | ||
.processStatus(ProcessStatus.PROCESSING) | ||
.build(); | ||
|
||
redisTemplate.opsForValue().set( | ||
key, | ||
response, | ||
RESPONSE_EXPIRATION, | ||
TimeUnit.MINUTES | ||
); | ||
} | ||
|
||
public boolean isProcessing(String idempotencyKey) { | ||
String key = KEY_PREFIX + idempotencyKey; | ||
IdempotencyResponse response = (IdempotencyResponse) redisTemplate.opsForValue().get(key); | ||
log.info("[처리 중]"); | ||
if (response == null) { | ||
return false; | ||
} | ||
return response.isProcessing(); | ||
} | ||
|
||
public Object getSucceedResponse(String idempotencyKey) { | ||
String key = KEY_PREFIX + idempotencyKey; | ||
IdempotencyResponse savedData = (IdempotencyResponse) redisTemplate.opsForValue().get(key); | ||
return savedData.getResult(); | ||
} | ||
|
||
public void saveSucceedResult(String idempotencyKey, Object result) { | ||
final String key = KEY_PREFIX + idempotencyKey; | ||
|
||
IdempotencyResponse response = IdempotencyResponse.builder() | ||
.processStatus(ProcessStatus.SUCCESS) | ||
.result(result) | ||
.build(); | ||
|
||
redisTemplate.opsForValue().set( | ||
key, | ||
response, | ||
RESPONSE_EXPIRATION, | ||
TimeUnit.MINUTES | ||
); | ||
} | ||
} |
23 changes: 23 additions & 0 deletions
23
src/main/java/org/cotato/csquiz/common/idempotency/IdempotencyResponse.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
package org.cotato.csquiz.common.idempotency; | ||
|
||
import java.io.Serializable; | ||
import lombok.AllArgsConstructor; | ||
import lombok.Builder; | ||
import lombok.Data; | ||
|
||
@Builder | ||
@AllArgsConstructor | ||
@Data | ||
public class IdempotencyResponse implements Serializable { | ||
|
||
private ProcessStatus processStatus; | ||
private Object result; | ||
|
||
public boolean isProcessing() { | ||
return this.processStatus == ProcessStatus.PROCESSING; | ||
} | ||
|
||
public boolean isSucceed() { | ||
return this.processStatus == ProcessStatus.SUCCESS; | ||
} | ||
} |
16 changes: 16 additions & 0 deletions
16
src/main/java/org/cotato/csquiz/common/idempotency/ProcessStatus.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
package org.cotato.csquiz.common.idempotency; | ||
|
||
import lombok.AllArgsConstructor; | ||
import org.cotato.csquiz.common.error.ErrorCode; | ||
|
||
@AllArgsConstructor | ||
public enum ProcessStatus { | ||
|
||
|
||
PROCESSING("현재 해당 요청 처리 중"), | ||
SUCCESS("요청 완료") | ||
|
||
; | ||
|
||
private final String description; | ||
} |