동시성의 기본 개념과 제어 전략

코-드 텐카이·2025년 1월 19일

Spring Boot

목록 보기
7/10

1. 동시성의 기초

동시성(Concurrency)

여러 주체가 하나의 자원에 동시에 접근하려고 할 때 발생하는 상황,
'공유 자원에 대한 동시 접근'입니다.

// 동시성 문제의 실제 예시
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원)
  • 여러 주체(스레드)가 공유 자원에 접근할 때 발생
  • 데이터의 일관성을 해칠 수 있는 상황
  • 의도하지 않은 결과를 초래할 수 있는 상황

대표적인 동시성 이슈 상황

  1. 데이터 정합성 깨짐
    • 여러 트랜잭션이 동시에 같은 데이터를 수정
    • 예: 은행 계좌 잔액, 상품 재고량
// 재고 관리의 예
public class InventoryService {
    private int stock = 100;

    public void decreaseStock() {
        if (stock > 0) {
            // 다른 스레드가 이 지점에서 개입 가능
            stock = stock - 1;
        }
    }
}
  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);
    }
}
  1. 데이터 유실
    • 동시 갱신으로 인한 데이터 덮어쓰기
    • 예: 좋아요 수 카운팅, 조회수 증가
// 데이터 유실이 발생할 수 있는 코드
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 내부에서 가장 기본적인 동시성을 제어합니다. 개발자가 직접 제어할 수 있는 가장 낮은 수준의 동시성 제어이며, 세밀한 제어가 가능하지만 그만큼 신중한 설계와 구현이 필요합니다.

  • JVM 힙 메모리 내에서 발생하는 동시성 제어
  • 가장 기본적이면서도 직접적인 제어 방식
  • 주요 메커니즘: synchronized, volatile, atomic 클래스
  • 적합한 케이스: 단일 서버, 메모리 캐시 데이터 관리
  • 특징: 가장 빠른 성능, 단순한 구현, 단일 JVM으로 제한

프레임워크 레벨

Spring과 같은 프레임워크가 제공하는 추상화된 동시성 제어 메커니즘을 사용하는 방식입니다. @Transactional, @Async와 같은 선언적 방식으로 동시성을 제어하며, 내부적으로는 애플리케이션 레벨의 동시성 제어를 사용합니다. 개발 생산성이 높고 검증된 방식이지만, 프레임워크가 제공하는 기능 안에서만 사용이 가능합니다.

  • 프레임워크가 제공하는 추상화된 동시성 제어
  • 선언적 방식으로 동시성 제어 가능
  • 주요 메커니즘: @Transactional, @Async, AOP
  • 적합한 케이스: 일반적인 웹 애플리케이션의 트랜잭션 처리
  • 특징: 개발 생산성 높음, 검증된 방식, 프레임워크 의존성

데이터베이스 레벨

데이터베이스 시스템이 제공하는 트랜잭션과 락 기반의 동시성 제어 메커니즘을 활용하는 방식입니다.
데이터베이스는 영속성을 가진 데이터의 정합성을 보장하는 가장 중요한 계층입니다. 따라서 가장 강력한 동시성 제어 메커니즘을 제공하며, 이는 비즈니스의 신뢰성과 직결됩니다.
각 데이터베이스는 서로 다른 방식의 락킹 메커니즘과 격리 수준을 제공합니다. MySQL, PostgreSQL, Oracle 등은 각자의 특성에 맞는 동시성 제어 방식을 가지고 있어, 선택한 데이터베이스의 특성을 잘 이해하고 활용하는 것이 중요합니다.
또한 Redis와 같은 인메모리 데이터베이스도 자체적인 동시성 제어 메커니즘을 제공하여, 고성능이 필요한 캐시나 세션 관리 등에 활용될 수 있습니다.

  • 데이터 영속성 계층에서의 동시성 제어
  • 가장 강력한 데이터 정합성 보장
  • 주요 메커니즘: 트랜잭션 격리 수준, 락
  • 적합한 케이스: 데이터 정합성이 매우 중요한 경우
  • 특징: 강력한 일관성, 성능 오버헤드 있음

분산 환경 레벨

여러 서버나 서비스 간의 동시성 문제를 분산 시스템 아키텍처를 통해 제어하는 방식으로 가장 복잡한 레벨입니다. MSA 환경에서는 단순히 하나의 서버나 데이터베이스의 동시성 제어만으로는 해결할 수 없는 문제들이 발생합니다. 분산 락이나 이벤트 기반 아키텍처를 통해 이를 해결하며, 데이터의 일관성과 가용성 사이의 트레이드오프를 고려해야 합니다. 높은 확장성을 제공하지만, 그만큼 구현이 복잡하고 일관성 보장이 어려워 신중한 설계가 필요합니다.

  • 여러 서버에 걸친 동시성 제어
  • 가장 복잡하지만 확장성 높음
  • 주요 메커니즘: 분산 락, 이벤트 소싱
  • 적합한 케이스: MSA 환경, 대규모 트래픽 처리
  • 특징: 높은 확장성, 복잡한 구현, 일관성 보장 어려움

동시성 제어 전략 수립을 위한 고려사항

동시성 제어 전략을 선택할 때는 다루는 데이터의 특성과 시스템의 요구사항을 면밀히 분석해야 합니다. 각 요소들은 서로 밀접하게 연관되어 있으며, 이들의 상호작용을 이해하는 것이 중요합니다.

데이터 특성에 따른 분석

데이터의 중요도와 정합성

데이터의 중요도는 어느 수준의 일관성을 보장해야 하는지를 결정합니다.

금융 데이터나 재고 데이터의 경우:

  • 데이터 부정확성이 직접적인 손실로 이어짐
  • 모든 트랜잭션이 완벽하게 직렬화되어야 함
  • 강력한 격리 수준과 비관적 락 전략이 필요
  • 성능이 다소 희생되더라도 정확성 우선

통계 데이터나 로그 데이터의 경우:

  • 일시적인 불일치나 근사값 허용 가능
  • 최종적 일관성 보장으로 충분
  • 성능 최적화를 위해 느슨한 결합 가능
  • 비동기 처리나 메시지 큐 활용 가능

데이터 갱신 패턴

데이터가 얼마나 자주, 어떤 패턴으로 변경되는지는 락 전략 선택에 큰 영향을 미칩니다.

빈번한 갱신이 발생하는 경우:

  • 락 경합이 성능에 직접적 영향
  • 락 점유 시간 최소화가 중요
  • 낙관적 락 사용시 충돌과 재시도 비용 증가
  • 분산 캐시나 메모리 캐시 활용 검토 필요

갱신이 드문 경우:

  • 락으로 인한 성능 저하 우려 적음
  • 낙관적 락으로 충분한 처리 가능
  • 버전 관리를 통한 충돌 감지 효과적

시스템 요구사항에 따른 분석

시스템 아키텍처 확장성

시스템의 확장 방향성은 동시성 제어 전략의 범위를 결정합니다.

수평적 확장이 필요한 경우:

  • 분산 환경에서의 데이터 동기화 고려
  • 분산 락 매커니즘 도입 필요
  • 이벤트 기반 아키텍처 검토
  • CAP 이론에 따른 트레이드오프 분석

단일 서버로 충분한 경우:

  • JVM 레벨의 동시성 제어로 충분
  • 단순한 락 매커니즘 사용 가능
  • 프레임워크가 제공하는 기능 활용

이러한 요소들을 종합적으로 고려하여, 상황에 가장 적합한 동시성 제어 전략을 수립해야 합니다.

0개의 댓글