예약 생성 기능을 만들다 보면 이런 정책이 필요하다.
같은 날짜, 같은 시간, 같은 테마로는 중복 예약할 수 없다.
DB에는 보통 이런 유니크 제약 조건을 둔다.
UNIQUE (date, time_id, theme_id)
그렇다면 애플리케이션에서는 이 중복을 어디서 처리하는 게 좋을까?
✅ 서비스에서 먼저 검증하는 방식
서비스 계층에서 중복 여부를 조회한 뒤, 이미 존재하면 도메인 예외를 던질 수 있다.
if (reservationRepository.existsByDateAndTimeIdAndThemeId(
reservation.getDate(),
reservation.getTime().getId(),
reservation.getTheme().getId()
)) {
throw new DomainException(ErrorCode.RESERVATION_ALREADY_EXISTS);
}
이 방식은 비즈니스 정책이 서비스에 명확히 드러난다.
“같은 날짜, 시간, 테마의 예약은 만들 수 없다”는 규칙은 단순한 DB 오류라기보다 예약 도메인의 정책에 가깝다. 그래서 서비스 계층에서 검증하는 것이 읽기 쉽고 의도가 잘 보인다.
단점은 쿼리가 하나 더 나간다는 점이다.
- 중복 여부 조회
- 예약 저장
하지만 id나 인덱스가 잡힌 조건으로 조회한다면 현재 규모에서는 큰 부담이 아닐 가능성이 높다.
✅ 리포지토리에서 예외를 변환하는 방식
다른 방법은 저장을 바로 시도하고, DB의 유니크 제약 조건 위반 예외를 잡아 처리하는 것이다.
try {
reservationRepository.save(reservation);
} catch (DuplicateKeyException e) {
throw new DomainException(ErrorCode.RESERVATION_ALREADY_EXISTS);
}
이 방식은 쿼리를 줄일 수 있다.
중복이 없으면 insert 한 번으로 끝난다.
하지만 리포지토리에서 `DomainException`을 직접 던지는 것은 조금 어색하다.
Repository는 저장소 접근을 담당하는 계층이다. 그런데 그 안에서 도메인 정책 예외를 만들어 던지면, 인프라 계층이 도메인 정책을 너무 많이 알게 된다.
반대로 `DuplicateKeyException` 같은 인프라 예외를 서비스나 컨트롤러까지 그대로 흘려보내는 것도 좋지 않다. DB 구현 세부사항이 외부 계층에 새어나가기 때문이다.
✅ 내가 선호하는 방식
개인적으로는 서비스 계층에서 exists 조회로 중복을 검증하는 방식이 더 적절하다고 본다.
private void validateNotDuplicated(Reservation reservation) {
if (reservationRepository.existsByDateAndTimeIdAndThemeId(
reservation.getDate(),
reservation.getTime().getId(),
reservation.getTheme().getId()
)) {
throw new DomainException(ErrorCode.RESERVATION_ALREADY_EXISTS);
}
}
이렇게 하면 비즈니스 규칙이 서비스에 명확하게 남는다.
또 나중에 DB 유니크 제약 조건이 바뀌거나 제거되더라도, “중복 예약을 막는다”는 정책 자체는 서비스에 그대로 유지된다. 비즈니스 정책 변경이 아닌 저장소 구현 변경 때문에 서비스 흐름을 추적하기 어려워지는 상황을 줄일 수 있다.
그래도 DB 제약 조건은 필요하다
서비스 검증을 하더라도 DB 유니크 제약 조건은 유지하는 게 좋다.
서비스의 exists 검증은 사용자에게 좋은 에러 메시지를 주기 위한 1차 검증이고, DB 유니크 제약 조건은 최후의 안전망이다. 동시에 여러 요청이 들어오는 상황에서는 서비스에서 미리 exists 조회를 해도, 그 직후 다른 트랜잭션이 같은 예약을 넣을 수 있다.
정리하면 이렇게 볼 수 있다.
- 서비스 계층: 비즈니스 정책을 명시적으로 검증한다.
- DB 유니크 제약 조건: 데이터 정합성을 최종적으로 보장한다.
- 리포지토리: 가능하면 저장소 접근에 집중한다.
'우테코 8기 > 본과정 탐구 일지' 카테고리의 다른 글
| [레벨2 - 방탈출 사용자] 예약 정보 소프트 딜리트 도입기 (0) | 2026.05.31 |
|---|---|
| [레벨2 - 방탈출 사용자] Clock을 통해 테스트 상황에서 시간 제어하기 (0) | 2026.05.18 |
| [레벨2 - 방탈출 사용자] 삭제할 리소스가 없을 때도 204 No Content를 내려도 될까? (0) | 2026.05.10 |
| [레벨2 - 방탈출 사용자] 201 Created 응답에서 Location 헤더는 꼭 필요할까? (0) | 2026.05.10 |
| [레벨 2 - 방탈출 관리자] 테이블 구조 변경으로 인한 코드 수정을 효율적으로 하기 (0) | 2026.05.01 |