제가 진행하고 있는 프로젝트에서 멱등키라는걸 사용해보려고하는데요.
멱등키를 적용해야겠다고 생각한 문제 상황은 다음과 같습니다.
멱등키를 사용해야겠다고 생각한 문제 상황
postman 으로 요청을 보내던 도중 응답이 좀 느려서 Send 버튼을 미친듯이 눌렀더니 DB 에 Id 값만 다른, 다른 데이터는 모두 동일한 데이터가 미친듯이 추가되어 있는 것을 확인했습니다.
이게 실제 상황이라면 같은 데이터가 같은 유저에게서 한번만 요청이 와야하는데 여러번 오면 이제 결제와 같이 금융적인 민감한 상황에서는 클라이언트의 의도와는 다르게 여러번의 결제가 일어나게 되는 엄청난 상황이 발생하게 될 지도 모릅니다! 이걸 어떻게 해결할까 하다가 멱등키라는 것을 알게돼서 '게시글 생성' 부분에 멱등키를 적용해보고자 했습니다.
먼저 멱등한 것이 무엇인지부터 살펴보겠습니다!
멱등성이란???
멱등하다는 것은 첫번째 수행을 한 뒤 여러 차례 적용해도 결과를 변형시키지 않는 작업 또는 기능의 속성을 뜻합니다. 즉, 멱등한 작업의 결과는 한번 수행하든 여러번 수행하든 그 결과도 같아집니다.
HTTP 메서드를 살펴보면,
GET, PUT, DELETE 가 멱등하다고 말하고,
POST, PATCH 가 멱등하지 않다고 말하는데요.
생각해봅시다! 단순한 GET 요청을 생각해보면 수백번 DB 에서 값을 조회해온다고 해서 그 결과값이 달라지지 않으며, DB 에도 전혀 영향을 미치지 않습니다. "엥 DB 값이 달라지면 GET 요청 달라지는데?" 라는 생각이 든다면 DB 를 바꾸는 메소드는 POST 요청으로 이루어지기 때문에 단순 GET 요청만으로는 절대로 결과값이나 리소스가 달라질 수 없습니다!!
왜 멱등성이 필요하지???
가장 일반적인 이유에서의 멱등성의 존재이유는 다음과 같습니다.
1. 네트워크 지연 또는 악의적인 사용자의 반복 요청(F5 연타, 네트워크 재전송, 중복 클릭 등)으로 인해 동일한 API 가 여러 번 호출되는 경우, 멱등키를 통해 중복 요청을 차단함으로써 서버의 무결성과 안정성을 확보할 수 있습니다.
2. 클라이언트가 응답을 못받았을 때 동일한 멱등키로 다시 요청하면, 중복 없이 이전 응답을 재사용할 수 있어 안정적입니다.
그러면, 왜 제 기능에서 멱등성이 필요했냐! 에 대해서 말해보자면
먼저 제 기능은 '멘토스'라고 하는 모임 홍보를 위한 게시글 생성이었는데요. 게시글이라고 하면 아주 간단하게 제목, 작성자, 내용이 필수적으로 필요할텐데 제가 멱등키를 이용한 것은 동일한 "요청"을 차단하기 위함이었습니다.
단순히 DB 의 Unique 키를 이용해서 동일한 사용자가 동일한 제목의 게시글을 생성하지 못하게 막아도 지금 저의 기능에서의 요구사항은 충족시킬 수 있었지만, 이렇게하게되면 이제 저의 서비스에서는 영영 똑같은 멘토스를 다시는 생성하지 못하게 되므로 사용자 경험이 저하된다는 단점이 발생됩니다!
단지, 일정 시간동안 들어오는 아주 동일한 요청에 대해서만 중복된 요청으로 판단하면 되겠구나! 했고, 그 결론은 멱등키였습니다.
멱등하려면 구현은 어떻게 하는거지??
멱등키를 구현하려면 API 요청 헤더에 멱등키를 포함시켜 프론트 -> 서버 로 전송하면 되는데요!

