-
복합 유니크 제약조건으로 중복 데이터 막기프로젝트, 트러블슈팅 2026. 3. 1. 23:27

시작하면서
프로젝트에서 리뷰 도움돼요 기능을 만들면서 같은 사람이 같은 리뷰에 도움돼요를 중복으로 누르는 걸 막아야 했다. 처음엔 서비스 로직에서 중복 체크를 하려 했는데, DB 레벨에서 아예 막는 게 더 확실하다는 걸 알게 됐다. 복합 유니크 제약조건을 쓰면 된다.
유니크 제약조건
특정 컬럼의 값이 테이블에서 딱 한 번만 존재해야 한다는 규칙이다. DB가 직접 막아주기 때문에 서비스 로직에서 중복 체크를 하지 않아도 된다.
가장 흔한 예시가 이메일이다.
@Column(unique = true) private String email;이렇게 하면 같은 이메일로 두 번 가입하려 할 때 DB에서 바로 에러를 던진다. 이게 단일 유니크 제약조건이다.
복합 유니크가 필요한 이유
도움돼요 기능에서 단일 유니크를 쓰면 문제가 생긴다.
@Column(unique = true) private Long reviewId; // 리뷰 ID만 유니크 @Column(unique = true) private Long userId; // 유저 ID만 유니크reviewId에 유니크를 걸면 리뷰 하나에 도움돼요를 딱 한 명만 누를 수 있게 된다. userId에 유니크를 걸면 한 사람이 도움돼요를 딱 하나만 누를 수 있게 된다. 둘 다 말이 안 된다.
우리가 원하는 건 같은 사람이 같은 리뷰에 도움돼요를 두 번 누르는 것만 막는 것이다. reviewId + userId 조합이 중복되는 걸 막아야 한다. 이게 복합 유니크 제약조건이다.
실제 코드
@Entity @Table( name = "review_helpful", uniqueConstraints = { @UniqueConstraint( name = "uk_review_helpful_review_user", columnNames = {"review_id", "user_id"} ) } ) public class ReviewHelpful { @ManyToOne private Review review; @ManyToOne private User user; }@Table의 uniqueConstraints에 두 컬럼을 같이 넣으면 된다. 이렇게 하면 DB에 이런 제약조건이 걸린다.
CONSTRAINT uk_review_helpful_review_user UNIQUE (review_id, user_id)
어떻게 동작하냐
review_id user_id 결과
1 영희 ✅ 성공 1 철수 ✅ 성공 (user_id가 다름) 2 영희 ✅ 성공 (review_id가 다름) 1 영희 ❌ 에러 (조합이 이미 존재) review_id가 같아도 user_id가 다르면 통과고, 반대도 마찬가지다. 두 값의 조합이 완전히 같을 때만 막는다.
서비스에서 예외 처리
중복 저장을 시도하면 DataIntegrityViolationException이 터진다.
try { reviewHelpfulRepository.save(new ReviewHelpful(review, user)); } catch (DataIntegrityViolationException e) { throw new AppException(ErrorCode.ALREADY_HELPFUL); }서비스 로직에서 미리 중복 체크를 할 수도 있지만, DB 제약조건이 있으면 최후의 보루가 생기는 거라 더 안전하다. 동시에 두 요청이 들어오는 경우엔 서비스 레벨 중복 체크를 통과해도 DB에서 막아준다.
마무리
복합 유니크 제약조건은 "A 단독으로는 중복 가능, B 단독으로는 중복 가능, A+B 조합은 중복 불가"를 구현할 때 쓴다. 서비스 로직으로 중복을 체크하는 것보다 DB 레벨에서 막는 게 더 확실하고, 동시성 문제에서도 안전하다.
'프로젝트, 트러블슈팅' 카테고리의 다른 글
도움돼요 토글, 비관적 락 대신 DB 원자적 UPDATE를 선택한 이유 (0) 2026.03.04 Redis 캐시에 빈 배열이 영구 저장되는 버그 트러블슈팅 (0) 2026.03.04 JPA N+1 문제와 @EntityGraph로 해결하기 (0) 2026.03.01 JWT 인증에서 Access Token, Refresh Token, Redis가 필요한 이유 (0) 2026.03.01 해커톤에서 OAuth2 로그인이 안 됐던 이유 (0) 2026.03.01