동시성 문제 해결의 핵심 개념인 JVM 레벨 동시성 제어와 DB 레벨 동시성 제어에 대해 함께 알아보겠습니다. 이 두 가지를 정확히 이해하고 상황에 맞춰 활용하는 것이 안정적이고 효율적인 시스템을 구축하는 데 매우 중요합니다.
이 부분은 자바 애플리케이션의 단일 JVM(메모리) 내부에서 여러 스레드가 공유하는 인메모리 데이터 (변수, 객체 등)의 일관성을 보장하기 위해 사용됩니다.
예시: 생산자-소비자 패턴 (JVM 레벨 동시성 제어)
다음 코드는 wait()와 notifyAll()을 활용하여 생산자 스레드와 소비자 스레드가 공유 큐를 안전하게 사용하는 예시입니다. 큐가 가득 차면 생산자는 대기하고, 큐가 비면 소비자가 대기하며 서로에게 알림을 보냅니다.
import java.util.LinkedList;
import java.util.Queue;
public class Main {
private static final int BUFFER_SIZE = 5;
private final Queue<Integer> queue = new LinkedList<>();
private final int maxSize;
public Main(int maxSize) {
this.maxSize = maxSize;
}
public synchronized void produce(int item) throws InterruptedException {
// 큐가 가득 찼으면 대기
while (queue.size() == maxSize) {
System.out.println("큐가 가득 찼어요! 생산자 대기 중...");
wait(); // 다른 스레드가 notify() 호출할 때까지 대기
}
// 아이템 추가
queue.add(item);
System.out.println("생산: " + item + " (큐 크기: " + queue.size() + ")");
// 소비자에게 알림
notifyAll();
}
public synchronized int consume() throws InterruptedException {
// 큐가 비었으면 대기
while (queue.isEmpty()) {
System.out.println("큐가 비었어요! 소비자 대기 중...");
wait(); // 다른 스레드가 notify() 호출할 때까지 대기
}
// 아이템 꺼내기
int item = queue.poll();
System.out.println("소비: " + item + " (큐 크기: " + queue.size() + ")");
// 생산자에게 알림
notifyAll();
return item;
}
public static void main(String[] args) {
Main example = new Main(BUFFER_SIZE);
// 생산자 스레드
Thread producer = new Thread(() -> {
try {
for (int i = 0; i < 20; i++) {
example.produce(i);
Thread.sleep(300); // 생산 속도 조절
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
// 소비자 스레드
Thread consumer = new Thread(() -> {
try {
for (int i = 0; i < 20; i++) {
example.consume();
Thread.sleep(100); // 소비 속도 조절 (생산보다 느림)
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
producer.start();
consumer.start();
}
}
이 영역은 DB(데이터베이스)에 저장된 영구 데이터에 여러 서버 인스턴스 또는 여러 애플리케이션의 트랜잭션들이 동시에 접근하여 변경할 때 데이터 무결성을 보장하기 위해 사용됩니다.
비관적 락 (Pessimistic Lock)
낙관적 락 (Optimistic Lock)
두 가지 동시성 제어 방식을 모두 이해하고 상황에 따라 적절히 결합해야 합니다.
대부분의 웹 애플리케이션에서 동시성 문제의 핵심은 DB 데이터를 여러 트랜잭션이 동시에 조작할 때 발생하기 때문에, 낙관적/비관적 락이 더 중요하고 빈번하게 사용됩니다. JVM 내부의 인메모리 데이터를 보호할 필요가 있을 때만 synchronized나 Lock 등을 추가적으로 사용하는 것이 일반적입니다.