최근에 흠뻑쇼를 보기 위해 티켓팅을 시도했다.
접속자 수가 너무 많아서, 응답이 너무 느리니까 결제버튼을 여러번 눌렀다.
"어... 근데 이 비싼 비용이 여러 번 결제되면 어떡하지…? 🤔"
이런 현상을 방지하고, 결제를 1회만 보장하기 위해 어떻게 해야할지 알아보았다.
그러기 위해선 먼저 "멱등성"이란 단어를 알아야 된다.
멱등성 (idempotent)
연산을 여러 번 적용하더라도 결과가 달라지지 않는 성질을 의미
위키백과에 나와있는 멱등성 정의다.
https://www.tosspayments.com/blog/articles/dev-1
(토스 기술블로그를 많이 참조했다.)
간단하게 정리를 해보자면,
조회를 하는 GET PUT, 삭제하는 DELETE 등 리소스를 조회하거나 대체하는 메서드는 아무리 여러번 요청을 해도 응답하는 값이 바뀌지 않는다.
이러한 경우를 "멱등"하다고 한다.
반면, 서버 데이터를 변경하는 POST PATCH 이런 메소드들은 호출할 때마다 응답이 달라진다.
이러한 경우는 "멱등"하다고 할 수 없다.

게시물이 등록이 안되어서 등록 버튼을 여러번 눌렀더니...
똑같은 게시물이 3개가 등록이 되었다. 😮💨
일명 "따닥" 이라는 중복 요청 현상이다.
나머지 두 개는 API 요청이 불필요하게 연속적으로 발생해버린 상황이다.
조회같은 API 요청에 비해,
게시물 업로드는 누른 만큼 요청이 진행되어서, 멱등성을 보장하지 못했다.
// Controller
@PostMapping
public ResponseResult saveEmotionWithMusicAndIdempotency(
@RequestHeader(name = "Idempotency-Key") String idempotencyKey,
@AuthenticationPrincipal Long userId,
@Valid @RequestBody EmotionRecordRequestDTO request) {
return emotionRecordService.saveEmotionRecordWithMusicAndIdempotent(idempotencyKey, userId, request);
}
Idempotency-Key: {IDEMPOTENCY_KEY}
멱등키라는 값을 헤더에 추가시켰다.

사용자가 게시물에 업로드를 하게 되면, 일단 Redis에 같은 멱등키가 있는지 조회한다.
만약 같은 멱등키가 없다면, 새로 게시물을 DB에 저장하고
Redis에 Key를 멱등키, Value를 응답값으로 저장한다.
{서버 Address}:6379> get EmotionRecord: f2eaf582-8f13-4a1d-a77a-86e11e0ff206
"{\"@class\": \"org.dfbf.{프로젝트 이름}.global.exception.ResponseResult\"
, \ "code\" : 200
, \"message)": \"\xec \x84\xb1\xea\xb3\xb5\", \"timestamp\":\"2025-06-13T09:11:18.71 4525189Z\", \"data\":null}"
이런식으로 저장이 되고, 응답하게 된다.
같은 멱등키가 있을 경우, Redis에 저장된 응답 값으로 응답한다.
"그러면.... 같은 페이지에서 새로운 요청 보내야 하면, 새로고침 해야 돼요...?"
Redis를 선택한 이유는 두 가지가 있다.
보통 네트워크 지연 시간을 넉넉하게 잡아서... 10초가 최대라고 생각하고 TTL을 10초로 설정했다.
최초로 요청을 보내고, 10초 이내에는 같은 페이지에서 같은 요청을 보낼 수 없다.
또한 이런 "멱등성 인증" 단계를 추가하게 되면,
불필요한 요청이 DB를 거치지 않게 되어 API 성능 개선에 도움된다.
이러한 로직으로,
멱등성을 보장하는 로직을 추가하면서 API 성능도 개선했다.
후... 자꾸 Redis에
\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00
이러한 값들이 저장되어 Redis에서 다시 클래스로 역직렬화 할 때,
직렬화가 진행되지 않았다...
GPT에게 계속 물어봐도 이상한 답변을 늘어놨다..;;
직접 에러를 구글링해서 찾아보던 도중..
https://stackoverflow.com/questions/47691478/springboot-redistemplate-value-has-x00-data
요런 글을 봤다.
redisTemplate.opsForValue().set(
"EmotionRecord:" + idempotencyKey,
responseResult,
10);
뒤에 "초 단위" 라고 명시하는 TimeUnit.SECONDS를 붙이지 않아서 생긴 오류였다.
redisTemplate.opsForValue().set(
"EmotionRecord:" + idempotencyKey,
responseResult,
10,
TimeUnit.SECONDS);
해결 완료!