1. 동시성 제어(Concurrency Control)란?
동시성 제어는 여러 프로세스나 스레드가 동시에 동일한 자원(데이터베이스, 파일 등)을 접근할 때 발생할 수 있는 문제를 방지하고, 데이터의 일관성과 무결성을 유지하기 위한 기술과 방법을 말합니다.
2. 동시성 문제의 예시
-
Dirty Read
- 한 트랜잭션이 처리 중인 데이터를 다른 트랜잭션이 읽는 경우
- 트랜잭션이 롤백되면 잘못된 데이터를 읽게 됩니다.
-
Lost Update
- 두 트랜잭션이 같은 데이터를 동시에 수정하고, 한 트랜잭션의 변경 사항이 덮어씌워지는 문제
-
Non-Repeatable Read
- 같은 데이터를 두 번 읽을 때 값이 달라지는 상황
-
Phantom Read
- 첫 번째 쿼리에서는 없었던 데이터가, 두 번째 쿼리에서 보이는 문제
3. 동시성 제어의 방법
1) 비관적 잠금 (Pessimistic Lock)
- 데이터를 수정하거나 조회하기 전에 잠금을 걸어 다른 트랜잭션이 접근하지 못하도록 합니다.
- 장점: 동시성 문제가 거의 발생하지 않음
- 단점: 트랜잭션 충돌이 적은 경우에도 성능 저하
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT p FROM Product p WHERE p.id = :id")
Product findByIdForUpdate(@Param("id") Long id);
2) 낙관적 잠금 (Optimistic Lock)
- 데이터를 수정할 때, 다른 트랜잭션이 변경하지 않았는지 확인하는 방식
- 주로 버전 번호(version)를 사용해 충돌 여부를 판단
- 장점: 충돌 가능성이 적은 경우 성능이 뛰어남
- 단점: 충돌 발생 시 재시도 로직 필요
@Entity
public class Product {
@Id
@GeneratedValue
private Long id;
@Version
private Integer version;
}
3) 데이터베이스 수준의 동시성 제어
- 데이터베이스에서 제공하는 트랜잭션 격리 수준을 설정
- 트랜잭션 격리 수준: Read Uncommitted, Read Committed, Repeatable Read, Serializable
- 격리 수준이 높을수록 동시성 문제가 줄어들지만 성능은 저하됩니다
4. 동시성 제어 구현
Java의 동시성 제어
-
Synchronized
- 특정 코드 블록에 대한 스레드 간 상호 배제를 보장
public synchronized void increment() {
count++;
}
-
ReentrantLock
- 명시적으로 락을 제어하며, 더 세부적인 동시성 처리가 가능
Lock lock = new ReentrantLock();
lock.lock();
try {
} finally {
lock.unlock();
}
Spring과 동시성 제어
-
@Transactional
- 데이터베이스 트랜잭션을 관리하고 동시성 문제를 완화
- 기본적으로 Read Committed 격리 수준을 사용
-
Redis 및 분산 락
- 다중 서버 환경에서 분산 락을 통해 동시성 제어
- 예:
Redisson 라이브러리를 사용
RLock lock = redissonClient.getLock("lockKey");
lock.lock();
try {
} finally {
lock.unlock();
}
5. 마무리
- 처음에는 동시성 제어가 어렵게 느껴졌지만, 비관적 잠금과 낙관적 잠금을 배우면서 상황에 맞는 선택이 중요하다는 것을 알게 되었습니다.
- 특히 낙관적 잠금은 성능을 유지하면서도 동시성 문제를 해결할 수 있어 효율적인 방법임을 배웠습니다.
- 또한, Redis를 활용한 분산 락 구현을 통해 동시성 제어가 단일 서버뿐 아니라 분산 환경에서도 필요함을 느꼈습니다.
- 앞으로 다양한 동시성 문제를 접하며, 더 효율적이고 안전한 코드를 작성하고 싶습니다.