회사의 WAS 서버 API를 개발 도중 공유하는 데이터에 접근해 이를 할당받는 API가 있다.
이 API를 코드리뷰 하던 도중, 프로님 한 분이 동시성 이슈가 들 수 있다고 이야기해 주셔서 이에 관해 글을 정리해 보려 한다.
현재 사내 데이터베이스는 아래와 같이 구성되어 있다. (예시)
(테이블들, 프로젝트들이랑
동시성 제어와 동시성 제어 방안 분석
동시성 제어에는 여러 방안이 있지만 이번에는 낙관적 락, 비관적 락, Redis pub/sub(분산 락), kafka messaging 기법을 분석해 보았다.
동시성 제어 : 낙관적 락(Optimistic Lock)
트랜잭션 충돌이 적을 것이라 예상하고, 트랙잭션에 잠금을 걸지 않은 상태로 데이터를 조회하여,
수정하는 시점(트랜잭션의 최종 커밋 단계)에 검증해 충돌을 감지하는 동시성 제어 방식이다.
동시 요청 중에 한건만 성공해야 하는 케이스에 적합한 잠금 방식이며, 트랜잭션, Lock 설정 없이 데이터 정합성을 보장할 수 있으므로 성능적으로 우위에 있다.
낙관적 락은 버전 번호나 타임스탬프를 통해 트랜잭션 간 충돌을 확인하는데, 일반적으로 버전 번호를 사용한다.
JPA에서 낙관적 락을 처리하는 방법은 @Version 어노테이션을 이용해 처리한다.

이 어노테이션은 Jpa Entity에 버전 관리용 필드를 추가해 트랜잭션 내에서 처음 조회되었을 때의 버전와 이후 수정 후 커밋되는 시점의 버전을 비교한다.
비교한 결과 버전이 서로 다르다면 이는 충돌이 발생한 것으로 판단하고 ObjectOptimisticLockingFailureException을 발생시킨다. 해당 Exception을 핸들링하여 재시도 로직을 구현할 수 있다.

이 방법은 데이터 충돌 가능성이 적을 때 유용하며, 데이터가 많거나 자주 갱신되는 환경에서 비관적 락처럼 데이터 접근을 제한하지 않기 때문에 성능 이점이 크다. 하지만 충돌이 발생했을 때 충돌을 해결해야만 하는 유스케이스라면 필수적으로 재시도 로직을 구현해야 한다.
이로 인해 충돌이 잦은 경우에는 낙관적 락을 사용할 때 재시도에 따른 성능 트레이드오프를 고려해야 한다.
장점 : 데이터에 대한 접근을 미리 제한하지 않아 동시성 성능 ↑ , Blocking이 발생하지 않아 시스템 자원의 활용 효율 ↑
단점 : 충돌이 발생했을 때 트랜잭션을 롤백하고 재시도 로직이 수행될 수 있으므로, 충돌이 자주 발생하는 환경에서 성능 극 저하됨
구현 복잡도 : 중 (재시도 로직)
동시성 제어 : 비관적 락(Pessimistic Lock)
동시 요청에서 순차적으로 진행될 때 성공할 수 있는 요청이라면 성공시키는 케이스에 적합한 잠금 방식이다.(+ 하지만 순서를 "반드시" 보장하지는 않는다.)
데이터 충돌 가능성이 크거나, 자주 변경되는 데이터에 대한 높은 동시성 요청을 예상할 때 사용된다.
트랜잭션이 데이터를 사용하는 동안 다른 트랜잭션이 해당 데이터에 접근하는것을 제한 하기 때문에, 충돌이 발생할 가능성을 사전에 차단할 수 있다.
비관적 락은 구체적인 기능은 아니고, 트랜잭션의 접근 방지를 위한 데이터 선점 전략을 표현한 개념이다.
이 개념에 속하는 구체적인 구현 기능들이 배타락, 공유락이다.
비관적 락을 이해하기 위해서는 InnoDB의 row-level-lock과 배타락, 공유락에 대한 내용을 확실히 숙지하여야 한다.
(추가+ JPA에서는 table-level-lock을 지원하지 않는다.)
row-level-lock이란 데이터베이스에서 특정 행(row)단위로 락을 거는 방식이다. InnoDB에서는 기본적으로 row-level-lock으로 발생하는데, 특정 행(row)단위로 락을 거는 방식을 뜻한다.
배타락(Exclusive Lock, X-Lock)이란 데이터에 대한 쓰기를 포함한 모든 접근을 제한하는 강력한 잠금 방식이다. 이 락이 걸린 데이터는 오직 하나의 트랜잭션만이 접근할 수 있으며, 다른 트랜잭션은 읽기와 쓰기 모두 불가능하다.
배타락의 특징으로는 단독 접근만 허용한다는 것과, 데이터의 일관성과 무결성을 확실히 보장 가능하다는 것, 그리고 교착상태(DeadLock) 가능성이 있다는 것이다.

공유락(Shared Lock, S-Lock)이란 데이터에 대한 읽기 작업만 허용하는 락이다.
공유락이 설정된 데이터는 여러 트랜잭션이 동시에 읽을 수 있지만 쓰기 작업은 제한된다.
주로 읽기 작업이 많고, 데이터 변경이 드물거나 변경 시 일관성을 유지해야 할 때 사용된다.
공유락의 특징으로는 다중 접근을 허용한다는 것, 배타락보다 상대적으로 경합이 적다는 것이다.

++ 교착상태(Deadlock)이란??
아래와 같은 케이스일 때 데드락이 발생한다.
- 트랜잭션 A와 B가 자원 1과 자원 2에 대해 공유 락을 설정하여 동시에 읽기 작업을 수행하고 있다.
- 트랜잭션 A가 자원 1에 대해 배타적 락을 요청하여 업데이트를 시도하지만, 자원 2에서 트랜잭션 B의 공유 락이 해제되기를 기다린다.
- 반대로 트랜잭션 B도 자원 2에 대해 배타적 락을 요청하지만, 자원 1에서 트랜잭션 A의 공유 락이 해제되기를 기다린다.
++ 그러면 어떻게 데드락을 방지할까??

장점 : 데이터에 대한 접근을 엄격하게 제어하여 순차적으로 처리하기 때문에 데이터의 일관성과 무결성을 보장 가능하며 Dirty Read상황이 발생할 가능성이 없다.
단점 : 락을 획득하기 위해 대기하는 시간이 길어질 수 있기 땜누에 시스템의 성능에 부정적인 영향을 미칠 수 있으며 교착상태(DeadLock) 발생 가능성이 있다.
구현 복잡도 : 하 (JPA를 통한 쉬운 구현)
'DEV > 개발일기 || 트러블슈팅' 카테고리의 다른 글
| Outbox만으로 충분할까? CDC와 DLQ를 통한 기초적인 이벤트 기반 설계를 알아보자 (0) | 2025.10.07 |
|---|---|
| Coroutine과 ReactiveMongo 다중DB 환경에서 @Transactional을 사용해 보자 (0) | 2025.03.10 |
| 데이터 압축을 위한 Gorilla 알고리즘 적용 사례: 사내 솔루션 개발기 및 회고 (5) | 2024.09.03 |
| JPA에서 대용량 데이터를 읽거나 수정/삭제 할때, 쿼리를 어떻게 작성해야 할까? (1) | 2024.06.10 |
| pg의 멀티테넌시(스키마) 환경에서 스프링 배치를 어떻게 사용해야 할까? (0) | 2023.12.15 |