앱 소개

오늘 하루 어땠어? 봉봉이가 묻고있어요. 귀엽고 포근한 리트리버 강아지 봉봉이가 사용자의 일기를 읽고 답장을 보내주는 앱, Hearu예요.
개요
사이드 프로젝트, 실무 등에서 데이터를 저장하는 과정에서 한 번만 저장되는 것을 기대하고 설계하였지만 같은 데이터가 중복 저장된 경험이 있으신가요? Hearu에서는 일기를 저장하는 과정에서 데이터가 중복 저장되었던 경험이 있어 이 문제를 해결하는 과정 정리한 글이에요.
문제 상황
일기를 저장하는 과정에서 데이터 중복이 발생했어요. 일기 저장하는 과정은 아래와 같아요.
감정 선택 화면에서 "전송하기" 버튼 클릭
|
| (버튼 비활성화 누락 → 재클릭 가능 → 중복 저장 발생!)
|
저장 API 호출
│
├─ 실패 (4xx / 5xx / 네트워크 오류)
│ ├─ 에러 토스트
│ └─ 화면 이동 없음
│
└─ 성공 (200 OK, diaryId 반환)
│
AI 응답 대기 화면으로 이동
원래는 "전송하기" 버튼을 누르면 버튼이 비활성화 되고, 나중에 실패했을 때 다시 활성화되도록 해야 하는데 누락되었어요. 그래서 네트워크가 불안정한 상황에서 API 호출이 원활하게 진행되지 않으니 화면은 넘어가지 않고, 버튼도 활성화가 되어있는 상태인거죠. 그래서 "전송하기" 버튼을 여러번 눌러서 동일한 데이터가 저장된거에요.
서버에서 데이터 중복 처리를 한 건 아니지만 이 문제는 앱에서 버튼을 비활성화하지 않아서 중복 클릭으로 인해 발생한 문제였어요. 클라이언트 버그는 고쳤지만, 만약 서버가 여러 클라이언트에서 호출된다면? API를 직접 호출하는 악의적 사용자라면? 클라이언트만 믿는 건 위험하다고 느껴서 서버 레벨의 멱등성 처리까지 알아봤어요.
멱등성
멱등성(Idempotency)이란 같은 요청을 여러 번 보내도 결과가 동일한 성질을 말해요.
예를 들어 엘리베이터 닫힘 버튼을 생각해볼게요. 이미 닫히는 중인데 버튼을 10번 더 눌러도 결과는 "닫힌 상태"로 동일해요. 반면 일기 저장이 중복 요청될 때마다 새 데이터가 생기죠. 이건 멱등하지 않은 상황이에요.
HTTP 메서드와 멱등성
HTTP 메서드에도 멱등성이 적용돼요.
| 메서드 | 멱등성 | 이유 |
| GET | ✅ | 조회만 하므로 몇 번 불러도 동일 |
| PUT | ✅ | 같은 내용으로 덮어쓰므로 결과 동일 |
| DELETE | ✅ | 이미 삭제된 리소스 재삭제는 Not Found |
| POST | ❌ | 호출마다 새 리소스 생성 |
Hearu의 일기 저장은 POST 요청이에요. 그래서 기본적으로 멱등하지 않기 때문에 별도 처리가 필요해요.
멱등성 보장 방법 3가지
멱등성을 보장하는 대표적인 세 가지를 알아봤어요.
1. DB Unique 제약 조건
가장 단순한 방법이에요. 중복이 안 되는 컬럼에 UNIQUE 제약 조건을 걸어 DB 레벨에서 막는 방식이에요.
구현이 쉽지만, 어떤 기준으로 "같은 요청"을 판단할지 설계가 필요해요.
예를 들어 하루에 일기를 1개만 쓸 수 있다면 (user_id, date) 조합에 UNIQUE를 걸면 돼요.
2. 클라이언트 요청 토큰
클라이언트가 요청 바디에 고유한 토큰을 포함시키고, 서버가 이를 기준으로 중복 여부를 판단하는 방식이에요. 구현 자체는 간단하지만 API 설계가 지저분해지는 단점이 있어요.
3. Idempotency-Key 헤더
Stripe 같은 결제 API에서 많이 쓰는 방식이에요. 클라이언트가 요청할 때 고유한 키(UUID 등)를 헤더에 담아 보내고, 서버는 이 키를 저장해 동일 키의 요청이 오면 기존 응답을 그대로 반환해요. 중복 요청 자체를 막는 게 아니라, 결과를 동일하게 만드는 방식이에요.
사용자가 "전송하기" 클릭
│
클라이언트가 UUID 생성 → Idempotency-Key 헤더에 포함해서 POST 요청
│
서버: 이 UUID 처리한 적 있어?
├─ 없음 → 일기 저장 + UUID 기록 → 응답 반환
└─ 있음 → 저장 없이 기존 응답 그대로 반환
Hearu에서 Idempotency-Key를 선택한 이유
세 가지 방법을 비교했을 때 Hearu에는 Idempotency-Key 헤더 방식이 가장 적합하다고 판단했어요.
- 하루에 일기를 여러 개 쓸 수 있어서 DB Unique 제약 조건은 적용 불가
- 요청 토큰 방식보다 API 설계가 명확 (비즈니스 데이터와 섞이지 않음)
- 실무에서도 검증된 패턴
Idempotency-Key 적용하기
앱 변경 사항
클라이언트에서 UUID를 언제 생성하느냐가 중요해요.
// ❌ 버튼 클릭할 때마다 UUID 생성 → 키가 매번 달라져서 의미 없음
onClickSend() {
val uuid = UUID.randomUUID()
api.saveDiary(uuid, ...)
}
// ✅ 일기 작성 시작할 때 한 번만 생성 → 재시도 시 같은 키 재사용
onStartWriting() {
val uuid = UUID.randomUUID()
}
버튼을 누를 때마다 새 UUID를 생성하면 Idempotency-Key 방식 자체가 무의미해집니다.
백엔드 변경 사항
별도 테이블 없이 diary 테이블에 컬럼하나만 추가했어요.
ALTER TABLE diary
ADD COLUMN idempotency_key VARCHAR(36) UNIQUE;
Entity에 @Column(unique = true)로 필드를 추가하고, Controller에서는 @RequestHeader로 키를 받아 Service에 넘겨줘요. 핵심 로직은 Service에 있어요.
// 예시 코드입니다.
@Transactional
public DiaryResponse saveDiary(String idempotencyKey, DiaryRequest request, Long userId) {
// 1. 이미 처리된 요청인지 확인
return diaryRepository.findByIdempotencyKey(idempotencyKey)
.map(DiaryResponse::from) // 있으면 기존 응답 반환
.orElseGet(() -> {
// 2. 없으면 저장
Diary diary = diaryRepository.save(
request.toEntity(userId, idempotencyKey)
);
return DiaryResponse.from(diary);
});
}
테스트
로컬 환경에서 JMeter로 5개 동시 요청이 들어왔을 때 일기 저장 멱등성이 깨지는지 확인하는 테스트 시나리오에요.
테스트 조건은 다음과 같아요.
- race condition은 고려하지 않는다.
- 5개의 요청이 0.1s 간격으로 발생한다. (사람이 물리적으로 연속 클릭 가능한 시간을 대략적으로 설정한 시간)
Idempotency-Key 방식 도입 전


