여러 주체가 하나의 자원에 동시에 접근하려고 할 때 발생하는 상황,
즉 '공유 자원에 대한 동시 접근'입니다.
// 동시성 문제의 실제 예시
public class BankAccount {
private int balance = 1000; // 잔액
public void withdraw(int amount) {
// 두 스레드가 동시에 이 메서드를 호출할 때
if (balance >= amount) {
// 잔액 검사 후 실제 차감 전에 다른 스레드가 끼어들 수 있음
balance = balance - amount;
}
}
}
// 실행 시나리오
BankAccount account = new BankAccount(); // 잔액 1000원
Thread t1 = new Thread(() -> account.withdraw(800)); // 800원 출금 시도
Thread t2 = new Thread(() -> account.withdraw(800)); // 동시에 800원 출금 시도
// 두 스레드가 모두 잔액 검사를 통과할 수 있음
// 결과적으로 1600원이 출금될 수 있음 (잔액 -600원)
// 재고 관리의 예
public class InventoryService {
private int stock = 100;
public void decreaseStock() {
if (stock > 0) {
// 다른 스레드가 이 지점에서 개입 가능
stock = stock - 1;
}
}
}
// 주문 금액 계산의 예
public class OrderService {
public BigDecimal calculateTotal(Long orderId) {
Order order = findOrder(orderId); // 조회 시점의 데이터
// 이 사이에 상품 가격이 변경될 수 있음
return order.getItems().stream()
.map(item -> item.getPrice())
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
}
// 데이터 유실이 발생할 수 있는 코드
public class PostService {
private int likeCount = 0;
public void addLike() {
// Thread 1과 Thread 2가 동시에 이 메서드 호출 시
int currentCount = likeCount; // 둘 다 0을 읽음
// 여기서 context switching 발생 가능
likeCount = currentCount + 1; // 둘 다 1을 저장
// 예상값은 2이지만, 실제로는 1이 저장됨 (데이터 유실)
}
}
동시성 이슈는 시스템의 여러 계층에서 발생하며, 각 계층별로 적합한 해결 방식이 다릅니다.
Java 언어가 제공하는 저수준의 동시성 제어 메커니즘을 직접 사용하는 방식입니다. synchronized, volatile, Atomic 클래스 등을 통해 JVM 내부에서 가장 기본적인 동시성을 제어합니다. 개발자가 직접 제어할 수 있는 가장 낮은 수준의 동시성 제어이며, 세밀한 제어가 가능하지만 그만큼 신중한 설계와 구현이 필요합니다.
Spring과 같은 프레임워크가 제공하는 추상화된 동시성 제어 메커니즘을 사용하는 방식입니다. @Transactional, @Async와 같은 선언적 방식으로 동시성을 제어하며, 내부적으로는 애플리케이션 레벨의 동시성 제어를 사용합니다. 개발 생산성이 높고 검증된 방식이지만, 프레임워크가 제공하는 기능 안에서만 사용이 가능합니다.
데이터베이스 시스템이 제공하는 트랜잭션과 락 기반의 동시성 제어 메커니즘을 활용하는 방식입니다.
데이터베이스는 영속성을 가진 데이터의 정합성을 보장하는 가장 중요한 계층입니다. 따라서 가장 강력한 동시성 제어 메커니즘을 제공하며, 이는 비즈니스의 신뢰성과 직결됩니다.
각 데이터베이스는 서로 다른 방식의 락킹 메커니즘과 격리 수준을 제공합니다. MySQL, PostgreSQL, Oracle 등은 각자의 특성에 맞는 동시성 제어 방식을 가지고 있어, 선택한 데이터베이스의 특성을 잘 이해하고 활용하는 것이 중요합니다.
또한 Redis와 같은 인메모리 데이터베이스도 자체적인 동시성 제어 메커니즘을 제공하여, 고성능이 필요한 캐시나 세션 관리 등에 활용될 수 있습니다.
여러 서버나 서비스 간의 동시성 문제를 분산 시스템 아키텍처를 통해 제어하는 방식으로 가장 복잡한 레벨입니다. MSA 환경에서는 단순히 하나의 서버나 데이터베이스의 동시성 제어만으로는 해결할 수 없는 문제들이 발생합니다. 분산 락이나 이벤트 기반 아키텍처를 통해 이를 해결하며, 데이터의 일관성과 가용성 사이의 트레이드오프를 고려해야 합니다. 높은 확장성을 제공하지만, 그만큼 구현이 복잡하고 일관성 보장이 어려워 신중한 설계가 필요합니다.
동시성 제어 전략을 선택할 때는 다루는 데이터의 특성과 시스템의 요구사항을 면밀히 분석해야 합니다. 각 요소들은 서로 밀접하게 연관되어 있으며, 이들의 상호작용을 이해하는 것이 중요합니다.
데이터의 중요도는 어느 수준의 일관성을 보장해야 하는지를 결정합니다.
금융 데이터나 재고 데이터의 경우:
통계 데이터나 로그 데이터의 경우:
데이터가 얼마나 자주, 어떤 패턴으로 변경되는지는 락 전략 선택에 큰 영향을 미칩니다.
빈번한 갱신이 발생하는 경우:
갱신이 드문 경우:
시스템의 확장 방향성은 동시성 제어 전략의 범위를 결정합니다.
수평적 확장이 필요한 경우:
단일 서버로 충분한 경우:
이러한 요소들을 종합적으로 고려하여, 상황에 가장 적합한 동시성 제어 전략을 수립해야 합니다.