저는 잘 동작이 되는지 확인하기 위해서 postman 에서 위의 사진처럼 테스트키를 넣고 동작시켜보았습니다.
이 멱등키를 저는 RDBMS가 아니라 Redis 에 저장해두는데요.
왜냐하면 멱등키는 영구저장이 목적이 아니고 일정시간 후에는 사라져야하는 키 값이니까요...!!
그래서 이제 멱등키를 일정시간 동안 저장해두기 위해서 Redis 설정을 시작해보겠습니다.
먼저 pom.xml 에 아래의 의존성을 추가해주세요
<dependency>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
<version>6.2.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
<version>2.6.6</version> <!-- Spring 5.3.x 호환 버전 -->
</dependency>
Redis 가 로컬 컴퓨터에 이미 설치되어 있다는 가정에서의 코드입니다. 혹시라도 설치되어 있지 않다면 이전 게시글을 참고해서 설치해주세요!! 여기에 접속할 Redis 정보를 넣어줍니다.
@Configuration
public class RedisConfig {
@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory("localhost", 6379);
}
@Bean
public RedisTemplate<String, String> redisTemplate() {
RedisTemplate<String, String> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory());
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new StringRedisSerializer());
return template;
}
}
그 다음 레디스에 멱등키를 중복된 값이 있는지 확인하고, 저장하고, 저장된 정보를 가져오는 메소드들로 이루어진 서비스코드를 작성해줍니다.
@Service
@Slf4j
public class IdempotencyService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
public boolean isDuplicate(String key) {
log.info("[IdempotencyService.isDuplicate]");
return Boolean.TRUE.equals(redisTemplate.hasKey(key));
}
public void saveKey(String key, String value) {
log.info("[IdempotencyService.saveKey]");
redisTemplate.opsForValue().set(key, value, 30, TimeUnit.SECONDS); // TTL : 30초
}
public String getSavedResponse(String key) {
log.info("[IdempotencyService.getSavedResponse]");
return redisTemplate.opsForValue().get(key);
}
}
이제 진짜 테스트를 하면서 멱등키가 잘 동작하는지 살펴보겠습니다.
아직 api 요청을 하기 전에 redis 에 저장된 키가 있는지 살펴보면 empty array 로 나오는 것을 확인할 수 있습니다.

이제 postman 으로 가서 요청보낼 post api 의 body 값들을 다 채워준 다음 요청을 보내면 저는 responseBody 에 아무런값도 담지 않았기 때문에 200 OK 응답만 잘 온것을 확인할 수 있습니다. 요청이 잘 왔다는 것은 프론트에서 보낸 멱등키가 서버의 Redis 에 저장이 되었다는 뜻이므로 확인해보겠습니다!!

다음과 같이 제가 헤더에 넣어서 보낸 멱등키가 저장되어 있는 것을 확인할 수 있고,

저는 프론트와 서버 모두 멱등키 유지시간을 30초로 지정해두었는데요! 그래서 그 30초가 지나기 전에 다시 postman 으로 동일한 요청을 보내게 되면 다음과 같이 result 라는 곳에 값이 들어오게 됩니다.

이미 동일한 키를 가지고 있는 요청이 redis 에 1건 존재한다! 이 의미가 되겠습니다. 30초가 지나서 redis 가 다음과 같이 비어있는 것을 확인하고 다시 동일한 요청을 날리게된다면 그때는 다시 디비에 삽입되는 것을 확인할 수 있습니다.


이 멱등키를 언제까지 유지해야하는지도 고민 포인트였는데요.
프론트도 서버도 30초는 유지를 하되, 만약 프론트에서 전송할 필드값이 하나라도 수정되면 그 즉시 멱등키 필드를 =null 로 바꾸고 새로운 멱등키를 발급받아 요청헤더에 넣을 수 있도록 구현해두었습니다!
끝!
'프로젝트' 카테고리의 다른 글
| [리펙토링]인덱스로 쿼리 실행시간 줄이기 (0) | 2024.10.05 |
|---|---|
| Spring 서버 HTTPS 로 배포하기 (0) | 2024.08.30 |
| WebRTC offer 와 answer 가 뭐지 (0) | 2024.08.23 |
| Mediasoup 초기 시작 세팅하기 (0) | 2024.08.22 |
| 서버 배포 후 카카오 로그인 먹통현상.. (0) | 2024.08.01 |