
팀 프로젝트에서 로컬 개발 환경을 세팅하던 중 동시성 이슈(StaleObjectStateException)로 애플리케이션 기동이 실패했습니다.
처음엔 단순 트러블슈팅처럼 보였지만, 해결 과정에서 자연스럽게 “실제 서비스라면 어떻게 설계해야 할까?”까지 고민이 확장되어 정리해보았습니다.
로컬에서 애플리케이션을 실행하자마자 아래 오류가 발생하며 기동이 실패했습니다.
StaleObjectStateException: Row was updated or deleted by another transaction
원인을 추적해보니 초기 데이터 자동 입력을 담당하는 데이터 시더(Seeder)가 문제였습니다.
Comment(댓글) 데이터를 넣는 과정에서, 부모 엔티티인 Article(기사)의 commentCount를 업데이트하는 로직이 함께 실행되고 있었는데요.
이 과정에서 같은 Article 레코드를 여러 트랜잭션이 동시에 수정하려고 하면서 충돌이 발생했습니다.
즉, “로컬 실행 → 시딩 → 동일 Row 업데이트 경합 → 예외 → 애플리케이션 종료” 흐름으로 개발 환경 세팅 자체가 막혀버렸습니다.
JPA에서는 동일 엔티티를 여러 트랜잭션이 동시에 갱신할 경우, 먼저 커밋된 변경으로 인해 이후 트랜잭션의 update가 실패하면서
StaleObjectStateException이 발생할 수 있습니다.
핵심 원인은 시더 실행 순서가 보장되지 않는 상태에서, 동일 레코드를 건드리는 작업이 겹쳤다는 점이었습니다.
그래서 모든 시더를 한 곳에서 통제하는 AllDataSeederRunner를 도입해 순차 실행을 강제했습니다.
@Profile("dev")
@Component
@RequiredArgsConstructor
public class AllDataSeederRunner {
private final List<DataSeeder> seeders;
@PostConstruct
public void runAllSeeders() {
seeders.stream()
.sorted(Comparator.comparingInt(this::getOrder))
.forEach(DataSeeder::seed);
}
private int getOrder(DataSeeder seeder) {
if (seeder instanceof UserDataSeeder) return 1;
if (seeder instanceof InterestDataSeeder) return 2;
if (seeder instanceof ArticleDataSeeder) return 3;
if (seeder instanceof CommentDataSeeder) return 4;
if (seeder instanceof CommentLikeDataSeeder) return 5;
return 99;
}
}
이렇게 실행 순서를 명확히 하니 충돌 없이 데이터가 정상 입력되었고, 애플리케이션도 정상 기동되었습니다. 😎
순차 실행을 보장함으로써 동일 레코드에 대한 경합을 원천 차단할 수 있었습니다.
덕분에 팀원 모두가 로컬에서 매번 동일하고 안정적인 초기 데이터를 기준으로 개발을 진행할 수 있게 되었습니다.
시더 문제는 순차 실행으로 해결했지만, "수만 명의 유저가 사용하는 실제 서비스라면?" 이야기가 완전히 달라질 수 있습니다.
예를 들어 회원가입/결제처럼 중복 요청이 들어오면 치명적인 도메인에서는, 단순한 if (exists) 체크만으로 동시성을 막을 수 없습니다.
유저가 가입 버튼을 여러 번 누르거나 네트워크 문제로 요청이 중복 전송되는 상황을 상상해보겠습니다.
-> 결과: 동일 아이디 유저가 중복 생성될 수 있음 😱
동시성 이슈를 막는 방법은 상황에 따라 달라지지만, 크게 아래 3가지 축으로 정리할 수 있습니다.
@Version으로 버전을 비교해, 누군가 먼저 수정했다면 “충돌 → 재시도/실패 처리”로 대응합니다. (충돌이 드물고 재시도 가능한 경우 유리)SELECT ... FOR UPDATE처럼 조회 시점부터 락을 걸어 다른 트랜잭션 접근을 막습니다.이번 경험을 통해 가장 크게 느낀 건,
여러 번 호출돼도 결과는 같아야 한다(멱등성)
라는 원칙이 시스템 안정성의 핵심이라는 점이었습니다.
예를 들어 결제 시스템이라면,
단순한 시더 오류 해결에서 시작된 고민이었지만, 동시성은 결국 시스템의 신뢰성을 결정짓는 방어선이라는 점을 깊이 체감했습니다.
앞으로도 “기능이 돌아가는 코드”를 넘어, 유저의 예측 불가능한 행동과 네트워크 불확실성까지 고려하는 방어적 설계를 습관화해야 할 것 같습니다.🙌