2026.04.14 - [Fivefy 프로젝트/트러블슈팅] - 동시 요청에서 발생할 수 있는 중복 데이터 문제와 DB 유니크 제약으로 해결
동시 요청에서 발생할 수 있는 중복 데이터 문제와 DB 유니크 제약으로 해결
문제 상황플레이리스트 트랙 추가 로직에서 동일 트랙 중복 추가는 서비스 코드로만 막고 있었고,position 값도 countByPlaylistId() + 1 방식으로 계산하고 있었다. CodeRabbit 리뷰를 통해 동시 요청 상황
sudaruuu.tistory.com
문제 상황
이전 글에서 DB 유니크 제약을 적용하고, DataIntegrityViolationException을 비즈니스 예외로 변환하도록 개선하였다.
하지만 실제 구현에서는 모든 제약 충돌을 하나의 에러로 처리하고 있었다.
catch (DataIntegrityViolationException e) {
throw new BusinessException(PlaylistErrorCode.PLAYLIST_TRACK_POSITION_CONFLICT);
}
이 방식에서는 어떤 제약이 깨졌는지 구분할 수 없었고,
- 트랙 중복인지
- 순서(position) 충돌인지
사용자에게 정확한 에러를 전달할 수 없는 문제가 있었다.
코드 리뷰 피드백
CodeRabbit는 다음과 같은 개선 방향을 제시하였다.
- DB 제약 충돌을 하나의 예외로 처리하지 말고, 원인에 따라 구분할 필요가 있다.
- 어떤 constraint가 깨졌는지를 기반으로 예외를 분리하는 것이 좋다.
- 클라이언트에게 보다 명확한 에러 메시지를 전달해야 한다.

원인 분석
1. DB 예외를 단일 비즈니스 예외로 처리하고 있음
기존 구현에서는 모든 DataIntegrityViolationException을 동일한 예외로 변환하고 있었다.
이로 인해 서로 다른 원인의 에러가 동일하게 처리되었다.
- (playlist_id, track_id) → 트랙 중복
- (playlist_id, position) → 순서 충돌
하지만 두 경우는 성격이 완전히 다른 문제임에도 불구하고, 동일한 에러 메시지로 처리되고 있었다.
2. DB constraint 정보를 활용하지 않음
DB는 예외 발생 시 어떤 제약 조건이 깨졌는지 정보를 포함하고 있다.
하지만 해당 정보를 활용하지 않고 단순히 예외 타입만 기준으로 처리하고 있었다.
해결 과정
1. constraint 이름 기반 예외 분기 처리
예외에서 constraint 이름을 추출하여 어떤 제약 조건이 깨졌는지 구분하도록 개선하였다.
private BusinessException handlePlaylistTrackConstraintException(DataIntegrityViolationException e) {
String constraintName = extractConstraintName(e);
if (UK_PLAYLIST_TRACK_PLAYLIST_TRACK.equalsIgnoreCase(constraintName)) {
return new BusinessException(PlaylistErrorCode.PLAYLIST_TRACK_ALREADY_EXISTS);
}
if (UK_PLAYLIST_TRACK_PLAYLIST_POSITION.equalsIgnoreCase(constraintName)) {
return new BusinessException(PlaylistErrorCode.PLAYLIST_TRACK_POSITION_CONFLICT);
}
throw e;
}
2. 예외에서 constraint 이름 추출 로직 추가
DB 예외 내부의 원인을 탐색하여 constraint 이름을 추출하도록 구현하였다.
private String extractConstraintName(Throwable throwable) {
Throwable current = throwable;
while (current != null) {
if (current instanceof ConstraintViolationException constraintViolationException) {
return constraintViolationException.getConstraintName();
}
current = current.getCause();
}
return null;
}
3. 서비스 로직에 예외 분기 적용
기존 단일 예외 처리에서 constraint 기반 분기로 변경하였다.
try {
PlaylistTrack savedPlaylistTrack = playlistTrackRepository.saveAndFlush(playlistTrack);
return PlaylistTrackResponse.from(savedPlaylistTrack);
} catch (DataIntegrityViolationException e) {
throw handlePlaylistTrackConstraintException(e);
}
결과
- 트랙 중복과 순서 충돌을 각각 다른 예외로 구분 가능해짐
- 클라이언트에 더 명확한 에러 메시지 전달 가능
- DB 제약조건을 단순 방어가 아닌 비즈니스 로직에 활용할 수 있게 됨
느낀 점
이전 글에서는 "DB 유니크 제약을 통해 데이터 무결성을 보장한다"는 개념에 집중했다면,
이번에는 한 단계 더 나아가 "DB 예외를 어떻게 의미 있게 사용할 것인가"에 대해 고민하게 되었다.
단순히 예외를 잡아서 처리하는 것이 아니라,
그 안에 포함된 정보(constraint name)를 활용하면 더 정확한 비즈니스 로직을 구성할 수 있다는 점을 알게 되었다.
결과적으로
- 서비스 레벨 → 사전 검증
- DB 제약 → 최종 무결성 보장
- 예외 처리 → 실패 원인을 의미 있게 전달
이라는 흐름을 연결해서 이해할 수 있게 되었고, 예외 처리 역시 하나의 중요한 설계 요소라는 것을 느끼게 되었다.
'Fivefy 프로젝트 > 트러블슈팅' 카테고리의 다른 글
| Soft Delete 기반 플레이리스트 정책 개선 (1) | 2026.04.22 |
|---|---|
| 유효 재생 기준 적용과 Projection 기반 집계 구조 개선 과정 (0) | 2026.04.21 |
| 스케줄러 기반 로직 테스트 설계와 운영 환경 고려 (0) | 2026.04.16 |
| 커버리지 100% 대신 의미 있는 테스트를 선택한 이유 (1) | 2026.04.15 |
| 동시 요청에서 발생할 수 있는 중복 데이터 문제와 DB 유니크 제약으로 해결 (2) | 2026.04.14 |