5개 요청에 대해서 서로 다른 데이터로 응답하는 것을 확인할 수 있어요.

일기가 중복으로 5개 저장되어 멱등성이 깨지는 것을 확인할 수 있었어요.
Idempotency-Key 방식 도입 후


5개 요청에 대해서 똑같은 데이터로 응답하는 것을 확인할 수 있어요.
(단, 요청이 거의 동시에 들어오는 경우 race condition으로 인해 경합에 실패한 요청은 500 에러가 나타납니다. 하지만 이러한 경우는 사용자의 동작을 벗어난 요청이므로 고려하지 않았습니다.)

5개의 요청이 들어와도 DB에는 동일한 idempotency-Key에 대해 하나의 데이터만 생성하는 것을 확인할 수 있어요.
결론
클라이언트 버그 하나가 멱등성이라는 개념까지 공부하게 만들었어요.
버튼 비활성화 누락이라는 단순한 실수였지만, 덕분에 서버가 중복 요청에 대해 어떻게 방어해야 하는지를 고민해볼 수 있었어요.
정리하면 이렇게 됐어요.
- 클라이언트 — 버튼 비활성화로 중복 클릭 방지
- 서버 — Idempotency-Key로 중복 저장 방지
다만 동시 요청이 들어오는 경우 경합에 실패한 요청은 500 에러가 발생하면서 멱등성(모든 요청에 동일한 응답)이 완전히 보장되지 않아요. 멱등성을 보장하기 위해서는 파일을 새로 만드는 등 개발 리소스가 발생하고, 코드가 불필요하게 복잡해져요. 뿐만 아니라 동시 요청은 사용자의 정상적인 동작 범위를 벗어난 경우라 판단해 현재 규모에서는 이 정도 방어로 충분하다고 판단했어요.
'Side Project > Hearu' 카테고리의 다른 글
| 해지 된 Google Play Console 계정에 계정 등록 결제했을 때 환불 받는 방법 | Hearu 프로젝트 (0) | 2026.05.28 |
|---|---|
| 테스트 코드 도입 배경기 | Hearu 프로젝트 (0) | 2026.05.27 |
| 로그 레벨 설정 | Hearu 프로젝트 (0) | 2026.05.23 |
| AI 모델 선택 과정 | Hearu 프로젝트 (0) | 2026.05.22 |
| 프롬프트 테스트 자동화 | Hearu 프로젝트 (1) | 2026.05.20 |