ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 복합 유니크 제약조건으로 중복 데이터 막기
    프로젝트, 트러블슈팅 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 레벨에서 막는 게 더 확실하고, 동시성 문제에서도 안전하다.

Designed by Tistory.