자바 스프링 애플리케이션에서 다중 스레드 환경에서의 동시성 제어는 필수적입니다. 이를 위해 자바는 synchronized 키워드와 ReentrantLock 클래스를 제공하며, 각기 다른 특징을 가지고 있습니다. 본 문서에서는 synchronized와 ReentrantLock의 장단점을 비교하고, 각각의 사용 시나리오에서 어떤 방법이 적합한지 분석합니다.
동시성 제어는 여러 스레드가 동일한 리소스에 접근할 때 데이터의 일관성과 무결성을 유지하기 위한 필수적인 기법입니다. 특히, 자바 스프링 애플리케이션은 웹 요청에 대해 다중 스레드로 처리되는 경우가 많아 동시성 제어의 중요성이 매우 큽니다.
synchronized: 자바의 기본적인 락 기법으로, 블록이나 메서드 단위로 사용되며, 락을 자동으로 획득하고 해제합니다.ReentrantLock:java.util.concurrent.locks패키지에서 제공하는 락 클래스입니다. 명시적으로 락을 획득(lock())하고 해제(unlock())해야 하며, 더 유연한 동시성 제어 기능을 제공합니다.
| 비교 항목 | synchronized |
ReentrantLock |
|---|---|---|
| 사용의 간편함 | 매우 간단하게 사용 가능, 블록/메서드에 키워드 추가만 필요 | 명시적으로 락을 획득하고 해제해야 하며 더 복잡함 |
| 재진입성 | 재진입 가능 | 재진입 가능 |
| 공정성 | 기본적으로 비공정 모드, 순서 보장 없음 | 공정 모드 설정 가능 |
| 타임아웃 | 지원하지 않음 | tryLock() 메서드를 통해 타임아웃 설정 가능 |
| 인터럽트 가능성 | 지원하지 않음 | lockInterruptibly()로 인터럽트 처리 가능 |
| 성능 | 간단한 동시성 제어에 더 나은 성능 | 상황에 따라 다르며, 공정성 설정 시 성능 저하 가능 |
| 디버깅 용이성 | 비교적 단순하여 디버깅이 용이함 | 복잡하여 디버깅이 어려울 수 있음 |
데이터베이스에서 동시성 제어는 트랜잭션이 여러 스레드 또는 프로세스에 의해 동시에 실행될 때 데이터 무결성을 유지하는 데 필수적입니다. 주로 사용되는 기법은 다음과 같습니다:
-
낙관적 락킹 (Optimistic Locking):
- 트랜잭션이 데이터베이스에서 데이터를 읽을 때 다른 트랜잭션이 해당 데이터를 수정하지 않았다는 가정하에 작업을 진행합니다.
- 주로 버전 필드를 사용하여 데이터의 변경 여부를 확인하고, 충돌이 발생하면 트랜잭션을 롤백합니다.
-
비관적 락킹 (Pessimistic Locking):
- 데이터에 대한 접근이 발생하기 전에 해당 데이터에 락을 걸어 다른 트랜잭션이 접근하지 못하도록 합니다.
- 데이터의 충돌이 예상되는 상황에서 주로 사용되며, 대기 시간이 길어질 수 있습니다.
-
Isolation Levels:
- DBMS는 트랜잭션의 격리 수준을 설정하여 서로 다른 트랜잭션 간의 영향을 제어합니다. 일반적으로 사용되는 격리 수준은 다음과 같습니다:
- READ UNCOMMITTED
- READ COMMITTED
- REPEATABLE READ
- SERIALIZABLE
- DBMS는 트랜잭션의 격리 수준을 설정하여 서로 다른 트랜잭션 간의 영향을 제어합니다. 일반적으로 사용되는 격리 수준은 다음과 같습니다:
이러한 방법을 통해 데이터베이스에서의 동시성 제어를 구현할 수 있으며, 특정 요구사항에 따라 적절한 방법을 선택하는 것이 중요합니다.
-
간단한 동시성 제어가 필요한 경우:
synchronized사용이 적합. 단순한 메서드 내 동기화.
-
복잡한 락 관리가 필요한 경우:
ReentrantLock사용이 적합. 타임아웃, 공정성, 인터럽트 처리가 필요할 때.
lock.lock(); try { // critical section } finally { lock.unlock(); }
-
데이터 무결성을 보장해야 하는 경우: -데이터베이스의 트랜잭션 및 락 메커니즘을 활용하는 것이 적합. ACID 속성을 보장하며, 동시성 충돌을 방지하기 위해 SELECT FOR UPDATE 쿼리 등을 사용할 수 있습니다.
BEGIN; SELECT * FROM user_points WHERE user_id = ? FOR UPDATE; UPDATE user_points SET points = points + ? WHERE user_id = ?; COMMIT;
-
높은 동시성을 처리해야 하는 경우: -데이터베이스에서 Optimistic Locking 또는 Pessimistic Locking을 고려. Optimistic Locking은 충돌이 적은 경우에 유용하며, Pessimistic Locking은 충돌 가능성이 높은 경우에 데이터를 잠가 안전성을 보장합니다. 예를 들어, JPA에서 @Version 애노테이션을 사용하여 낙관적 락을 구현할 수 있습니다.
@Entity public class UserPoint { @Id private Long id; @Version private Long version; private Long points; } -
성능과 무결성을 동시에 고려해야 하는 경우: -코드에서의 동시성 제어와 DB에서의 동시성 제어를 혼합하여 사용할 수 있습니다. 비즈니스 로직에서 동기화 필요성을 관리하고, 데이터베이스에서 추가적인 무결성 보장을 수행하여 최적의 성능과 안정성을 확보할 수 있습니다.
-
락 대기 시간이 중요한 경우: -
ReentrantLock의tryLock()을 사용하여 타임아웃 설정이 가능. -
공정성이 중요한 경우: -
ReentrantLock의 공정 모드를 사용하여 스레드 순서를 보장. -
인터럽트 가능한 락이 필요한 경우: -
ReentrantLock의lockInterruptibly()로 인터럽트 처리 가능. -
재귀적 메서드 호출이 필요한 경우: -재진입 특성을 제공하는 두 방식 모두 가능하지만,
ReentrantLock이 복잡한 시나리오에 적합.
-synchronized:
-간단한 락 상황에서 성능이 우수하며, 자원 접근이 빈번하지 않을 때 적합.
-ReentrantLock:
-공정성 설정, 타임아웃 등의 기능을 제공하지만, 복잡한 락 관리에서 성능 손실이 있을 수 있음.
- DB환경이 아니기에
transation과JPA를 이용한 방법은 제외 - 더 나은 성능:
-
Synchronized는 너무 많이 사용했을 때에 성능저하를 발생 - 공정성:
-공정성이란?
공정한 lock을 사용할 경우 | 경쟁 발생 -> 가장 오랫동안 기다린 쓰레드에게 lock을 제공
-
Synchronized와 달리ReentratLock은 공정성 설정이 가능 - ConcurrentHashMap:
-
ReentrantLock과ConcurrentHashMap을 함께 사용하면 스레드 안전성을 유지하면서도 성능을 최적화할 수 있는 유연한 동시성 관리가 가능 / 각 도구의 장점을 활용하여 복잡한 동시성 요구 사항을 효과적으로 처리 가능
-간단한 동기화는 synchronized가 더 간편하고 성능적으로 유리.
-그러나 타임아웃, 공정성, 인터럽트 처리 등이 필요한 경우에는 ReentrantLock을 사용하는 것이 적합.