본문 바로가기

Project

복합 유니크 제약 조건의 필요성

1. 문제상황

오먹추 프로젝트에서 "사용자가 하루에 한 번 특정 카테고리의 메뉴에만 투표할 수 있다"는 요구사항이 있었다.
예를 들어 BREAKFAST 카테고리에서 투표는 1번만 진행 가능, 하지만 LUNCH 때는 투표를 다시 1회 가능.

그래서 다음과 같은 제약을 의도했다

  • 한 유저는 하루에 한 번만 투표할 수 있어야 한다
  • 같은 날 같은 카테고리에 중복 투표가 불가능해야 한다.
왜 이 제약 조건이 중요한가?

예를 들어 오먹추에서 투표 결과를 집계할 때 
user_id + category + vote_date가 중복되면 잘못된 순위 계산이나 추천 음식의 왜곡이 발생할 수 있다.

 

처음엔 이 제약을 서비스 로직에서만 체크했다.

if (voteRepository.existsByUSerIdAndCategoryAndVoteDate(...)) {
     throw new ILLegalStateException("이미 투표를 완료하셨습니다");
}

하지만 테스트 중 Postman으로 빠르게 2번 요청을 보내보니 중복 투표가 DB에 저장되는 상황이 발생했다.


2. 원인분석

표면적으로는 서비스 로직에서 중복을 체크하고 있었지만 사실 이는 트랜잭션 경합이나 동시성에 취약한 구조였다.

 

문제 핵심

  1. 사용자는 투표 API에 두 번 요청을 보냄(동시에)
  2. 컨트롤러 -> 서비스 -> existsByUserIdAndCategoryAndVoteDate(...) 검사
  3. 두 요청이 동시에 false를 반환함 (DB에는 아직 저장된 데이터가 없음)
  4. 두 요청이 각각 voteRepository.save(...) 호출
  5. 둘 다 저장됨
    => 동일한 (userId, category, date) 조합이 두 개 DB에 저장됨

즉 애플리케이션 레벨에서는 중복을 막을 수 없고, 가장 마지막 방어선인 DB에 안전장치가 필요했다.


3. 해결과정

해결 방법은 바로 복합 유니크 제약 조건을 DB 테이블에 직접 설정하는 것이었다.

JPA에서는 @Column(unique = true) 로는 복합 조건을 걸 수 없기 때문에 @Table(uniqueConstraints = ...) 를 사용해야 했다.

 

적용한 코드

@Entity
@Table(name = "votes",
        uniqueConstraints = @UniqueConstraint(columnNames = {"user_id", "category", "vote_date"}))
@Getter
@NoArgsConstructor
public class Vote {
	...
}

 

이렇게 하자 다음과 같은 장점이 생겼다.

  • 중복 요청이 동시에 들어와도 DB에서 제약 조건 위반으로 막아줌
  • 서비스 코드에서의 중복 검사와 DB의 무결성 제약이 함께 작동함
  • 이로써 데이터 정합성이 보장됨

4. 결론 및 회고

  • JPA에서 단일 필드의 유일성은 @Column(unique=true)로 해결되지만
    비지니스 로직에 해당하는 복합 제약 조건은 반드시 @Table 수준에서 명시해야 한다.