2026. 2. 9. 03:46ㆍBackend
이 글은 동시성 환경에서 자식 엔티티를 생성할 때, 부모 엔티티의 사전 존재 검증 쿼리가 과연 필요한지에 대해 다룹니다.
JPA의 existsById()기반의 사전 검증은 직관적으로 안전해 보이지만, 동시성 환경에서의 상태 변화를 보장하지 못합니다. 또한 InnoDB는 자식 엔티티 INSERT시 외래 키(FK) 제약 검증 과정에서 이미 부모 엔티티의 존재를 확인합니다.
이 글에서는
- InnoDB가 실제로 수행하는 검증 방식
- 사전 검증 쿼리가 동시성 문제를 해결하지 못하는 이유
- SELECT … FOR SHARE와 같은 락 기반 접근의 한계
- 고트래픽 환경에서의 현실적인 선택과 트레이드오프
를 살펴보고, 사전 검증이 의미 있는 경우와 그렇지 않은 경우를 구분해 정리합니다.
익숙한 검증의 필요성
부모–자식 관계의 엔티티를 생성할 때, 다음과 같이 사전 존재 검증 코드를 작성할 수 있습니다.
@Transactional
public CreateChildResponse create(Long parentId, CreateChildRequest request) {
if (!parentRepository.existsById(parentId)) {
/*
* 부모 엔티티가 존재하지 않으므로 자식 엔티티 생성 불가
* ex) 삭제된 게시글에는 댓글을 작성할 수 없습니다.
*/
throw new IllegalStateException("부모 엔티티가 존재하지 않습니다.");
}
Parent proxyParent = parentRepository.getReferenceById(parentId);
Child child = new Child(proxyParent, request);
Child saved = childRepository.save(child);
return CreateChildResponse.of(saved);
}
이 코드의 의도는 다음과 같습니다.
- 존재하지 않는 부모 엔티티에 대한 자식 생성 방지
- 의미 있는 예외를 빠르게 반환하여 사용자 경험 개선
하지만 동시성 환경에서도 이 검증은 여전히 의미가 있을까요?
자식 엔티티 생성 시, InnoDB가 실제로 하는 일
사전 검증 쿼리 parentRepository.existsById(parentId) 는 JPA에 의해 다음과 같은 SQL로 변환됩니다.
select 1 from parent where id = ? limit 1;
이 SELECT 는 InnoDB의 MVCC(Multi-Version Concurrency Control) 기반 스냅샷 읽기로 수행됩니다.
즉, 트랜잭션 격리 수준이 READ COMMITTED 또는 REPEATABLE READ 인 경우, 락을 획득하지 않고도 특정 시점의 일관된 데이터를 읽을 수 있습니다.
공식 문서에서도 이를 다음과 같이 명시하고 있습니다.
Consistent read is the default mode in which InnoDB processes SELECT statements in READ COMMITTED and REPEATABLE READ isolation levels. A consistent read does not set any locks on the tables it accesses, and therefore other sessions are free to modify those tables at the same time a consistent read is being performed on the table.
http://dev.mysql.com/doc/refman/8.4/en/innodb-consistent-read.html
MySQL :: MySQL 8.4 Reference Manual :: 17.7.2.3 Consistent Nonlocking Reads
17.7.2.3 Consistent Nonlocking Reads A consistent read means that InnoDB uses multi-versioning to present to a query a snapshot of the database at a point in time. The query sees the changes made by transactions that committed before that point in time, a
dev.mysql.com
따라서 existsById()에 의한 사전 검증 쿼리는 부모 엔티티의 ‘과거 어느 시점의 존재 여부’만 확인할 뿐 이후의 상태 변화(삭제, 수정)를 막아주지는 않습니다.
결과적으로 사전 검증 후, 다른 트랜잭션에 의해 부모 엔티티가 삭제되었다면 자식 엔티티의 생성은 FK 제약에 의해 실패하게 됩니다.

쓰기 작업을 막는 SELECT
InnoDB에는 스냅샷 읽기 외에도 락을 동반한 SELECT가 존재합니다.
다음과 같이 명시적으로 S-lock(Shared Lock)을 획득할 수 있습니다.
SELECT * FROM t FOR SHARE;
이 경우, 부모 row에 대해 공유 락(S-lock)이 걸리므로 다른 트랜잭션에서 해당 row를 UPDATE 또는 DELETE 하려면 대기해야 하므로 사전 검증 쿼리가 원래 의도대로 사용될 수 있어보입니다.
하지만 고트래픽의 자식 엔티티 생성 요청이 몰릴 경우, 부모 row에는 요청 수 만큼의 공유락이 누적되어, 다른 트랜잭션에서의 해당 row의 UPDATE/DELETE가 지연됩니다.

즉 FOR SHARE는 데이터 정합성은 강화시키지만, 쓰기 작업이 지연되는 문제가 발생합니다.
불필요한 검증인가?
이 시점에서 사전 검증 쿼리는 다음과 같은 특성을 가집니다.
- 정합성을 보장하지 못하고
- 동시성 예외를 제거하지 못하며
- DB가 어차피 수행하는 검증을 한 번 더 수행
결과적으로 사전 검증 쿼리가 무의미해 보일 수 있지만, 항상 불필요한 것은 아닙니다.
사전 검증이 의미 있는 경우
- 명확한 실패 원인을 규정하고 싶은 경우
- 악의적 요청을 빠르게 차단하고 싶은 경우
- 부모 엔티티가 비즈니스 정책에 의한 상태 검증이 필요한 경우
- 관리자 기능 또는 저빈도 API
- 사용자 친화적인 에러 메시지가 중요한 경우
사전 검증 제외를 고려해야 하는 경우
- 고트래픽 자식 엔티티 생성 경로
- 동시성 충돌 가능성이 높은 경우
- DB 제약에 의해 실패가 이미 보장되는 경우
예시: 게시글에 댓글 작성하기
다음은 게시글에 댓글을 작성하는 예시입니다.
@Transactional
public CreateCommentResponse create(Long memberId, Long postId, CreateCommentRequest request) {
if (!postRepository.existsById(postId)) {
throw new IllegalStateException("삭제된 게시글입니다.");
}
if (!memberRepository.existsById(memberId)) {
throw new IllegalStateException("탈퇴한 회원입니다.");
}
Post post = postRepository.getReferenceById(postId);
Member author = memberRepository.getReferenceById(memberId);
Comment comment = Comment.create(post, author, request);
Comment saved = commentRepository.save(comment);
return CreateCommentResponse.of(saved);
}
댓글 생성 요청에는 다음과 같은 특징이 있습니다.
- 인기 게시글에는 짧은 시간 동안 다수의 댓글이 동시에 작성될 수 있음
- 게시글 삭제는 상대적으로 저빈도로 발생하는 이벤트
- 비공개, 권한 제한과 같은 복잡한 상태가 없고 하드 삭제를 지원
- 삭제 시점 전후에는 사전 검증이 있어도 동시성 문제를 완전히 피할 수 없음
즉, 댓글 작성은 고트래픽 경로인 반면 게시글 삭제는 저빈도 이벤트라는 특성을 가집니다.
이러한 특성에서 사전 검증 쿼리는 동시성 문제를 해결하지 못하면서, 고트래픽 경로에 불필요한 SELECT 부하를 추가할 가능성이 있습니다.
댓글이 몰리는 인기 게시글이 삭제되는 경우
이 상황은 충분히 발생할 수 있지만, 항상 지속적으로 발생하는 패턴은 아닙니다.
대부분 다음과 같은 일시적인 레이스 컨디션에서 발생합니다.
시나리오
- 사용자가 게시글 페이지에 접근한다.
- 게시글을 조회한 뒤 댓글 작성을 시도한다.
- 작성자가 게시글을 삭제한다.
- UI 상으로는 접근이 차단되었지만, 이미 열린 탭, 네트워크 지연, 재시도 로직 등으로 인해 삭제 직후 일정 시간 동안 댓글 작성 요청이 계속 유입된다.
이 경우 비교 대상은 다음으로 축소할 수 있습니다.
- 사전 검증 SELECT가 실패하는 경우
- 댓글 INSERT가 FK 제약에 의해 실패하는 경우
사전 검증 vs FK 제약 실패
INSERT가 FK 제약에서 실패하더라도, InnoDB는 이미 쓰기 경로에 진입해 제약 검사를 수행하고 오류를 생성해 상위 계층으로 전달합니다.
이는 MVCC 기반의 SELECT miss보다 구조적으로 더 많은 비용을 수반합니다.
다만 이러한 비용 차이가 실제 서비스에서 명확한 성능 병목으로 드러나는지는 실패 비율과 요청 빈도에 따라 달라집니다.
대부분의 시스템에서는 해당 실패가 짧은 시간 동안만 발생하고, 전체 요청 중 차지하는 비율이 낮기 때문에 즉각적인 병목으로 관측되지는 않을 것입니다.
선택: 고트래픽 경로에서 사전 검증 제거
이러한 특성을 고려해, 댓글 작성과 같이 고트래픽이 예상되는 경로에서는
- 사전 검증 쿼리를 제거하고
- DB의 FK 제약에 실패를 위임한 뒤
- 예외를 명시적으로 처리하는 방식을 선택합니다.
이를 통해 요청당 쿼리 수를 3회(SELECT + SELECT + INSERT)에서 1회(INSERT)로 줄일 수 있습니다.
@Transactional
public CreateCommentResponse create(Long memberId, Long postId, CreateCommentRequest request) {
/*
if (!postRepository.existsById(postId)) {
throw new IllegalStateException("삭제된 게시글입니다.");
}
if (!memberRepository.existsById(memberId)) {
throw new IllegalStateException("탈퇴한 회원입니다.");
}
*/
Post post = postRepository.getReferenceById(postId);
Member author = memberRepository.getReferenceById(memberId);
Comment comment = Comment.create(post, author, request);
try {
Comment saved = commentRepository.save(comment);
return CommentResponse.of(saved);
} catch (DataIntegrityViolationException ex) {
throw new IllegalStateException("유효하지 않은 요청입니다.");
}
}
이 선택은 모든 상황에 대한 정답이라기보다는, 실제 병목이 발생할 가능성이 높은 지점에서의 합리적인 트레이드오프입니다.
사전 검증은 여전히 관리자 도구, 저빈도 API, 사용자 친화적인 오류 메시지가 중요한 영역에서는 의미 있는 선택이 될 수 있습니다
정리
- InnoDB의 MVCC 스냅샷 읽기 특성상, 사전 검증 SELECT는 동시성 환경에서 이후 상태 변화(삭제, 수정)를 보장하지 못한다.
- 사전 검증 쿼리는 부모 엔티티의 삭제를 막지 못하며, 결국 자식 엔티티 INSERT 시점에서 FK 제약에 의해 실패할 수 있다.
- SELECT … FOR SHARE를 통한 공유 락 획득은 정합성을 강화할 수 있지만, 고트래픽 자식 생성 경로에서는 부모 엔티티의 UPDATE·DELETE를 지연시키는 부작용을 동반한다.
- 따라서 사전 검증 쿼리는 동시성 안전성을 확보하기 위한 수단이라기보다, 비즈니스 규칙 표현이나 사용자 친화적인 오류 메시지가 중요한 경우에 제한적으로 사용하는 것이 합리적이다.
- 고트래픽이 예상되는 자식 엔티티 생성 경로에서는, 사전 검증을 제거하고 DB의 FK 제약에 실패를 위임한 뒤 예외를 명시적으로 처리하는 방식이 더 단순하고 효율적인 선택이 될 수 있다.
- 사전 검증은 여전히 관리자 도구, 저빈도 API, 사용자 친화적인 오류 메시지가 중요한 영역에서는 의미 있는 선택이 될 수 있습니다.
'Backend' 카테고리의 다른 글
| 댓글을 많이 쓰면 데드락? (0) | 2026.02.21 |
|---|---|
| 게시글 하나 삭제했는데 DELETE가 N번? (0) | 2026.02.02 |
| [Spring] 스프링부트 @Schedule 시간대 문제 해결 (1) | 2024.11.10 |
| [짧고 굵게 배우는 JSP 웹 프로그래밍과 스프링 프레임워크] 연습문제 5장 정답 (0) | 2024.07.07 |
| [짧고 굵게 배우는 JSP 웹 프로그래밍과 스프링 프레임워크] 연습문제 6장 정답 (0) | 2024.07.07 |